diff --git a/surfsense_web/components/editor/plate-editor.tsx b/surfsense_web/components/editor/plate-editor.tsx index 4f47bc590..2cbc04ba9 100644 --- a/surfsense_web/components/editor/plate-editor.tsx +++ b/surfsense_web/components/editor/plate-editor.tsx @@ -10,6 +10,7 @@ import { AutoformatKit } from '@/components/editor/plugins/autoformat-classic-ki import { BasicNodesKit } from '@/components/editor/plugins/basic-nodes-kit'; import { CalloutKit } from '@/components/editor/plugins/callout-kit'; import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit'; +import { FixedToolbarKit } from '@/components/editor/plugins/fixed-toolbar-kit'; import { FloatingToolbarKit } from '@/components/editor/plugins/floating-toolbar-kit'; import { IndentKit } from '@/components/editor/plugins/indent-kit'; import { LinkKit } from '@/components/editor/plugins/link-kit'; @@ -62,6 +63,7 @@ export function PlateEditor({ ...MathKit, ...SelectionKit, ...SlashCommandKit, + ...FixedToolbarKit, ...FloatingToolbarKit, ...AutoformatKit, MarkdownPlugin.configure({ diff --git a/surfsense_web/components/editor/plugins/fixed-toolbar-kit.tsx b/surfsense_web/components/editor/plugins/fixed-toolbar-kit.tsx new file mode 100644 index 000000000..062ee8fef --- /dev/null +++ b/surfsense_web/components/editor/plugins/fixed-toolbar-kit.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { createPlatePlugin } from 'platejs/react'; + +import { FixedToolbar } from '@/components/ui/fixed-toolbar'; +import { FixedToolbarButtons } from '@/components/ui/fixed-toolbar-buttons'; + +export const FixedToolbarKit = [ + createPlatePlugin({ + key: 'fixed-toolbar', + render: { + beforeEditable: () => ( + + + + ), + }, + }), +]; + diff --git a/surfsense_web/components/ui/fixed-toolbar-buttons.tsx b/surfsense_web/components/ui/fixed-toolbar-buttons.tsx new file mode 100644 index 000000000..24ee9a44f --- /dev/null +++ b/surfsense_web/components/ui/fixed-toolbar-buttons.tsx @@ -0,0 +1,105 @@ +'use client'; + +import * as React from 'react'; + +import { + BoldIcon, + Code2Icon, + HighlighterIcon, + ItalicIcon, + RedoIcon, + StrikethroughIcon, + UnderlineIcon, + UndoIcon, +} from 'lucide-react'; +import { KEYS } from 'platejs'; +import { useEditorReadOnly, useEditorRef } from 'platejs/react'; + +import { InsertToolbarButton } from './insert-toolbar-button'; +import { LinkToolbarButton } from './link-toolbar-button'; +import { MarkToolbarButton } from './mark-toolbar-button'; +import { MoreToolbarButton } from './more-toolbar-button'; +import { ToolbarButton, ToolbarGroup } from './toolbar'; +import { TurnIntoToolbarButton } from './turn-into-toolbar-button'; + +export function FixedToolbarButtons() { + const readOnly = useEditorReadOnly(); + const editor = useEditorRef(); + + if (readOnly) return null; + + return ( +
+ + { + editor.undo(); + editor.tf.focus(); + }} + > + + + + { + editor.redo(); + editor.tf.focus(); + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + diff --git a/surfsense_web/components/ui/fixed-toolbar.tsx b/surfsense_web/components/ui/fixed-toolbar.tsx new file mode 100644 index 000000000..40d513587 --- /dev/null +++ b/surfsense_web/components/ui/fixed-toolbar.tsx @@ -0,0 +1,26 @@ +'use client'; + +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +import { Toolbar } from './toolbar'; + +export function FixedToolbar({ + children, + className, + ...props +}: React.ComponentProps) { + return ( + + {children} + + ); +} + diff --git a/surfsense_web/components/ui/insert-toolbar-button.tsx b/surfsense_web/components/ui/insert-toolbar-button.tsx new file mode 100644 index 000000000..420d05b69 --- /dev/null +++ b/surfsense_web/components/ui/insert-toolbar-button.tsx @@ -0,0 +1,205 @@ +'use client'; + +import * as React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { + ChevronRightIcon, + FileCodeIcon, + Heading1Icon, + Heading2Icon, + Heading3Icon, + InfoIcon, + ListIcon, + ListOrderedIcon, + MinusIcon, + PilcrowIcon, + PlusIcon, + QuoteIcon, + RadicalIcon, + SquareIcon, + TableIcon, +} from 'lucide-react'; +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 '@/components/editor/transforms'; + +import { ToolbarButton, ToolbarMenuGroup } from './toolbar'; + +type Group = { + group: string; + items: Item[]; +}; + +type 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 block', + value: KEYS.codeBlock, + }, + { + icon: , + label: 'Quote', + value: KEYS.blockquote, + }, + { + icon: , + label: 'Divider', + value: KEYS.hr, + }, + ].map((item) => ({ + ...item, + onSelect: (editor: PlateEditor, value: string) => { + 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: PlateEditor, value: string) => { + insertBlock(editor, value); + }, + })), + }, + { + group: 'Advanced', + items: [ + { + icon: , + label: 'Callout', + value: KEYS.callout, + }, + { + focusEditor: false, + icon: , + label: 'Equation', + value: KEYS.equation, + }, + ].map((item) => ({ + ...item, + onSelect: (editor: PlateEditor, value: string) => { + if (item.value === KEYS.equation) { + insertInlineElement(editor, value); + } else { + insertBlock(editor, value); + } + }, + })), + }, +]; + +export function InsertToolbarButton(props: DropdownMenuProps) { + const editor = useEditorRef(); + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + + + {groups.map(({ group, items }) => ( + + + {items.map(({ icon, label, value, onSelect, focusEditor }) => ( + { + onSelect(editor, value); + if (focusEditor !== false) { + editor.tf.focus(); + } + setOpen(false); + }} + className="group" + > +
+ {icon} + {label || value} +
+
+ ))} +
+
+ ))} +
+
+ ); +} + diff --git a/surfsense_web/components/ui/table-node.tsx b/surfsense_web/components/ui/table-node.tsx index dd6b4c25e..89707c56d 100644 --- a/surfsense_web/components/ui/table-node.tsx +++ b/surfsense_web/components/ui/table-node.tsx @@ -9,7 +9,6 @@ import { BlockSelectionPlugin, useBlockSelected, } from '@platejs/selection/react'; -import { setCellBackground } from '@platejs/table'; import { TablePlugin, TableProvider, @@ -27,10 +26,8 @@ import { ArrowRight, ArrowUp, CombineIcon, - EraserIcon, Grid2X2Icon, GripVertical, - PaintBucketIcon, SquareSplitHorizontalIcon, Trash2Icon, XIcon, @@ -66,7 +63,6 @@ import { DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, - DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; @@ -74,10 +70,6 @@ import { Popover, PopoverContent } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; import { blockSelectionVariants } from './block-selection'; -import { - ColorDropdownMenuItems, - DEFAULT_COLORS, -} from './font-color-toolbar-button'; import { ResizeHandle } from './resize-handle'; import { BorderAllIcon, @@ -91,8 +83,8 @@ import { Toolbar, ToolbarButton, ToolbarGroup, - ToolbarMenuGroup, } from './toolbar'; + export const TableElement = withHOC( TableProvider, function TableElement({ @@ -181,9 +173,6 @@ function TableFloatingToolbar({ contentEditable={false} > - - - {canMerge && ( tf.table.merge()} @@ -370,59 +359,6 @@ function TableBordersDropdownMenuContent( ); } -function ColorDropdownMenu({ - children, - tooltip, -}: { - children: React.ReactNode; - tooltip: string; -}) { - const [open, setOpen] = React.useState(false); - - const editor = useEditorRef(); - const selectedCells = usePluginOption(TablePlugin, 'selectedCells'); - - const onUpdateColor = React.useCallback( - (color: string) => { - setOpen(false); - setCellBackground(editor, { color, selectedCells: selectedCells ?? [] }); - }, - [selectedCells, editor] - ); - - const onClearColor = React.useCallback(() => { - setOpen(false); - setCellBackground(editor, { - color: null, - selectedCells: selectedCells ?? [], - }); - }, [selectedCells, editor]); - - return ( - - - {children} - - - - - - - - - - Clear - - - - - ); -} - export function TableRowElement({ children, ...props diff --git a/surfsense_web/package.json b/surfsense_web/package.json index fe42c1fbf..764efe1ff 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -42,7 +42,6 @@ "@platejs/dnd": "^52.0.11", "@platejs/floating": "^52.0.11", "@platejs/indent": "^52.0.11", - "@platejs/layout": "^52.0.11", "@platejs/link": "^52.0.11", "@platejs/list-classic": "^52.0.11", "@platejs/markdown": "^52.1.0", @@ -101,7 +100,6 @@ "jotai": "^2.15.1", "jotai-tanstack-query": "^0.11.0", "katex": "^0.16.28", - "lodash": "^4.17.23", "lowlight": "^3.3.0", "lucide-react": "^0.477.0", "motion": "^12.23.22", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index a4f15ad51..c5c69d8e0 100644 --- a/surfsense_web/pnpm-lock.yaml +++ b/surfsense_web/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: '@platejs/indent': specifier: ^52.0.11 version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@platejs/layout': - specifier: ^52.0.11 - version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@platejs/link': specifier: ^52.0.11 version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -248,9 +245,6 @@ importers: katex: specifier: ^0.16.28 version: 0.16.28 - lodash: - specifier: ^4.17.23 - version: 4.17.23 lowlight: specifier: ^3.3.0 version: 3.3.0 @@ -2301,13 +2295,6 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - '@platejs/layout@52.0.11': - resolution: {integrity: sha512-qd/1RLqxFy67kIGc9WpfHA2JMMriW6PaXUibtN9/jmOYi5muSRq314pSvfnB2jq3CB1NYLrseL8qSynEb4liCg==} - peerDependencies: - platejs: '>=52.0.11' - react: '>=18.0.0' - react-dom: '>=18.0.0' - '@platejs/link@52.0.11': resolution: {integrity: sha512-ykfTL1NtvDJYGbp+QZiZU+N1WNo5xLiInuBl/Gb/EVeI/PU8He1PLR1ub/zhIuJiiT487u07QT5b4jltvnd86g==} peerDependencies: @@ -9736,13 +9723,6 @@ snapshots: react-compiler-runtime: 1.0.0(react@19.2.3) react-dom: 19.2.3(react@19.2.3) - '@platejs/layout@52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - platejs: 52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)) - react: 19.2.3 - react-compiler-runtime: 1.0.0(react@19.2.3) - react-dom: 19.2.3(react@19.2.3) - '@platejs/link@52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@platejs/floating': 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)