From ceae7678258de7604c550e5c54bc2831a3b7f8e8 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 3 Sep 2026 13:28:20 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20adapt=20side=20m?= =?UTF-8?q?enu=20with=20Blocknote=200.54.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocknote changed the way the side menu is positioned. Because we are modifying the height of the heading blocks we need to adjust the offset of the side menu to match the new height of the heading blocks. To do so we have to override the default offset of the side menu with a custom offset that takes into account the new height of the heading blocks. --- src/frontend/apps/impress/package.json | 1 + .../DocsSideMenu/DocsDragHandleMenu.tsx | 66 ++++++++ .../components/DocsSideMenu/DocsSideMenu.tsx | 151 +++++++++++------- .../DocsSideMenu/SideMenuSeparator.tsx | 38 +++++ .../DocsSideMenu/TableHeaderSeparator.tsx | 24 --- .../src/features/docs/doc-editor/styles.tsx | 21 --- 6 files changed, 196 insertions(+), 105 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsDragHandleMenu.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/SideMenuSeparator.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/TableHeaderSeparator.tsx diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index 28dc88295..5ef956bde 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -39,6 +39,7 @@ "@dnd-kit/modifiers": "9.0.0", "@emoji-mart/data": "1.2.1", "@emoji-mart/react": "1.1.1", + "@floating-ui/react": "^0.27.19", "@fontsource-variable/inter": "5.3.0", "@fontsource-variable/material-symbols-outlined": "5.3.3", "@fontsource/material-icons": "5.3.0", diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsDragHandleMenu.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsDragHandleMenu.tsx new file mode 100644 index 000000000..198f246f0 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsDragHandleMenu.tsx @@ -0,0 +1,66 @@ +/** + * Overrides the default DragHandleMenu with a custom implementation for the Docs editor. + * See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/DragHandleMenu/DragHandleMenu.tsx + */ +import { + BlockColorsItem, + DragHandleMenu, + RemoveBlockItem, + TableColumnHeaderItem, + TableRowHeaderItem, + useComponentsContext, + useDictionary, +} from '@blocknote/react'; + +import { Box } from '@/components'; +import { PresentBlockItem } from '@/docs/doc-presenter/components/PresentBlockItem'; +import ColorIcon from '@/icons/color.svg'; +import TableHeaderColumnIcon from '@/icons/table-header-column.svg'; +import TableHeaderRowIcon from '@/icons/table-header-row.svg'; +import TrashIcon from '@/icons/trash.svg'; + +import { LinkToBlockItem } from './LinkToBlockItem'; +import { SideMenuSeparator } from './SideMenuSeparator'; + +export const DocsDragHandleMenu = () => { + const Components = useComponentsContext(); + const dict = useDictionary(); + + if (Components === undefined) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsSideMenu.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsSideMenu.tsx index 529a6ede2..fcacad77b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsSideMenu.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/DocsSideMenu.tsx @@ -1,74 +1,105 @@ /** - * Overrides the default DragHandleMenu with a custom implementation for the Docs editor. - * See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/DragHandleMenu/DragHandleMenu.tsx + * Overrides the default SideMenu with a custom implementation for the Docs editor. + * See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/SideMenu.tsx */ +import type { Block } from '@blocknote/core'; +import { SideMenuExtension } from '@blocknote/core/extensions'; import { - BlockColorsItem, - DragHandleMenu, - RemoveBlockItem, SideMenu, SideMenuController, - TableColumnHeaderItem, - TableRowHeaderItem, - useComponentsContext, - useDictionary, + useBlockNoteEditor, + useExtensionState, } from '@blocknote/react'; +import { offset } from '@floating-ui/react'; +import { useMemo } from 'react'; -import { Box, HorizontalSeparator } from '@/components'; -import { PresentBlockItem } from '@/docs/doc-presenter/components/PresentBlockItem'; -import ColorIcon from '@/icons/color.svg'; -import TableHeaderColumnIcon from '@/icons/table-header-column.svg'; -import TableHeaderRowIcon from '@/icons/table-header-row.svg'; -import TrashIcon from '@/icons/trash.svg'; +import type { + DocsBlockNoteEditor, + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema, +} from '../../types'; -import { LinkToBlockItem } from './LinkToBlockItem'; -import { TableHeaderSeparator } from './TableHeaderSeparator'; +import { DocsDragHandleMenu } from './DocsDragHandleMenu'; -const DocsDragHandleMenu = () => { - const Components = useComponentsContext(); - const dict = useDictionary(); - - if (Components === undefined) { - return null; +/** + * Blocknote v0.54.0 + * See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/SideMenuController.tsx#L21 + * + * Blocknote changed the way the side menu is positioned. + * Because we are modifying the height of the heading blocks we need to adjust the offset of the side menu to match the + * new height of the heading blocks. + * + * @todo Try to see with Blocknote if we can have a better way to handle this, maybe a prop to set the offset of the side menu. + */ +function getBlockOffset( + editor: DocsBlockNoteEditor, + block: Block, +): number { + if (block.type === 'heading') { + switch (block.props.level) { + case 1: + return 13; + case 2: + return 7; + case 3: + return 4; + default: + return 0; + } } + // File blocks without a URL all render the same "Add file" button, + // regardless of their type. + if ( + editor.schema.blockSpecs[block.type]?.implementation.meta + ?.fileBlockAccept && + (!('url' in block.props) || !block.props.url) + ) { + return 12; + } + + if (block.type === 'file') { + return 4; + } + + if (block.type === 'audio' || block.type === 'table') { + return 15; + } + + return 0; +} + +export const DocsSideMenu = () => { + const editor = useBlockNoteEditor< + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema + >(); + const block = useExtensionState(SideMenuExtension, { + selector: (state) => + state?.block as Block< + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema + > | null, + }); + + const floatingUIOptions = useMemo( + () => ({ + useFloatingOptions: { + middleware: [ + offset({ crossAxis: block ? getBlockOffset(editor, block) : 0 }), + ], + }, + }), + [editor, block], + ); + return ( - - - - - - - - - - - - - - - - - - - - - - - + } + /> ); }; - -export const DocsSideMenu = () => ( - } - /> -); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/SideMenuSeparator.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/SideMenuSeparator.tsx new file mode 100644 index 000000000..8909f6922 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/SideMenuSeparator.tsx @@ -0,0 +1,38 @@ +import { SideMenuExtension } from '@blocknote/core/extensions'; +import { useBlockNoteEditor, useExtensionState } from '@blocknote/react'; + +import { type BoxType, HorizontalSeparator } from '@/components'; + +import type { DocsBlockNoteEditor } from '../../types'; + +type SideMenuSeparatorProps = { + typeDisplay?: 'table'; + typeHide?: 'codeBlock'; +} & BoxType; + +export const SideMenuSeparator = ({ + typeDisplay, + typeHide, + ...props +}: SideMenuSeparatorProps) => { + const editor: DocsBlockNoteEditor = useBlockNoteEditor(); + const block = useExtensionState(SideMenuExtension, { + editor, + selector: (state) => state?.block, + }); + + if ( + typeDisplay && + (block === undefined || + block.type !== typeDisplay || + !editor.settings.tables.headers) + ) { + return null; + } + + if (typeHide && (block === undefined || block.type === typeHide)) { + return null; + } + + return ; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/TableHeaderSeparator.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/TableHeaderSeparator.tsx deleted file mode 100644 index bae9030f2..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocsSideMenu/TableHeaderSeparator.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { SideMenuExtension } from '@blocknote/core/extensions'; -import { useBlockNoteEditor, useExtensionState } from '@blocknote/react'; - -import { HorizontalSeparator } from '@/components'; - -import type { DocsBlockNoteEditor } from '../../types'; - -export const TableHeaderSeparator = () => { - const editor: DocsBlockNoteEditor = useBlockNoteEditor(); - const block = useExtensionState(SideMenuExtension, { - editor, - selector: (state) => state?.block, - }); - - if ( - block === undefined || - block.type !== 'table' || - !editor.settings.tables.headers - ) { - return null; - } - - return ; -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx index d5bc96493..f43d5f014 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx @@ -192,9 +192,6 @@ export const DocsEditorStyle = createGlobalStyle` var(--c--contextuals--border--semantic--brand--tertiary); background: var(--c--contextuals--border--semantic--brand--secondary); } - .bn-side-menu[data-block-type='divider'] { - height: 28px; - } /** * Checklist items @@ -251,15 +248,6 @@ export const DocsEditorStyle = createGlobalStyle` --level: 0.875rem; } } - .bn-side-menu[data-block-type='heading'][data-level='1'] { - height: 54px; - } - .bn-side-menu[data-block-type='heading'][data-level='2'] { - height: 43px; - } - .bn-side-menu[data-block-type='heading'][data-level='3'] { - height: 35px; - } & .bn-default-styles h1 { font-size: 1.875rem; } @@ -309,15 +297,6 @@ export const DocsEditorStyle = createGlobalStyle` & .bn-editor { padding-right: 10px; } - .bn-side-menu[data-block-type='heading'][data-level='1'] { - height: 46px; - } - .bn-side-menu[data-block-type='heading'][data-level='2'] { - height: 40px; - } - .bn-side-menu[data-block-type='heading'][data-level='3'] { - height: 40px; - } [data-content-type='heading'] { --level: 1.6rem; &[data-level='2'] {