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 }) }