From 68b9342401d69f617c1a8dab9fa1ffee6e48d13e Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 21 May 2026 11:02:03 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20move=20table=20o?= =?UTF-8?q?f=20content=20to=20right=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We move the floating table of content to the right panel. This allows us to have a more consistent UI and to make room for the right sidebar. --- .../app-impress/doc-table-content.spec.ts | 61 ++-- .../src/assets/icons/ui-kit/bulleted-list.svg | 26 ++ .../doc-editor/components/BlockNoteEditor.tsx | 12 +- .../docs/doc-editor/components/DocEditor.tsx | 19 +- .../components/comments/CommentSideBar.tsx | 23 +- .../comments/useCommentSidebarStore.ts | 6 +- .../InterlinkingLinkInlineContent.tsx | 2 +- .../docs/doc-editor/hook/useHeadings.tsx | 10 +- .../doc-export/components/ModalExport.tsx | 2 +- .../doc-table-content/components/Heading.tsx | 16 +- .../components/TableContent.tsx | 284 ------------------ .../components/TableContentSideBar.tsx | 166 ++++++++++ .../doc-table-content/components/index.ts | 1 - .../right-panel/components/RightPanel.tsx | 37 ++- .../components/RightPanelCollapseButton.tsx | 7 +- .../components/useRightPanelStore.tsx | 19 +- .../impress/src/pages/docs/[id]/index.tsx | 5 +- 17 files changed, 328 insertions(+), 368 deletions(-) create mode 100644 src/frontend/apps/impress/src/assets/icons/ui-kit/bulleted-list.svg delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts index 5ec43b77a..3009837db 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; -import { createDoc, verifyDocName } from './utils-common'; +import { createDoc } from './utils-common'; +import { tryFocusEditorContent } from './utils-editor'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -8,40 +9,58 @@ test.beforeEach(async ({ page }) => { test.describe('Doc Table Content', () => { test('it checks the doc table content', async ({ page, browserName }) => { - const [randomDoc] = await createDoc( - page, - 'doc-table-content', - browserName, - 1, - ); - - await verifyDocName(page, randomDoc); - - await page.locator('.ProseMirror').click(); + await createDoc(page, 'doc-table-content', browserName, 1); await expect( - page.getByRole('button', { name: 'Show the table of contents' }), + page.getByRole('button', { name: 'Show the table of contents sidebar' }), ).toBeHidden(); - await page.keyboard.type('# Level 1\n## Level 2\n### Level 3'); + const editor = await tryFocusEditorContent({ page }); + await page.keyboard.type('# Level 1'); + for (let i = 0; i < 20; i++) { + await page.keyboard.press('Enter'); + } + await page.keyboard.type('## Level 2'); + for (let i = 0; i < 20; i++) { + await page.keyboard.press('Enter'); + } + await page.keyboard.type('### Level 3'); - const summaryContainer = page.locator('#summaryContainer'); - await summaryContainer.click(); + await page + .getByRole('button', { name: 'Show the table of contents sidebar' }) + .click(); - const level1 = summaryContainer.getByText('Level 1'); - const level2 = summaryContainer.getByText('Level 2'); - const level3 = summaryContainer.getByText('Level 3'); + const elSidePanel = page.getByLabel('Table of contents side panel'); + + const level1 = elSidePanel.getByText('Level 1'); + const editorLevel1 = editor.getByText('Level 1'); + const level2 = elSidePanel.getByText('Level 2'); + const editorLevel2 = editor.getByText('Level 2'); + const level3 = elSidePanel.getByText('Level 3'); await expect(level1).toBeVisible(); - await expect(level1).toHaveCSS('padding', /4px 0px/); - await expect(level1).toHaveAttribute('aria-selected', 'true'); + await expect(level1).toHaveCSS('padding', /0px 0px 0px 8px/); + await expect(editorLevel1).not.toBeInViewport(); + await expect(level1).toHaveAttribute('aria-selected', 'false'); await expect(level2).toBeVisible(); await expect(level2).toHaveCSS('padding-left', /14.4px/); - await expect(level2).toHaveAttribute('aria-selected', 'false'); + await expect(editorLevel2).toBeInViewport(); + await expect(level2).toHaveAttribute('aria-selected', 'true'); await expect(level3).toBeVisible(); await expect(level3).toHaveCSS('padding-left', /24px/); await expect(level3).toHaveAttribute('aria-selected', 'false'); + + await level1.click(); + await expect(editorLevel1).toBeInViewport(); + await expect(level1).toHaveAttribute('aria-selected', 'true'); + await expect(level2).toHaveAttribute('aria-selected', 'false'); + + await level2.click(); + await expect(editorLevel1).not.toBeInViewport(); + await expect(editorLevel2).toBeInViewport(); + await expect(level2).toHaveAttribute('aria-selected', 'true'); + await expect(level1).toHaveAttribute('aria-selected', 'false'); }); }); diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/bulleted-list.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/bulleted-list.svg new file mode 100644 index 000000000..7b0450e5b --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/bulleted-list.svg @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 6fb2bd437..3d463d208 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -29,6 +29,7 @@ import { useConfig } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; import { Doc } from '@/docs/doc-management'; import { avatarUrlFromName, useAuth } from '@/features/auth'; +import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore'; import { useAnalytics } from '@/libs/Analytics'; import { AI_FEATURE_FLAG, DEFAULT_LOCALE } from '../conf'; @@ -135,11 +136,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { ); // Comment sidebar - const { - threadsSidebarTarget, - filter: threadsSidebarFilter, - isSideBarOpen, - } = useCommentSidebarStore(); + const { threadsSidebarTarget, filter: threadsSidebarFilter } = + useCommentSidebarStore(); + const { activePanel, isPanelOpen } = useRightPanelStore(); + const isCommentSideBarOpen = isPanelOpen && activePanel === 'comments'; const currentUserAvatarUrl = useMemo(() => { if (canSeeComment) { @@ -303,7 +303,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { {showComments && } - {showComments && !isSideBarOpen && } + {showComments && !isCommentSideBarOpen && } {threadsSidebarTarget && createPortal( { useCollaboration(doc.id); - const { isDesktop } = useResponsiveStore(); const { isEditable, isLoading } = useIsCollaborativeEditable(doc); const isDeletedDoc = !!doc.deleted_at; const readOnly = @@ -126,16 +124,13 @@ export const DocEditor = ({ doc }: DocEditorProps) => { }, [authenticated, hasTracked, isPublicDoc, trackEvent]); return ( - <> - {isDesktop && } - } - isDeletedDoc={isDeletedDoc} - readOnly={readOnly} - > - - - + } + isDeletedDoc={isDeletedDoc} + readOnly={readOnly} + > + + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx index 14a1cc000..5f7024d69 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx @@ -110,25 +110,28 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => { export const CommentSideBarButton = () => { const { t } = useTranslation(); - const { isPanelOpen, togglePanel } = useRightPanelStore(); - const { setIsSideBarOpen } = useCommentSidebarStore(); + const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } = + useRightPanelStore(); - useEffect(() => { - setIsSideBarOpen(isPanelOpen); - }, [isPanelOpen, setIsSideBarOpen]); - - const ariaLabel = isPanelOpen + const isActive = isPanelOpen && activePanel === 'comments'; + const ariaLabel = isActive ? t('Hide the comments sidebar') : t('Show the comments sidebar'); return ( ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts index 3d4cb6e02..f53d786cf 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts @@ -2,18 +2,14 @@ import { create } from 'zustand'; interface CommentSidebarStore { filter: 'open' | 'resolved'; - isSideBarOpen: boolean; - setIsSideBarOpen: (isSideBarOpen: boolean) => void; - setThreadsSidebarTarget: (el: HTMLElement | null) => void; setFilter: (filter: 'open' | 'resolved') => void; + setThreadsSidebarTarget: (el: HTMLElement | null) => void; threadsSidebarTarget: HTMLElement | null; } export const useCommentSidebarStore = create((set) => ({ filter: 'open', - isSideBarOpen: false, setFilter: (filter) => set(() => ({ filter })), - setIsSideBarOpen: (isSideBarOpen) => set(() => ({ isSideBarOpen })), setThreadsSidebarTarget: (threadsSidebarTarget) => { set(() => ({ threadsSidebarTarget })); }, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx index bba78e8cd..317e192b7 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx @@ -5,9 +5,9 @@ import { TFunction } from 'i18next'; import { useEffect } from 'react'; import { validate as uuidValidate } from 'uuid'; -import { DocsBlockNoteEditor } from '@/docs/doc-editor'; import LinkPageIcon from '@/docs/doc-editor/assets/doc-link.svg'; import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg'; +import { DocsBlockNoteEditor } from '@/docs/doc-editor/types'; import { useCreateChildDocTree, useDocStore } from '@/docs/doc-management'; import { LinkSelected } from './LinkSelected'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx index b7f0172b6..10f397003 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx @@ -22,15 +22,9 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => { timeoutId = setTimeout(() => { const blocksChanges = context.getChanges(); - if (!blocksChanges.length) { - return; - } - - const blockChanges = blocksChanges[0]; - if ( - blockChanges.type !== 'update' || - blockChanges.block.type !== 'heading' + !blocksChanges.length || + !blocksChanges.find((change) => change.block.type === 'heading') ) { return; } diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx index 7fe65eeaf..780654ca0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx @@ -20,7 +20,7 @@ import { css } from 'styled-components'; import { Box, ButtonCloseModal, Text } from '@/components'; import { useMediaUrl } from '@/core'; -import { useEditorStore } from '@/docs/doc-editor'; +import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; import { Doc, useTrans } from '@/docs/doc-management'; import { fallbackLng } from '@/i18n/config'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx index 91fc9bca5..2480b4284 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx @@ -3,13 +3,13 @@ import { css } from 'styled-components'; import { BoxButton, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { DocsBlockNoteEditor } from '@/docs/doc-editor'; +import { DocsBlockNoteEditor } from '@/docs/doc-editor/types'; import { useResponsiveStore } from '@/stores'; const leftPaddingMap: { [key: number]: string } = { 3: '1.5rem', 2: '0.9rem', - 1: '0.3rem', + 1: 'xs', }; export type HeadingsHighlight = { @@ -40,7 +40,9 @@ export const Heading = ({ return ( setIsHover(true)} onMouseLeave={() => setIsHover(false)} onClick={() => { @@ -62,9 +64,10 @@ export const Heading = ({ $radius="var(--c--globals--spacings--st)" $background={ isActive - ? 'var(--c--contextuals--background--semantic--neutral--secondary)' + ? 'var(--c--contextuals--background--semantic--overlay--primary)' : 'none' } + $justify="center" $css={css` text-align: left; &:focus-visible { @@ -74,15 +77,14 @@ export const Heading = ({ border-radius: var(--c--globals--spacings--st); } `} - className="--docs--table-content-heading" aria-label={text} aria-selected={isHighlight} aria-current={isHighlight ? 'true' : undefined} > { - const { spacingsTokens, colorsTokens } = useCunninghamTheme(); - const [containerHeight, setContainerHeight] = useState('100vh'); - const { headings } = useHeadingStore(); - const { editor } = useEditorStore(); - - const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); - - /** - * Calculate container height based on the scrollable content - */ - useEffect(() => { - const layout = document.querySelector(selector); - if (!layout) { - return; - } - - let timeout: ReturnType; - const updateHeight = () => { - clearTimeout(timeout); - timeout = setTimeout(() => { - setContainerHeight(`${layout.scrollHeight}px`); - }, 300); - }; - - updateHeight(); - - const observer = new ResizeObserver(updateHeight); - observer.observe(layout); - - return () => { - clearTimeout(timeout); - observer.disconnect(); - }; - }, [selector]); - - const onOpen = () => { - setIsOpen(true); - }; - - if ( - !editor || - !headings || - headings.length === 0 || - (headings.length === 1 && !headings[0].contentText) - ) { - return null; - } - - return ( - - - {!isOpen && ( - - - - )} - {isOpen && ( - - )} - - - ); -}; - -const TableContentOpened = ({ - setIsOpen, - headings, - editor, -}: { - setIsOpen: (isOpen: boolean) => void; - headings: HeadingBlock[]; - editor: DocsBlockNoteEditor; -}) => { - const { spacingsTokens, colorsTokens } = useCunninghamTheme(); - const [headingIdHighlight, setHeadingIdHighlight] = useState(); - const { t } = useTranslation(); - - /** - * Handle scroll to highlight the current heading in the table of content - */ - useEffect(() => { - const handleScroll = () => { - if (!headings) { - return; - } - - for (const heading of headings) { - const elHeading = document.body.querySelector( - `.bn-block-outer[data-id="${heading.id}"] [data-content-type="heading"]:first-child`, - ); - - if (!elHeading) { - return; - } - - const rect = elHeading.getBoundingClientRect(); - const isVisible = - rect.top + rect.height >= 1 && - rect.bottom <= - (window.innerHeight || document.documentElement.clientHeight); - - if (isVisible) { - setHeadingIdHighlight(heading.id); - break; - } - } - }; - - let timeout: NodeJS.Timeout; - const scrollFn = () => { - if (timeout) { - clearTimeout(timeout); - } - - timeout = setTimeout(() => { - handleScroll(); - }, 300); - }; - - document - .getElementById(MAIN_LAYOUT_ID) - ?.addEventListener('scroll', scrollFn); - - handleScroll(); - - return () => { - document - .getElementById(MAIN_LAYOUT_ID) - ?.removeEventListener('scroll', scrollFn); - }; - }, [headings]); - - const onClose = () => { - setIsOpen(false); - }; - - return ( - - - - {t('Summary')} - - - - - - - {headings?.map( - (heading) => - heading.contentText && ( - - - - ), - )} - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx new file mode 100644 index 000000000..fe25b9771 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx @@ -0,0 +1,166 @@ +import { Button } from '@gouvfr-lasuite/cunningham-react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import TableContentIcon from '@/assets/icons/ui-kit/bulleted-list.svg'; +import { Box, ButtonCloseModal, Text } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; +import { useHeadingStore } from '@/docs/doc-editor/stores/useHeadingStore'; +import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore'; +import { MAIN_LAYOUT_ID } from '@/layouts/conf'; + +import { Heading } from './Heading'; + +interface TableContentSideBarProps { + onClose: () => void; +} + +export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => { + const { t } = useTranslation(); + const { spacingsTokens } = useCunninghamTheme(); + const { headings } = useHeadingStore(); + const { editor } = useEditorStore(); + const [headingIdHighlight, setHeadingIdHighlight] = useState(); + + useEffect(() => { + const handleScroll = () => { + if (!headings) { + return; + } + + let activeHeadingId: string | undefined; + + for (const heading of headings) { + const elHeading = document.body.querySelector( + `.bn-block-outer[data-id="${heading.id}"] [data-content-type="heading"]:first-child`, + ); + + if (!elHeading) { + continue; + } + + const rect = elHeading.getBoundingClientRect(); + + if (rect.top > 0) { + activeHeadingId = heading.id; + break; + } + } + + // If no heading has passed the top yet, fall back to the first heading + if (!activeHeadingId && headings.length > 0) { + activeHeadingId = headings[0].id; + } + + setHeadingIdHighlight(activeHeadingId); + }; + + let timeout: NodeJS.Timeout; + const scrollFn = () => { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + handleScroll(); + }, 300); + }; + + document + .getElementById(MAIN_LAYOUT_ID) + ?.addEventListener('scroll', scrollFn); + + handleScroll(); + + return () => { + if (timeout) { + clearTimeout(timeout); + } + document + .getElementById(MAIN_LAYOUT_ID) + ?.removeEventListener('scroll', scrollFn); + }; + }, [headings]); + + return ( + + + + {t('Table of Contents')} + + + + {editor && headings && headings.length > 0 && ( + + {headings.map( + (heading) => + heading.contentText && ( + + + + ), + )} + + )} + + ); +}; + +export const TableContentSideBarButton = () => { + const { t } = useTranslation(); + const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } = + useRightPanelStore(); + + const isActive = isPanelOpen && activePanel === 'tableContent'; + const ariaLabel = isActive + ? t('Hide the table of contents sidebar') + : t('Show the table of contents sidebar'); + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts index d1a13b2a8..6406e7b07 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/index.ts @@ -1,2 +1 @@ -export * from './TableContent'; export * from './Heading'; diff --git a/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx b/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx index 982b3595d..948e1a111 100644 --- a/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx +++ b/src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx @@ -1,31 +1,57 @@ +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box } from '@/components'; import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/CommentSideBar'; import { useDocStore, useProviderStore } from '@/features/docs/doc-management'; +import { TableContentSideBar } from '@/features/docs/doc-table-content/components/TableContentSideBar'; import { HEADER_HEIGHT } from '@/features/header'; import { useResponsiveStore } from '@/stores'; -import { useRightPanelStore } from './useRightPanelStore'; +import { RightPanelView, useRightPanelStore } from './useRightPanelStore'; export const RightPanel = () => { const { t } = useTranslation(); const { currentDoc: doc } = useDocStore(); - const { setIsPanelOpen, isPanelOpen } = useRightPanelStore(); + const { setIsPanelOpen, isPanelOpen, activePanel } = useRightPanelStore(); const { isMobile } = useResponsiveStore(); const { provider, isReady } = useProviderStore(); const isProviderReady = isReady && provider && provider?.configuration.name === doc?.id; + /** + * Keep rendering the last active panel during the close animation, + * so the content doesn't vanish before the panel finishes sliding out. + * When switching panels, the content swaps instantly. + */ + const [renderedPanel, setRenderedPanel] = useState( + null, + ); + useEffect(() => { + if (activePanel !== null) { + setRenderedPanel(activePanel); + } else { + const timer = setTimeout(() => setRenderedPanel(null), 500); + return () => clearTimeout(timer); + } + }, [activePanel]); + if (!doc || !isProviderReady) { return null; } + const ariaLabel = isPanelOpen + ? t('Right panel, currently open') + : t('Right panel, currently closed'); + + const handleClose = () => setIsPanelOpen(false); + return ( { `} `} > - setIsPanelOpen(false)} /> + {renderedPanel === 'tableContent' && ( + + )} + {renderedPanel === 'comments' && } ); }; diff --git a/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx index b6acba189..0dae1c510 100644 --- a/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx +++ b/src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx @@ -4,12 +4,16 @@ import { css } from 'styled-components'; import { Card } from '@/components'; import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar'; import { useEditorStore } from '@/features/docs/doc-editor/stores/useEditorStore'; +import { useHeadingStore } from '@/features/docs/doc-editor/stores/useHeadingStore'; +import { TableContentSideBarButton } from '@/features/docs/doc-table-content/components/TableContentSideBar'; export const RightPanelCollapseButton = () => { const { threadStore } = useEditorStore(); const [hasThreads, setHasThreads] = useState( !!threadStore?.getThreads().size, ); + const { headings } = useHeadingStore(); + const hasHeadings = headings.length > 0; useEffect(() => { if (!threadStore) { @@ -21,7 +25,7 @@ export const RightPanelCollapseButton = () => { }); }, [threadStore]); - if (!hasThreads) { + if (!hasThreads && !hasHeadings) { return null; } @@ -37,6 +41,7 @@ export const RightPanelCollapseButton = () => { box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); `} > + {hasHeadings && } {hasThreads && } ); diff --git a/src/frontend/apps/impress/src/features/right-panel/components/useRightPanelStore.tsx b/src/frontend/apps/impress/src/features/right-panel/components/useRightPanelStore.tsx index 74638b918..6ffbbbbb8 100644 --- a/src/frontend/apps/impress/src/features/right-panel/components/useRightPanelStore.tsx +++ b/src/frontend/apps/impress/src/features/right-panel/components/useRightPanelStore.tsx @@ -1,17 +1,24 @@ import { create } from 'zustand'; +export type RightPanelView = 'tableContent' | 'comments'; + export interface UseRightPanelStore { isPanelOpen: boolean; + activePanel: RightPanelView | null; + setActivePanel: (panel: RightPanelView | null) => void; setIsPanelOpen: (isOpen: boolean) => void; togglePanel: () => void; } export const useRightPanelStore = create((set) => ({ isPanelOpen: false, - setIsPanelOpen: (isPanelOpen) => { - set(() => ({ isPanelOpen })); - }, - togglePanel: () => { - set((state) => ({ isPanelOpen: !state.isPanelOpen })); - }, + activePanel: null, + setActivePanel: (activePanel) => + set(() => ({ activePanel, isPanelOpen: activePanel !== null })), + setIsPanelOpen: (isPanelOpen) => + set((state) => ({ + isPanelOpen, + activePanel: isPanelOpen ? state.activePanel : null, + })), + togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })), })); diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx index 76008b7fd..f074f20d9 100644 --- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx @@ -25,7 +25,10 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf'; import { NextPageWithLayout } from '@/types/next'; const DocEditor = dynamic( - () => import('@/docs/doc-editor').then((mod) => ({ default: mod.DocEditor })), + () => + import('@/docs/doc-editor/components/DocEditor').then((mod) => ({ + default: mod.DocEditor, + })), { ssr: false, loading: () => ,