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: () => ,