diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md
index 3ff3fde7..c71ef586 100644
--- a/docs/ABSTRACTIONS.md
+++ b/docs/ABSTRACTIONS.md
@@ -436,9 +436,10 @@ Defined in `src/components/editorSchema.tsx` and styled in `src/components/Edito
Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tolariaEditorFormattingConfig.ts`:
-- `SingleEditorView` disables BlockNote's default formatting toolbar and `/` menu, then mounts Tolaria-owned controllers so the visible formatting surface matches Tolaria's markdown round-trip guarantees.
+- `SingleEditorView` disables BlockNote's default formatting toolbar, `/` menu, and side menu, then mounts Tolaria-owned controllers so the visible formatting surface matches Tolaria's markdown round-trip guarantees.
- The formatting toolbar only exposes inline controls that persist through `blocksToMarkdownLossy()` in Tolaria's save pipeline: bold, italic, strike, nesting, and link creation. Controls that BlockNote can render temporarily but Tolaria cannot faithfully persist, such as underline, color, alignment, and the block-type dropdown, are hidden instead of appearing to work and later disappearing.
- The `/` slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, and list blocks. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model.
+- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface.
### Markdown-to-BlockNote Pipeline
diff --git a/src/App.test.tsx b/src/App.test.tsx
index f7de5c1d..1768569d 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,4 +1,5 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import type { ReactNode } from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Provide a localStorage mock that supports all methods (jsdom's may be incomplete)
@@ -125,6 +126,12 @@ vi.mock('@blocknote/core/extensions', () => ({
vi.mock('@blocknote/react', () => ({
createReactInlineContentSpec: () => ({ render: () => null }),
+ BlockNoteViewRaw: ({ children }: { children?: ReactNode }) => (
+
{children}
+ ),
+ ComponentsContext: {
+ Provider: ({ children }: { children?: ReactNode }) => <>{children}>,
+ },
useCreateBlockNote: () => ({
tryParseMarkdownToBlocks: async () => [],
replaceBlocks: () => {},
@@ -132,6 +139,7 @@ vi.mock('@blocknote/react', () => ({
insertInlineContent: () => {},
onMount: (cb: () => void) => { cb(); return () => {} },
}),
+ SideMenuController: () => null,
SuggestionMenuController: () => null,
}))
diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx
index f4c53304..cd11d50a 100644
--- a/src/components/Editor.test.tsx
+++ b/src/components/Editor.test.tsx
@@ -70,6 +70,7 @@ vi.mock('@blocknote/react', () => ({
),
FormattingToolbarController: () => null,
+ SideMenuController: () => null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock
SuggestionMenuController: (props: any) => {
capturedGetItemsByTrigger[props.triggerCharacter] = props.getItems
diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx
index 17c15614..59a321ce 100644
--- a/src/components/SingleEditorView.tsx
+++ b/src/components/SingleEditorView.tsx
@@ -5,6 +5,7 @@ import {
SuggestionMenuController,
BlockNoteViewRaw,
ComponentsContext,
+ SideMenuController,
} from '@blocknote/react'
import { components } from '@blocknote/mantine'
import { MantineContext, MantineProvider } from '@mantine/core'
@@ -23,6 +24,7 @@ import {
TolariaFormattingToolbar,
TolariaFormattingToolbarController,
} from './tolariaEditorFormatting'
+import { TolariaSideMenu } from './tolariaBlockNoteSideMenu'
import { useEditorLinkActivation } from './useEditorLinkActivation'
const TEST_TABLE_MARKDOWN = `| Head 1 | Head 2 | Head 3 |
@@ -223,7 +225,9 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange
editable={editable}
formattingToolbar={false}
slashMenu={false}
+ sideMenu={false}
>
+
({
+ DragHandleMenu: ({ children }: PropsWithChildren) => (
+ {children}
+ ),
+ RemoveBlockItem: ({ children }: PropsWithChildren) => {children}
,
+ SideMenu: ({ dragHandleMenu }: { dragHandleMenu?: ComponentType }) => {
+ capturedDragHandleMenu = dragHandleMenu ?? null
+ return
+ },
+ TableColumnHeaderItem: ({ children }: PropsWithChildren) => {children}
,
+ TableRowHeaderItem: ({ children }: PropsWithChildren) => {children}
,
+ useDictionary: () => ({
+ drag_handle: {
+ delete_menuitem: 'Delete',
+ header_row_menuitem: 'Header row',
+ header_column_menuitem: 'Header column',
+ colors_menuitem: 'Colors',
+ },
+ }),
+}))
+
+describe('TolariaSideMenu', () => {
+ it('replaces BlockNote block colors with markdown-safe drag-handle items', () => {
+ render()
+
+ expect(screen.getByTestId('side-menu')).toBeInTheDocument()
+ expect(capturedDragHandleMenu).not.toBeNull()
+
+ const DragHandleMenuComponent = capturedDragHandleMenu!
+ render()
+
+ expect(screen.getByText('Delete')).toBeInTheDocument()
+ expect(screen.getByText('Header row')).toBeInTheDocument()
+ expect(screen.getByText('Header column')).toBeInTheDocument()
+ expect(screen.queryByText('Colors')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/components/tolariaBlockNoteSideMenu.tsx b/src/components/tolariaBlockNoteSideMenu.tsx
new file mode 100644
index 00000000..2c132ecd
--- /dev/null
+++ b/src/components/tolariaBlockNoteSideMenu.tsx
@@ -0,0 +1,25 @@
+import {
+ DragHandleMenu,
+ RemoveBlockItem,
+ SideMenu,
+ TableColumnHeaderItem,
+ TableRowHeaderItem,
+ type SideMenuProps,
+ useDictionary,
+} from '@blocknote/react'
+
+function TolariaDragHandleMenu() {
+ const dict = useDictionary()
+
+ return (
+
+ {dict.drag_handle.delete_menuitem}
+ {dict.drag_handle.header_row_menuitem}
+ {dict.drag_handle.header_column_menuitem}
+
+ )
+}
+
+export function TolariaSideMenu(props: SideMenuProps) {
+ return
+}
diff --git a/tests/smoke/editor-block-handle-zoom.spec.ts b/tests/smoke/editor-block-handle-zoom.spec.ts
index 4fd9435d..b4af31c5 100644
--- a/tests/smoke/editor-block-handle-zoom.spec.ts
+++ b/tests/smoke/editor-block-handle-zoom.spec.ts
@@ -66,6 +66,8 @@ async function expectZoomedHandleClickToWork(
const menu = page.locator('.mantine-Menu-dropdown').filter({ has: page.locator('.mantine-Menu-item') }).first()
await expect(menu).toBeVisible({ timeout: 5_000 })
+ await expect(menu.getByRole('menuitem', { name: 'Delete' })).toBeVisible()
+ await expect(menu.getByRole('menuitem', { name: 'Colors' })).toHaveCount(0)
await page.keyboard.press('Escape')
await expect(menu).toBeHidden({ timeout: 5_000 })
}