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)