diff --git a/apps/web/src/components/ui/alert-dialog/AlertDialogBase.tsx b/apps/web/src/components/ui/alert-dialog/AlertDialogBase.tsx new file mode 100644 index 000000000..d2848d8aa --- /dev/null +++ b/apps/web/src/components/ui/alert-dialog/AlertDialogBase.tsx @@ -0,0 +1,136 @@ +'use client'; + +import * as React from 'react'; + +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; + +import { buttonVariants } from '@/components/ui/buttons'; +import { cn } from '@/lib/utils'; + +function AlertDialog({ ...props }: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return ; +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger +}; diff --git a/apps/web/src/components/ui/alert-dialog/index.ts b/apps/web/src/components/ui/alert-dialog/index.ts new file mode 100644 index 000000000..7d9281753 --- /dev/null +++ b/apps/web/src/components/ui/alert-dialog/index.ts @@ -0,0 +1 @@ +export * from './AlertDialogBase'; diff --git a/apps/web/src/components/ui/report/ReportEditor.tsx b/apps/web/src/components/ui/report/ReportEditor.tsx index cad687426..78ef20402 100644 --- a/apps/web/src/components/ui/report/ReportEditor.tsx +++ b/apps/web/src/components/ui/report/ReportEditor.tsx @@ -6,6 +6,7 @@ import { Editor } from './Editor'; import { useReportEditor } from './useReportEditor'; import { useMemoizedFn } from '@/hooks'; import { ReportElements } from '@buster/server-shared/reports'; +import { cn } from '@/lib/utils'; interface ReportEditorProps { // We accept the generic Value type but recommend using ReportTypes.Value for type safety @@ -56,7 +57,7 @@ export const ReportEditor = React.memo( variant={variant} readonly={readOnly} disabled={disabled} - className={className}> + className={cn('pb-[20vh]', className)}> diff --git a/apps/web/src/components/ui/report/elements/AlignToolbarButton.tsx b/apps/web/src/components/ui/report/elements/AlignToolbarButton.tsx new file mode 100644 index 000000000..a0c210d6b --- /dev/null +++ b/apps/web/src/components/ui/report/elements/AlignToolbarButton.tsx @@ -0,0 +1,84 @@ +'use client'; + +import * as React from 'react'; + +import type { Alignment } from '@platejs/basic-styles'; +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { TextAlignPlugin } from '@platejs/basic-styles/react'; +import { + TextAlignCenter, + TextAlignJustify, + TextAlignLeft, + TextAlignRight +} from '@/components/ui/icons'; +import { useEditorPlugin, useSelectionFragmentProp } from 'platejs/react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; + +import { ToolbarButton } from './Toolbar'; + +const items = [ + { + icon: TextAlignLeft, + value: 'left' + }, + { + icon: TextAlignCenter, + value: 'center' + }, + { + icon: TextAlignRight, + value: 'right' + }, + { + icon: TextAlignJustify, + value: 'justify' + } +]; + +export function AlignToolbarButton(props: DropdownMenuProps) { + const { editor, tf } = useEditorPlugin(TextAlignPlugin); + const value = + useSelectionFragmentProp({ + defaultValue: 'start', + getProp: (node) => node.align + }) ?? 'left'; + + const [open, setOpen] = React.useState(false); + const IconValue = items.find((item) => item.value === value)?.icon ?? TextAlignLeft; + + return ( + + + + + + + + + { + tf.textAlign.setNodes(value as Alignment); + editor.tf.focus(); + }}> + {items.map(({ icon: Icon, value: itemValue }) => ( + + + + ))} + + + + ); +} diff --git a/apps/web/src/components/ui/report/elements/Comment.tsx b/apps/web/src/components/ui/report/elements/Comment.tsx index 970621400..de6dd43da 100644 --- a/apps/web/src/components/ui/report/elements/Comment.tsx +++ b/apps/web/src/components/ui/report/elements/Comment.tsx @@ -31,7 +31,7 @@ import { BasicMarksKit } from '../plugins/basic-marks-kit'; import { type TDiscussion, discussionPlugin } from '../plugins/discussion-kit'; import { EditorContainer } from '../EditorContainer'; -import { EditorContent as Editor } from '../Editor'; +import { Editor } from '../Editor'; export interface TComment { id: string; diff --git a/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx b/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx index 5417100c0..db0129296 100644 --- a/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx +++ b/apps/web/src/components/ui/report/elements/FixedToolbarButtons.tsx @@ -8,7 +8,11 @@ import { TextItalic, TextStrikethrough, TextUnderline, - WandSparkle + WandSparkle, + ArrowUpToLine, + TextColor2, + BucketPaint, + TextHighlight2 } from '@/components/ui/icons'; import { KEYS } from 'platejs'; import { useEditorReadOnly } from 'platejs/react'; @@ -23,6 +27,24 @@ import { SuggestionToolbarButton } from './SuggestionToolbarButton'; import { ToolbarGroup } from './Toolbar'; import { TurnIntoToolbarButton } from './TurnIntoToolbarButton'; import { UndoToolbarButton, RedoToolbarButton } from './UndoToolbarButton'; +import { ExportToolbarButton } from './ExportToolbarButton'; +import { ImportToolbarButton } from './ImportToolbarButton'; +import { InsertToolbarButton } from './InsertToolbarButton'; +import { FontSizeToolbarButton } from './FontSizeToolbarButton'; +import { FontColorToolbarButton } from './FontColorToolbarButton'; +import { AlignToolbarButton } from './AlignToolbarButton'; +import { + BulletedListToolbarButton, + NumberedListToolbarButton, + TodoListToolbarButton +} from './ListToolbarButton'; +import { ToggleToolbarButton } from './ToggleToolbarButton'; +import { TableToolbarButton } from './TableToolbarButton'; +import { EmojiToolbarButton } from './EmojiToolbarButton'; +import { MediaToolbarButton } from './MediaToolbarButton'; +import { LineHeightToolbarButton } from './LineHeightToolbarButton'; +import { IndentToolbarButton, OutdentToolbarButton } from './IndentToolbarButton'; +import { ModeToolbarButton } from './ModeToolbarButton'; export function FixedToolbarButtons() { const readOnly = useEditorReadOnly(); @@ -58,31 +80,31 @@ export function FixedToolbarButtons() { - + - + - + - + - + - + - + @@ -124,7 +146,7 @@ export function FixedToolbarButtons() { - + diff --git a/apps/web/src/components/ui/report/elements/FontSizeToolbarButton.tsx b/apps/web/src/components/ui/report/elements/FontSizeToolbarButton.tsx new file mode 100644 index 000000000..c7e11fee8 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/FontSizeToolbarButton.tsx @@ -0,0 +1,143 @@ +'use client'; + +import * as React from 'react'; + +import type { TElement } from 'platejs'; + +import { toUnitLess } from '@platejs/basic-styles'; +import { FontSizePlugin } from '@platejs/basic-styles/react'; +import { Minus, Plus } from '@/components/ui/icons'; +import { KEYS } from 'platejs'; +import { useEditorPlugin, useEditorSelector } from 'platejs/react'; + +import { PopoverBase, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { cn } from '@/lib/utils'; + +import { ToolbarButton } from './Toolbar'; + +const DEFAULT_FONT_SIZE = '16'; + +const FONT_SIZE_MAP = { + h1: '36', + h2: '24', + h3: '20' +} as const; + +const FONT_SIZES = [ + '8', + '9', + '10', + '12', + '14', + '16', + '18', + '24', + '30', + '36', + '48', + '60', + '72', + '96' +] as const; + +export function FontSizeToolbarButton() { + const [inputValue, setInputValue] = React.useState(DEFAULT_FONT_SIZE); + const [isFocused, setIsFocused] = React.useState(false); + const { editor, tf } = useEditorPlugin(FontSizePlugin); + + const cursorFontSize = useEditorSelector((editor) => { + const fontSize = editor.api.marks()?.[KEYS.fontSize]; + + if (fontSize) { + return toUnitLess(fontSize as string); + } + + const [block] = editor.api.block() || []; + + if (!block?.type) return DEFAULT_FONT_SIZE; + + return block.type in FONT_SIZE_MAP + ? FONT_SIZE_MAP[block.type as keyof typeof FONT_SIZE_MAP] + : DEFAULT_FONT_SIZE; + }, []); + + const handleInputChange = () => { + const newSize = toUnitLess(inputValue); + + if (Number.parseInt(newSize) < 1 || Number.parseInt(newSize) > 100) { + editor.tf.focus(); + + return; + } + if (newSize !== toUnitLess(cursorFontSize)) { + tf.fontSize.addMark(`${newSize}px`); + } + + editor.tf.focus(); + }; + + const handleFontSizeChange = (delta: number) => { + const newSize = Number(displayValue) + delta; + tf.fontSize.addMark(`${newSize}px`); + editor.tf.focus(); + }; + + const displayValue = isFocused ? inputValue : cursorFontSize; + + return ( +
+ handleFontSizeChange(-1)}> + + + + + + { + setIsFocused(false); + handleInputChange(); + }} + onChange={(e) => setInputValue(e.target.value)} + onFocus={() => { + setIsFocused(true); + setInputValue(toUnitLess(cursorFontSize)); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleInputChange(); + } + }} + data-plate-focus="true" + type="text" + /> + + e.preventDefault()}> + {FONT_SIZES.map((size) => ( + + ))} + + + + handleFontSizeChange(1)}> + + +
+ ); +} diff --git a/apps/web/src/components/ui/report/elements/ImportToolbarButton.tsx b/apps/web/src/components/ui/report/elements/ImportToolbarButton.tsx new file mode 100644 index 000000000..9ca7b6fc1 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/ImportToolbarButton.tsx @@ -0,0 +1,99 @@ +'use client'; + +import * as React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { MarkdownPlugin } from '@platejs/markdown'; +import { ArrowUpToLine } from '@/components/ui/icons'; +import { getEditorDOMFromHtmlString } from 'platejs'; +import { useEditorRef } from 'platejs/react'; +import { useFilePicker } from 'use-file-picker'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; + +import { ToolbarButton } from './Toolbar'; + +type ImportType = 'html' | 'markdown'; + +export function ImportToolbarButton(props: DropdownMenuProps) { + const editor = useEditorRef(); + const [open, setOpen] = React.useState(false); + + const getFileNodes = (text: string, type: ImportType) => { + if (type === 'html') { + const editorNode = getEditorDOMFromHtmlString(text); + const nodes = editor.api.html.deserialize({ + element: editorNode + }); + + return nodes; + } + + if (type === 'markdown') { + return editor.getApi(MarkdownPlugin).markdown.deserialize(text); + } + + return []; + }; + + const { openFilePicker: openMdFilePicker } = useFilePicker({ + accept: ['.md', '.mdx'], + multiple: false, + onFilesSelected: async ({ plainFiles }) => { + const text = await plainFiles[0].text(); + + const nodes = getFileNodes(text, 'markdown'); + + editor.tf.insertNodes(nodes); + } + }); + + const { openFilePicker: openHtmlFilePicker } = useFilePicker({ + accept: ['text/html'], + multiple: false, + onFilesSelected: async ({ plainFiles }) => { + const text = await plainFiles[0].text(); + + const nodes = getFileNodes(text, 'html'); + + editor.tf.insertNodes(nodes); + } + }); + + return ( + + + +
+ +
+
+
+ + + + { + openHtmlFilePicker(); + }}> + Import from HTML + + + { + openMdFilePicker(); + }}> + Import from Markdown + + + +
+ ); +} diff --git a/apps/web/src/components/ui/report/elements/IndentToolbarButton.tsx b/apps/web/src/components/ui/report/elements/IndentToolbarButton.tsx new file mode 100644 index 000000000..6fe29d9da --- /dev/null +++ b/apps/web/src/components/ui/report/elements/IndentToolbarButton.tsx @@ -0,0 +1,28 @@ +'use client'; + +import * as React from 'react'; + +import { useIndentButton, useOutdentButton } from '@platejs/indent/react'; +import { IndentIncrease, IndentDecrease } from '@/components/ui/icons'; + +import { ToolbarButton } from './Toolbar'; + +export function IndentToolbarButton(props: React.ComponentProps) { + const { props: buttonProps } = useIndentButton(); + + return ( + + + + ); +} + +export function OutdentToolbarButton(props: React.ComponentProps) { + const { props: buttonProps } = useOutdentButton(); + + return ( + + + + ); +} diff --git a/apps/web/src/components/ui/report/elements/InsertToolbarButton.tsx b/apps/web/src/components/ui/report/elements/InsertToolbarButton.tsx new file mode 100644 index 000000000..b0bafaf1e --- /dev/null +++ b/apps/web/src/components/ui/report/elements/InsertToolbarButton.tsx @@ -0,0 +1,246 @@ +'use client'; + +import * as React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { + Calendar, + ChevronRight, + GridLayoutCols3, + FileCloud, + Film, + Heading1, + Heading2, + Heading3, + Image, + Link2, + UnorderedList, + OrderedList, + Minus, + Pilcrow, + Plus, + Quote, + Equation, + ShapeSquare, + Table, + Book2, + Code2 +} from '@/components/ui/icons'; +import { KEYS } from 'platejs'; +import { type PlateEditor, useEditorRef } from 'platejs/react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { insertBlock, insertInlineElement } from './transforms'; + +import { ToolbarButton, ToolbarMenuGroup } from './Toolbar'; + +type Group = { + group: string; + items: Item[]; +}; + +interface Item { + icon: React.ReactNode; + value: string; + onSelect: (editor: PlateEditor, value: string) => void; + focusEditor?: boolean; + label?: string; +} + +const groups: Group[] = [ + { + group: 'Basic blocks', + items: [ + { + icon: , + label: 'Paragraph', + value: KEYS.p + }, + { + icon: , + label: 'Heading 1', + value: 'h1' + }, + { + icon: , + label: 'Heading 2', + value: 'h2' + }, + { + icon: , + label: 'Heading 3', + value: 'h3' + }, + { + icon: , + label: 'Table', + value: KEYS.table + }, + { + icon: , + label: 'Code', + value: KEYS.codeBlock + }, + { + icon: , + label: 'Quote', + value: KEYS.blockquote + }, + { + icon: , + label: 'Divider', + value: KEYS.hr + } + ].map((item) => ({ + ...item, + onSelect: (editor, value) => { + insertBlock(editor, value); + } + })) + }, + { + group: 'Lists', + items: [ + { + icon: , + label: 'Bulleted list', + value: KEYS.ul + }, + { + icon: , + label: 'Numbered list', + value: KEYS.ol + }, + { + icon: , + label: 'To-do list', + value: KEYS.listTodo + }, + { + icon: , + label: 'Toggle list', + value: KEYS.toggle + } + ].map((item) => ({ + ...item, + onSelect: (editor, value) => { + insertBlock(editor, value); + } + })) + }, + { + group: 'Media', + items: [ + { + icon: , + label: 'Image', + value: KEYS.img + }, + { + icon: , + label: 'Embed', + value: KEYS.mediaEmbed + } + ].map((item) => ({ + ...item, + onSelect: (editor, value) => { + insertBlock(editor, value); + } + })) + }, + { + group: 'Advanced blocks', + items: [ + { + icon: , + label: 'Table of contents', + value: KEYS.toc + }, + { + icon: , + label: '3 columns', + value: 'action_three_columns' + }, + { + focusEditor: false, + icon: , + label: 'Equation', + value: KEYS.equation + } + ].map((item) => ({ + ...item, + onSelect: (editor, value) => { + insertBlock(editor, value); + } + })) + }, + { + group: 'Inline', + items: [ + { + icon: , + label: 'Link', + value: KEYS.link + }, + { + focusEditor: true, + icon: , + label: 'Date', + value: KEYS.date + }, + { + focusEditor: false, + icon: , + label: 'Inline Equation', + value: KEYS.inlineEquation + } + ].map((item) => ({ + ...item, + onSelect: (editor, value) => { + insertInlineElement(editor, value); + } + })) + } +]; + +export function InsertToolbarButton(props: DropdownMenuProps) { + const editor = useEditorRef(); + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + + + {groups.map(({ group, items: nestedItems }) => ( + + {nestedItems.map(({ icon, label, value, onSelect }) => ( + { + onSelect(editor, value); + editor.tf.focus(); + }}> + {icon} + {label} + + ))} + + ))} + + + ); +} diff --git a/apps/web/src/components/ui/report/elements/LineHeightToolbarButton.tsx b/apps/web/src/components/ui/report/elements/LineHeightToolbarButton.tsx new file mode 100644 index 000000000..d48089a8d --- /dev/null +++ b/apps/web/src/components/ui/report/elements/LineHeightToolbarButton.tsx @@ -0,0 +1,68 @@ +'use client'; + +import * as React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { LineHeightPlugin } from '@platejs/basic-styles/react'; +import { DropdownMenuItemIndicator } from '@radix-ui/react-dropdown-menu'; +import { Check, TextTool2 } from '@/components/ui/icons'; +import { useEditorRef, useSelectionFragmentProp } from 'platejs/react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; + +import { ToolbarButton } from './Toolbar'; + +export function LineHeightToolbarButton(props: DropdownMenuProps) { + const editor = useEditorRef(); + const { defaultNodeValue, validNodeValues: values = [] } = + editor.getInjectProps(LineHeightPlugin); + + const value = useSelectionFragmentProp({ + defaultValue: defaultNodeValue, + getProp: (node) => node.lineHeight + }); + + const [open, setOpen] = React.useState(false); + + return ( + + + +
+ +
+
+
+ + + { + editor.getTransforms(LineHeightPlugin).lineHeight.setNodes(Number(newValue)); + editor.tf.focus(); + }}> + {values.map((value) => ( + + + + + + + {value} + + ))} + + +
+ ); +} diff --git a/apps/web/src/components/ui/report/elements/ListToolbarButton.tsx b/apps/web/src/components/ui/report/elements/ListToolbarButton.tsx new file mode 100644 index 000000000..15f80ca07 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/ListToolbarButton.tsx @@ -0,0 +1,192 @@ +'use client'; + +import * as React from 'react'; + +import { ListStyleType, someList, toggleList } from '@platejs/list'; +import { useIndentTodoToolBarButton, useIndentTodoToolBarButtonState } from '@platejs/list/react'; +import { UnorderedList, OrderedList, ListTodo } from '@/components/ui/icons'; +import { useEditorRef, useEditorSelector } from 'platejs/react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; + +import { + ToolbarButton, + ToolbarSplitButton, + ToolbarSplitButtonPrimary, + ToolbarSplitButtonSecondary +} from './Toolbar'; + +export function BulletedListToolbarButton() { + const editor = useEditorRef(); + const [open, setOpen] = React.useState(false); + + const pressed = useEditorSelector( + (editor) => someList(editor, [ListStyleType.Disc, ListStyleType.Circle, ListStyleType.Square]), + [] + ); + + return ( + + { + toggleList(editor, { + listStyleType: ListStyleType.Disc + }); + }} + data-state={pressed ? 'on' : 'off'}> +
+ +
+
+ + + + + + + + + + toggleList(editor, { + listStyleType: ListStyleType.Disc + }) + }> +
+
+ Default +
+ + + toggleList(editor, { + listStyleType: ListStyleType.Circle + }) + }> +
+
+ Circle +
+ + + toggleList(editor, { + listStyleType: ListStyleType.Square + }) + }> +
+
+ Square +
+ + + + + + ); +} + +export function NumberedListToolbarButton() { + const editor = useEditorRef(); + const [open, setOpen] = React.useState(false); + + const pressed = useEditorSelector( + (editor) => + someList(editor, [ + ListStyleType.Decimal, + ListStyleType.LowerAlpha, + ListStyleType.UpperAlpha, + ListStyleType.LowerRoman, + ListStyleType.UpperRoman + ]), + [] + ); + + return ( + + + toggleList(editor, { + listStyleType: ListStyleType.Decimal + }) + } + data-state={pressed ? 'on' : 'off'}> +
+ +
+
+ + + + + + + + + + toggleList(editor, { + listStyleType: ListStyleType.Decimal + }) + }> + Decimal (1, 2, 3) + + + toggleList(editor, { + listStyleType: ListStyleType.LowerAlpha + }) + }> + Lower Alpha (a, b, c) + + + toggleList(editor, { + listStyleType: ListStyleType.UpperAlpha + }) + }> + Upper Alpha (A, B, C) + + + toggleList(editor, { + listStyleType: ListStyleType.LowerRoman + }) + }> + Lower Roman (i, ii, iii) + + + toggleList(editor, { + listStyleType: ListStyleType.UpperRoman + }) + }> + Upper Roman (I, II, III) + + + + +
+ ); +} + +export function TodoListToolbarButton(props: React.ComponentProps) { + const state = useIndentTodoToolBarButtonState({ nodeType: 'todo' }); + const { props: buttonProps } = useIndentTodoToolBarButton(state); + + return ( + +
+ +
+
+ ); +} diff --git a/apps/web/src/components/ui/report/elements/MediaToolbarButton.tsx b/apps/web/src/components/ui/report/elements/MediaToolbarButton.tsx new file mode 100644 index 000000000..3630d9f83 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/MediaToolbarButton.tsx @@ -0,0 +1,224 @@ +'use client'; + +import * as React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { PlaceholderPlugin } from '@platejs/media/react'; +import { VolumeUp, FileCloud, Film, Image, Link } from '@/components/ui/icons'; +import { isUrl, KEYS } from 'platejs'; +import { useEditorRef } from 'platejs/react'; +import { toast } from 'sonner'; +import { useFilePicker } from 'use-file-picker'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/inputs'; + +import { + ToolbarSplitButton, + ToolbarSplitButtonPrimary, + ToolbarSplitButtonSecondary +} from './Toolbar'; + +const MEDIA_CONFIG: Record< + string, + { + accept: string[]; + icon: React.ReactNode; + title: string; + tooltip: string; + } +> = { + [KEYS.audio]: { + accept: ['audio/*'], + icon: ( +
+ +
+ ), + title: 'Insert Audio', + tooltip: 'Audio' + }, + [KEYS.file]: { + accept: ['*'], + icon: ( +
+ +
+ ), + title: 'Insert File', + tooltip: 'File' + }, + [KEYS.img]: { + accept: ['image/*'], + icon: ( +
+ +
+ ), + title: 'Insert Image', + tooltip: 'Image' + }, + [KEYS.video]: { + accept: ['video/*'], + icon: ( +
+ +
+ ), + title: 'Insert Video', + tooltip: 'Video' + } +}; + +export function MediaToolbarButton({ + nodeType, + ...props +}: DropdownMenuProps & { nodeType: string }) { + const currentConfig = MEDIA_CONFIG[nodeType]; + + const editor = useEditorRef(); + const [open, setOpen] = React.useState(false); + const [dialogOpen, setDialogOpen] = React.useState(false); + + const { openFilePicker } = useFilePicker({ + accept: currentConfig.accept, + multiple: true, + onFilesSelected: ({ plainFiles: updatedFiles }) => { + editor.getTransforms(PlaceholderPlugin).insert.media(updatedFiles); + } + }); + + return ( + <> + { + openFilePicker(); + }} + onKeyDown={(e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setOpen(true); + } + }} + pressed={open}> + {currentConfig.icon} + + + + + + + e.stopPropagation()} align="start" alignOffset={-32}> + + openFilePicker()}> + {currentConfig.icon} + Upload from computer + + setDialogOpen(true)}> +
+ +
+ Insert via URL +
+
+
+
+
+ + { + setDialogOpen(value); + }}> + + + + + + ); +} + +function MediaUrlDialogContent({ + currentConfig, + nodeType, + setOpen +}: { + currentConfig: (typeof MEDIA_CONFIG)[string]; + nodeType: string; + setOpen: (value: boolean) => void; +}) { + const editor = useEditorRef(); + const [url, setUrl] = React.useState(''); + + const embedMedia = React.useCallback(() => { + if (!isUrl(url)) return toast.error('Invalid URL'); + + setOpen(false); + editor.tf.insertNodes({ + children: [{ text: '' }], + name: nodeType === KEYS.file ? url.split('/').pop() : undefined, + type: nodeType, + url + }); + }, [url, editor, nodeType, setOpen]); + + return ( + <> + + {currentConfig.title} + + + + + setUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') embedMedia(); + }} + placeholder="" + type="url" + autoFocus + /> + + + + Cancel + { + e.preventDefault(); + embedMedia(); + }}> + Accept + + + + ); +} diff --git a/apps/web/src/components/ui/report/elements/ModeToolbarButton.tsx b/apps/web/src/components/ui/report/elements/ModeToolbarButton.tsx new file mode 100644 index 000000000..9ee072e76 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/ModeToolbarButton.tsx @@ -0,0 +1,120 @@ +'use client'; + +import * as React from 'react'; + +import { SuggestionPlugin } from '@platejs/suggestion/react'; +import { type DropdownMenuProps, DropdownMenuItemIndicator } from '@radix-ui/react-dropdown-menu'; +import { Check, Eye, Pencil2, Pen } from '@/components/ui/icons'; +import { useEditorRef, usePlateState, usePluginOption } from 'platejs/react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; + +import { ToolbarButton } from './Toolbar'; + +export function ModeToolbarButton(props: DropdownMenuProps) { + const editor = useEditorRef(); + const [readOnly, setReadOnly] = usePlateState('readOnly'); + const [open, setOpen] = React.useState(false); + + const isSuggesting = usePluginOption(SuggestionPlugin, 'isSuggesting'); + + let value = 'editing'; + + if (readOnly) value = 'viewing'; + + if (isSuggesting) value = 'suggestion'; + + const item: Record = { + editing: { + icon: , + label: 'Editing' + }, + suggestion: { + icon: , + label: 'Suggestion' + }, + viewing: { + icon: , + label: 'Viewing' + } + }; + + return ( + + + + {item[value].icon} + {item[value].label} + + + + + { + if (newValue === 'viewing') { + setReadOnly(true); + + return; + } else { + setReadOnly(false); + } + + if (newValue === 'suggestion') { + editor.setOption(SuggestionPlugin, 'isSuggesting', true); + + return; + } else { + editor.setOption(SuggestionPlugin, 'isSuggesting', false); + } + + if (newValue === 'editing') { + editor.tf.focus(); + + return; + } + }}> + + + {item.editing.icon} + {item.editing.label} + + + + + {item.viewing.icon} + {item.viewing.label} + + + + + {item.suggestion.icon} + {item.suggestion.label} + + + + + ); +} + +function Indicator() { + return ( + + + + + + ); +} diff --git a/apps/web/src/components/ui/report/elements/TableToolbarButton.tsx b/apps/web/src/components/ui/report/elements/TableToolbarButton.tsx new file mode 100644 index 000000000..c0e5f61d1 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/TableToolbarButton.tsx @@ -0,0 +1,272 @@ +'use client'; + +import * as React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { TablePlugin, useTableMergeState } from '@platejs/table/react'; +// import { +// ArrowDown, +// ArrowLeft, +// ArrowRight, +// ArrowUp, +// Combine, +// Grid3x3Icon, +// Table, +// Trash2Icon, +// Ungroup, +// XIcon +// } from 'lucide-react'; +import { + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + Merge, + Grid3X3, + Table, + Trash2, + Ungroup, + Xmark +} from '@/components/ui/icons'; +import { KEYS } from 'platejs'; +import { useEditorPlugin, useEditorSelector } from 'platejs/react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { cn } from '@/lib/utils'; + +import { ToolbarButton } from './Toolbar'; + +export function TableToolbarButton(props: DropdownMenuProps) { + const tableSelected = useEditorSelector( + (editor) => editor.api.some({ match: { type: KEYS.table } }), + [] + ); + + const { editor, tf } = useEditorPlugin(TablePlugin); + const [open, setOpen] = React.useState(false); + const mergeState = useTableMergeState(); + + return ( + + + +
+ + + + + + + +
+ +
+ Table +
+ + + +
+ + + +
+ Cell + + + { + tf.table.merge(); + editor.tf.focus(); + }}> +
+ +
+ Merge cells +
+ { + tf.table.split(); + editor.tf.focus(); + }}> + + Split cell + +
+ + + + +
+ Row + + + { + tf.insert.tableRow({ before: true }); + editor.tf.focus(); + }}> + + Insert row before + + { + tf.insert.tableRow(); + editor.tf.focus(); + }}> + + Insert row after + + { + tf.remove.tableRow(); + editor.tf.focus(); + }}> +
+ +
+ Delete row +
+
+ + + + +
+ Column + + + { + tf.insert.tableColumn({ before: true }); + editor.tf.focus(); + }}> + + Insert column before + + { + tf.insert.tableColumn(); + editor.tf.focus(); + }}> + + Insert column after + + { + tf.remove.tableColumn(); + editor.tf.focus(); + }}> +
+ +
+ Delete column +
+
+ + + { + tf.remove.table(); + editor.tf.focus(); + }}> +
+ +
+ Delete table +
+ + + + ); +} + +function TablePicker() { + const { editor, tf } = useEditorPlugin(TablePlugin); + + const [tablePicker, setTablePicker] = React.useState({ + grid: Array.from({ length: 8 }, () => Array.from({ length: 8 }).fill(0)), + size: { colCount: 0, rowCount: 0 } + }); + + const onCellMove = (rowIndex: number, colIndex: number) => { + const newGrid = [...tablePicker.grid]; + + for (let i = 0; i < newGrid.length; i++) { + for (let j = 0; j < newGrid[i].length; j++) { + newGrid[i][j] = i >= 0 && i <= rowIndex && j >= 0 && j <= colIndex ? 1 : 0; + } + } + + setTablePicker({ + grid: newGrid, + size: { colCount: colIndex + 1, rowCount: rowIndex + 1 } + }); + }; + + return ( +
{ + tf.insert.table(tablePicker.size, { select: true }); + editor.tf.focus(); + }}> +
+ {tablePicker.grid.map((rows, rowIndex) => + rows.map((value, columIndex) => { + return ( +
{ + onCellMove(rowIndex, columIndex); + }} + /> + ); + }) + )} +
+ +
+ {tablePicker.size.rowCount} x {tablePicker.size.colCount} +
+
+ ); +} diff --git a/apps/web/src/components/ui/report/elements/ToggleToolbarButton.tsx b/apps/web/src/components/ui/report/elements/ToggleToolbarButton.tsx new file mode 100644 index 000000000..82f712e71 --- /dev/null +++ b/apps/web/src/components/ui/report/elements/ToggleToolbarButton.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; + +import { useToggleToolbarButton, useToggleToolbarButtonState } from '@platejs/toggle/react'; +import { DescendingSorting } from '@/components/ui/icons'; + +import { ToolbarButton } from './Toolbar'; + +export function ToggleToolbarButton(props: React.ComponentProps) { + const state = useToggleToolbarButtonState(); + const { props: buttonProps } = useToggleToolbarButton(state); + + return ( + + + + ); +}