mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
♻️(frontend) move table of content to right panel
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
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 }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -8,40 +9,58 @@ test.beforeEach(async ({ page }) => {
|
|||||||
|
|
||||||
test.describe('Doc Table Content', () => {
|
test.describe('Doc Table Content', () => {
|
||||||
test('it checks the doc table content', async ({ page, browserName }) => {
|
test('it checks the doc table content', async ({ page, browserName }) => {
|
||||||
const [randomDoc] = await createDoc(
|
await createDoc(page, 'doc-table-content', browserName, 1);
|
||||||
page,
|
|
||||||
'doc-table-content',
|
|
||||||
browserName,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
|
||||||
|
|
||||||
await page.locator('.ProseMirror').click();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Show the table of contents' }),
|
page.getByRole('button', { name: 'Show the table of contents sidebar' }),
|
||||||
).toBeHidden();
|
).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 page
|
||||||
await summaryContainer.click();
|
.getByRole('button', { name: 'Show the table of contents sidebar' })
|
||||||
|
.click();
|
||||||
|
|
||||||
const level1 = summaryContainer.getByText('Level 1');
|
const elSidePanel = page.getByLabel('Table of contents side panel');
|
||||||
const level2 = summaryContainer.getByText('Level 2');
|
|
||||||
const level3 = summaryContainer.getByText('Level 3');
|
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).toBeVisible();
|
||||||
await expect(level1).toHaveCSS('padding', /4px 0px/);
|
await expect(level1).toHaveCSS('padding', /0px 0px 0px 8px/);
|
||||||
await expect(level1).toHaveAttribute('aria-selected', 'true');
|
await expect(editorLevel1).not.toBeInViewport();
|
||||||
|
await expect(level1).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
await expect(level2).toBeVisible();
|
await expect(level2).toBeVisible();
|
||||||
await expect(level2).toHaveCSS('padding-left', /14.4px/);
|
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).toBeVisible();
|
||||||
await expect(level3).toHaveCSS('padding-left', /24px/);
|
await expect(level3).toHaveCSS('padding-left', /24px/);
|
||||||
await expect(level3).toHaveAttribute('aria-selected', 'false');
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M3.00977 18C3.56205 18 4.00977 18.4477 4.00977 19C4.00977 19.5523 3.56205 20 3.00977 20H3C2.44772 20 2 19.5523 2 19C2 18.4477 2.44772 18 3 18H3.00977Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 18C21.5523 18 22 18.4477 22 19C22 19.5523 21.5523 20 21 20H8C7.44772 20 7 19.5523 7 19C7 18.4477 7.44772 18 8 18H21Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.00977 11C3.56205 11 4.00977 11.4477 4.00977 12C4.00977 12.5523 3.56205 13 3.00977 13H3C2.44772 13 2 12.5523 2 12C2 11.4477 2.44772 11 3 11H3.00977Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 11C21.5523 11 22 11.4477 22 12C22 12.5523 21.5523 13 21 13H8C7.44772 13 7 12.5523 7 12C7 11.4477 7.44772 11 8 11H21Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.00977 4C3.56205 4 4.00977 4.44772 4.00977 5C4.00977 5.55228 3.56205 6 3.00977 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H3.00977Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 4C21.5523 4 22 4.44772 22 5C22 5.55228 21.5523 6 21 6H8C7.44772 6 7 5.55228 7 5C7 4.44772 7.44772 4 8 4H21Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
+6
-6
@@ -29,6 +29,7 @@ import { useConfig } from '@/core';
|
|||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { Doc } from '@/docs/doc-management';
|
import { Doc } from '@/docs/doc-management';
|
||||||
import { avatarUrlFromName, useAuth } from '@/features/auth';
|
import { avatarUrlFromName, useAuth } from '@/features/auth';
|
||||||
|
import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore';
|
||||||
import { useAnalytics } from '@/libs/Analytics';
|
import { useAnalytics } from '@/libs/Analytics';
|
||||||
|
|
||||||
import { AI_FEATURE_FLAG, DEFAULT_LOCALE } from '../conf';
|
import { AI_FEATURE_FLAG, DEFAULT_LOCALE } from '../conf';
|
||||||
@@ -135,11 +136,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Comment sidebar
|
// Comment sidebar
|
||||||
const {
|
const { threadsSidebarTarget, filter: threadsSidebarFilter } =
|
||||||
threadsSidebarTarget,
|
useCommentSidebarStore();
|
||||||
filter: threadsSidebarFilter,
|
const { activePanel, isPanelOpen } = useRightPanelStore();
|
||||||
isSideBarOpen,
|
const isCommentSideBarOpen = isPanelOpen && activePanel === 'comments';
|
||||||
} = useCommentSidebarStore();
|
|
||||||
|
|
||||||
const currentUserAvatarUrl = useMemo(() => {
|
const currentUserAvatarUrl = useMemo(() => {
|
||||||
if (canSeeComment) {
|
if (canSeeComment) {
|
||||||
@@ -303,7 +303,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
|
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
|
||||||
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
|
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
|
||||||
{showComments && <FloatingComposerController />}
|
{showComments && <FloatingComposerController />}
|
||||||
{showComments && !isSideBarOpen && <FloatingThreadController />}
|
{showComments && !isCommentSideBarOpen && <FloatingThreadController />}
|
||||||
{threadsSidebarTarget &&
|
{threadsSidebarTarget &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<ThreadsSidebar
|
<ThreadsSidebar
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
useIsCollaborativeEditable,
|
useIsCollaborativeEditable,
|
||||||
useProviderStore,
|
useProviderStore,
|
||||||
} from '@/docs/doc-management';
|
} from '@/docs/doc-management';
|
||||||
import { TableContent } from '@/docs/doc-table-content/';
|
|
||||||
import { useAuth } from '@/features/auth/';
|
import { useAuth } from '@/features/auth/';
|
||||||
import { SkeletonEditorCore, useSkeletonStore } from '@/features/skeletons';
|
import { SkeletonEditorCore, useSkeletonStore } from '@/features/skeletons';
|
||||||
import { useSkeletonFadeOut } from '@/features/skeletons/hooks/useFadeOut';
|
import { useSkeletonFadeOut } from '@/features/skeletons/hooks/useFadeOut';
|
||||||
@@ -86,7 +85,6 @@ interface DocEditorProps {
|
|||||||
|
|
||||||
export const DocEditor = ({ doc }: DocEditorProps) => {
|
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||||
useCollaboration(doc.id);
|
useCollaboration(doc.id);
|
||||||
const { isDesktop } = useResponsiveStore();
|
|
||||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||||
const isDeletedDoc = !!doc.deleted_at;
|
const isDeletedDoc = !!doc.deleted_at;
|
||||||
const readOnly =
|
const readOnly =
|
||||||
@@ -126,16 +124,13 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
|||||||
}, [authenticated, hasTracked, isPublicDoc, trackEvent]);
|
}, [authenticated, hasTracked, isPublicDoc, trackEvent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DocEditorContainer
|
||||||
{isDesktop && <TableContent selector={`.${DOCS_EDITOR_CLASS}`} />}
|
docHeader={<DocHeader doc={doc} />}
|
||||||
<DocEditorContainer
|
isDeletedDoc={isDeletedDoc}
|
||||||
docHeader={<DocHeader doc={doc} />}
|
readOnly={readOnly}
|
||||||
isDeletedDoc={isDeletedDoc}
|
>
|
||||||
readOnly={readOnly}
|
<DocCoreEditor doc={doc} readOnly={readOnly} />
|
||||||
>
|
</DocEditorContainer>
|
||||||
<DocCoreEditor doc={doc} readOnly={readOnly} />
|
|
||||||
</DocEditorContainer>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+13
-10
@@ -110,25 +110,28 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
|
|||||||
|
|
||||||
export const CommentSideBarButton = () => {
|
export const CommentSideBarButton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isPanelOpen, togglePanel } = useRightPanelStore();
|
const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } =
|
||||||
const { setIsSideBarOpen } = useCommentSidebarStore();
|
useRightPanelStore();
|
||||||
|
|
||||||
useEffect(() => {
|
const isActive = isPanelOpen && activePanel === 'comments';
|
||||||
setIsSideBarOpen(isPanelOpen);
|
const ariaLabel = isActive
|
||||||
}, [isPanelOpen, setIsSideBarOpen]);
|
|
||||||
|
|
||||||
const ariaLabel = isPanelOpen
|
|
||||||
? t('Hide the comments sidebar')
|
? t('Hide the comments sidebar')
|
||||||
: t('Show the comments sidebar');
|
: t('Show the comments sidebar');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={togglePanel}
|
onClick={() => {
|
||||||
|
if (isActive) {
|
||||||
|
setIsPanelOpen(false);
|
||||||
|
} else {
|
||||||
|
setActivePanel('comments');
|
||||||
|
}
|
||||||
|
}}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-expanded={isPanelOpen}
|
aria-expanded={isActive}
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant={isPanelOpen ? 'secondary' : 'tertiary'}
|
variant={isActive ? 'secondary' : 'tertiary'}
|
||||||
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
|
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
|
||||||
></Button>
|
></Button>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-5
@@ -2,18 +2,14 @@ import { create } from 'zustand';
|
|||||||
|
|
||||||
interface CommentSidebarStore {
|
interface CommentSidebarStore {
|
||||||
filter: 'open' | 'resolved';
|
filter: 'open' | 'resolved';
|
||||||
isSideBarOpen: boolean;
|
|
||||||
setIsSideBarOpen: (isSideBarOpen: boolean) => void;
|
|
||||||
setThreadsSidebarTarget: (el: HTMLElement | null) => void;
|
|
||||||
setFilter: (filter: 'open' | 'resolved') => void;
|
setFilter: (filter: 'open' | 'resolved') => void;
|
||||||
|
setThreadsSidebarTarget: (el: HTMLElement | null) => void;
|
||||||
threadsSidebarTarget: HTMLElement | null;
|
threadsSidebarTarget: HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommentSidebarStore = create<CommentSidebarStore>((set) => ({
|
export const useCommentSidebarStore = create<CommentSidebarStore>((set) => ({
|
||||||
filter: 'open',
|
filter: 'open',
|
||||||
isSideBarOpen: false,
|
|
||||||
setFilter: (filter) => set(() => ({ filter })),
|
setFilter: (filter) => set(() => ({ filter })),
|
||||||
setIsSideBarOpen: (isSideBarOpen) => set(() => ({ isSideBarOpen })),
|
|
||||||
setThreadsSidebarTarget: (threadsSidebarTarget) => {
|
setThreadsSidebarTarget: (threadsSidebarTarget) => {
|
||||||
set(() => ({ threadsSidebarTarget }));
|
set(() => ({ threadsSidebarTarget }));
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -5,9 +5,9 @@ import { TFunction } from 'i18next';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { validate as uuidValidate } from 'uuid';
|
import { validate as uuidValidate } from 'uuid';
|
||||||
|
|
||||||
import { DocsBlockNoteEditor } from '@/docs/doc-editor';
|
|
||||||
import LinkPageIcon from '@/docs/doc-editor/assets/doc-link.svg';
|
import LinkPageIcon from '@/docs/doc-editor/assets/doc-link.svg';
|
||||||
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.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 { useCreateChildDocTree, useDocStore } from '@/docs/doc-management';
|
||||||
|
|
||||||
import { LinkSelected } from './LinkSelected';
|
import { LinkSelected } from './LinkSelected';
|
||||||
|
|||||||
@@ -22,15 +22,9 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
|||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
const blocksChanges = context.getChanges();
|
const blocksChanges = context.getChanges();
|
||||||
|
|
||||||
if (!blocksChanges.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockChanges = blocksChanges[0];
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
blockChanges.type !== 'update' ||
|
!blocksChanges.length ||
|
||||||
blockChanges.block.type !== 'heading'
|
!blocksChanges.find((change) => change.block.type === 'heading')
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { css } from 'styled-components';
|
|||||||
|
|
||||||
import { Box, ButtonCloseModal, Text } from '@/components';
|
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||||
import { useMediaUrl } from '@/core';
|
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 { Doc, useTrans } from '@/docs/doc-management';
|
||||||
import { fallbackLng } from '@/i18n/config';
|
import { fallbackLng } from '@/i18n/config';
|
||||||
|
|
||||||
|
|||||||
+9
-7
@@ -3,13 +3,13 @@ import { css } from 'styled-components';
|
|||||||
|
|
||||||
import { BoxButton, Text } from '@/components';
|
import { BoxButton, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { DocsBlockNoteEditor } from '@/docs/doc-editor';
|
import { DocsBlockNoteEditor } from '@/docs/doc-editor/types';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
const leftPaddingMap: { [key: number]: string } = {
|
const leftPaddingMap: { [key: number]: string } = {
|
||||||
3: '1.5rem',
|
3: '1.5rem',
|
||||||
2: '0.9rem',
|
2: '0.9rem',
|
||||||
1: '0.3rem',
|
1: 'xs',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HeadingsHighlight = {
|
export type HeadingsHighlight = {
|
||||||
@@ -40,7 +40,9 @@ export const Heading = ({
|
|||||||
return (
|
return (
|
||||||
<BoxButton
|
<BoxButton
|
||||||
id={`heading-${headingId}`}
|
id={`heading-${headingId}`}
|
||||||
|
className="--docs--table-content-heading"
|
||||||
$width="100%"
|
$width="100%"
|
||||||
|
$height="var(--c--globals--spacings--lg)"
|
||||||
onMouseOver={() => setIsHover(true)}
|
onMouseOver={() => setIsHover(true)}
|
||||||
onMouseLeave={() => setIsHover(false)}
|
onMouseLeave={() => setIsHover(false)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -62,9 +64,10 @@ export const Heading = ({
|
|||||||
$radius="var(--c--globals--spacings--st)"
|
$radius="var(--c--globals--spacings--st)"
|
||||||
$background={
|
$background={
|
||||||
isActive
|
isActive
|
||||||
? 'var(--c--contextuals--background--semantic--neutral--secondary)'
|
? 'var(--c--contextuals--background--semantic--overlay--primary)'
|
||||||
: 'none'
|
: 'none'
|
||||||
}
|
}
|
||||||
|
$justify="center"
|
||||||
$css={css`
|
$css={css`
|
||||||
text-align: left;
|
text-align: left;
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
@@ -74,15 +77,14 @@ export const Heading = ({
|
|||||||
border-radius: var(--c--globals--spacings--st);
|
border-radius: var(--c--globals--spacings--st);
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
className="--docs--table-content-heading"
|
|
||||||
aria-label={text}
|
aria-label={text}
|
||||||
aria-selected={isHighlight}
|
aria-selected={isHighlight}
|
||||||
aria-current={isHighlight ? 'true' : undefined}
|
aria-current={isHighlight ? 'true' : undefined}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
$width="100%"
|
$size="sm"
|
||||||
$padding={{ vertical: 'xtiny', left: leftPaddingMap[level] }}
|
$padding={{ left: leftPaddingMap[level] }}
|
||||||
$weight={isHighlight ? 'bold' : 'normal'}
|
$weight={isHighlight ? '700' : '500'}
|
||||||
$css="overflow-wrap: break-word;"
|
$css="overflow-wrap: break-word;"
|
||||||
$hasTransition
|
$hasTransition
|
||||||
aria-selected={isHighlight}
|
aria-selected={isHighlight}
|
||||||
|
|||||||
-284
@@ -1,284 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
import {
|
|
||||||
DocsBlockNoteEditor,
|
|
||||||
HeadingBlock,
|
|
||||||
useEditorStore,
|
|
||||||
useHeadingStore,
|
|
||||||
} from '@/docs/doc-editor';
|
|
||||||
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
|
||||||
|
|
||||||
import { Heading } from './Heading';
|
|
||||||
|
|
||||||
export const TableContent = ({ selector }: { selector: string }) => {
|
|
||||||
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<HTMLElement>(selector);
|
|
||||||
if (!layout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
|
||||||
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 (
|
|
||||||
<Box
|
|
||||||
$height={containerHeight}
|
|
||||||
$position="absolute"
|
|
||||||
$css={css`
|
|
||||||
top: 72px;
|
|
||||||
right: 20px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
as="nav"
|
|
||||||
id="summaryContainer"
|
|
||||||
$width={!isOpen ? '40px' : '200px'}
|
|
||||||
$height={!isOpen ? '40px' : 'auto'}
|
|
||||||
$maxHeight="calc(50vh - 60px)"
|
|
||||||
$zIndex={1000}
|
|
||||||
$align="center"
|
|
||||||
$padding={isOpen ? 'xs' : '0'}
|
|
||||||
$justify="center"
|
|
||||||
$position="sticky"
|
|
||||||
aria-label={t('Summary')}
|
|
||||||
$css={css`
|
|
||||||
top: var(--c--globals--spacings--0);
|
|
||||||
border: 1px solid ${colorsTokens['brand-100']};
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: ${spacingsTokens['3xs']};
|
|
||||||
background: var(--c--contextuals--background--surface--primary);
|
|
||||||
${isOpen &&
|
|
||||||
css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: ${spacingsTokens['2xs']};
|
|
||||||
`}
|
|
||||||
`}
|
|
||||||
className="--docs--table-content"
|
|
||||||
>
|
|
||||||
{!isOpen && (
|
|
||||||
<BoxButton
|
|
||||||
onClick={onOpen}
|
|
||||||
$width="100%"
|
|
||||||
$height="100%"
|
|
||||||
$justify="center"
|
|
||||||
$align="center"
|
|
||||||
aria-label={t('Show the table of contents')}
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
aria-controls="toc-list"
|
|
||||||
$css={css`
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 4px ${colorsTokens['brand-400']};
|
|
||||||
background: ${colorsTokens['brand-100']};
|
|
||||||
width: 90%;
|
|
||||||
height: 90%;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
$theme="brand"
|
|
||||||
$variation="tertiary"
|
|
||||||
iconName="list"
|
|
||||||
variant="symbols-outlined"
|
|
||||||
/>
|
|
||||||
</BoxButton>
|
|
||||||
)}
|
|
||||||
{isOpen && (
|
|
||||||
<TableContentOpened
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
headings={headings}
|
|
||||||
editor={editor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableContentOpened = ({
|
|
||||||
setIsOpen,
|
|
||||||
headings,
|
|
||||||
editor,
|
|
||||||
}: {
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
headings: HeadingBlock[];
|
|
||||||
editor: DocsBlockNoteEditor;
|
|
||||||
}) => {
|
|
||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
|
||||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
|
||||||
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 (
|
|
||||||
<Box
|
|
||||||
$width="100%"
|
|
||||||
$overflow="hidden"
|
|
||||||
$css={css`
|
|
||||||
user-select: none;
|
|
||||||
padding: ${spacingsTokens['4xs']};
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
$margin={{ bottom: spacingsTokens.xs }}
|
|
||||||
$direction="row"
|
|
||||||
$justify="space-between"
|
|
||||||
$align="center"
|
|
||||||
>
|
|
||||||
<Text $weight="500" $size="sm">
|
|
||||||
{t('Summary')}
|
|
||||||
</Text>
|
|
||||||
<BoxButton
|
|
||||||
onClick={onClose}
|
|
||||||
$justify="center"
|
|
||||||
$align="center"
|
|
||||||
aria-label={t('Hide the table of contents')}
|
|
||||||
aria-expanded={true}
|
|
||||||
aria-controls="toc-list"
|
|
||||||
$css={css`
|
|
||||||
transition: none !important;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
|
|
||||||
border-radius: var(--c--globals--spacings--st);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Icon iconName="menu_open" $theme="brand" $variation="tertiary" />
|
|
||||||
</BoxButton>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
as="ul"
|
|
||||||
id="toc-list"
|
|
||||||
role="list"
|
|
||||||
$gap={spacingsTokens['3xs']}
|
|
||||||
$css={css`
|
|
||||||
overflow-y: auto;
|
|
||||||
list-style: none;
|
|
||||||
padding: ${spacingsTokens['3xs']};
|
|
||||||
margin: 0;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{headings?.map(
|
|
||||||
(heading) =>
|
|
||||||
heading.contentText && (
|
|
||||||
<Box as="li" role="listitem" key={heading.id}>
|
|
||||||
<Heading
|
|
||||||
editor={editor}
|
|
||||||
headingId={heading.id}
|
|
||||||
level={heading.props.level}
|
|
||||||
text={heading.contentText}
|
|
||||||
isHighlight={headingIdHighlight === heading.id}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+166
@@ -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<string>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Box $height="inherit">
|
||||||
|
<Box
|
||||||
|
$padding={{ vertical: 'base', horizontal: 'sm' }}
|
||||||
|
$css={css`
|
||||||
|
border-bottom: 1px solid
|
||||||
|
var(--c--contextuals--border--surface--primary);
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Box $direction="row" $align="center" $justify="space-between">
|
||||||
|
<Text $weight="bold">{t('Table of Contents')}</Text>
|
||||||
|
<ButtonCloseModal
|
||||||
|
aria-label={t('Close the table of contents sidebar')}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{editor && headings && headings.length > 0 && (
|
||||||
|
<Box
|
||||||
|
as="ul"
|
||||||
|
role="list"
|
||||||
|
$gap={spacingsTokens['3xs']}
|
||||||
|
$padding={{
|
||||||
|
vertical: 'base',
|
||||||
|
horizontal: 'sm',
|
||||||
|
}}
|
||||||
|
$css={css`
|
||||||
|
overflow-y: auto;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{headings.map(
|
||||||
|
(heading) =>
|
||||||
|
heading.contentText && (
|
||||||
|
<Box as="li" role="listitem" key={heading.id}>
|
||||||
|
<Heading
|
||||||
|
editor={editor}
|
||||||
|
headingId={heading.id}
|
||||||
|
level={heading.props.level}
|
||||||
|
text={heading.contentText}
|
||||||
|
isHighlight={headingIdHighlight === heading.id}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
if (isActive) {
|
||||||
|
setIsPanelOpen(false);
|
||||||
|
} else {
|
||||||
|
setActivePanel('tableContent');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-expanded={isActive}
|
||||||
|
color="neutral"
|
||||||
|
variant={isActive ? 'secondary' : 'tertiary'}
|
||||||
|
icon={<TableContentIcon width={24} height={24} aria-hidden="true" />}
|
||||||
|
></Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from './TableContent';
|
|
||||||
export * from './Heading';
|
export * from './Heading';
|
||||||
|
|||||||
@@ -1,31 +1,57 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
||||||
import { useDocStore, useProviderStore } from '@/features/docs/doc-management';
|
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 { HEADER_HEIGHT } from '@/features/header';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { useRightPanelStore } from './useRightPanelStore';
|
import { RightPanelView, useRightPanelStore } from './useRightPanelStore';
|
||||||
|
|
||||||
export const RightPanel = () => {
|
export const RightPanel = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentDoc: doc } = useDocStore();
|
const { currentDoc: doc } = useDocStore();
|
||||||
const { setIsPanelOpen, isPanelOpen } = useRightPanelStore();
|
const { setIsPanelOpen, isPanelOpen, activePanel } = useRightPanelStore();
|
||||||
const { isMobile } = useResponsiveStore();
|
const { isMobile } = useResponsiveStore();
|
||||||
const { provider, isReady } = useProviderStore();
|
const { provider, isReady } = useProviderStore();
|
||||||
const isProviderReady =
|
const isProviderReady =
|
||||||
isReady && provider && provider?.configuration.name === doc?.id;
|
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<RightPanelView | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (activePanel !== null) {
|
||||||
|
setRenderedPanel(activePanel);
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => setRenderedPanel(null), 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [activePanel]);
|
||||||
|
|
||||||
if (!doc || !isProviderReady) {
|
if (!doc || !isProviderReady) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ariaLabel = isPanelOpen
|
||||||
|
? t('Right panel, currently open')
|
||||||
|
: t('Right panel, currently closed');
|
||||||
|
|
||||||
|
const handleClose = () => setIsPanelOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="--docs--right-panel"
|
className="--docs--right-panel"
|
||||||
aria-label={t('Right panel')}
|
aria-label={ariaLabel}
|
||||||
|
aria-expanded={isPanelOpen}
|
||||||
$width="300px"
|
$width="300px"
|
||||||
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
|
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
|
||||||
$position={isMobile ? 'absolute' : 'sticky'}
|
$position={isMobile ? 'absolute' : 'sticky'}
|
||||||
@@ -49,7 +75,10 @@ export const RightPanel = () => {
|
|||||||
`}
|
`}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<CommentSideBar onClose={() => setIsPanelOpen(false)} />
|
{renderedPanel === 'tableContent' && (
|
||||||
|
<TableContentSideBar onClose={handleClose} />
|
||||||
|
)}
|
||||||
|
{renderedPanel === 'comments' && <CommentSideBar onClose={handleClose} />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-1
@@ -4,12 +4,16 @@ import { css } from 'styled-components';
|
|||||||
import { Card } from '@/components';
|
import { Card } from '@/components';
|
||||||
import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
|
||||||
import { useEditorStore } from '@/features/docs/doc-editor/stores/useEditorStore';
|
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 = () => {
|
export const RightPanelCollapseButton = () => {
|
||||||
const { threadStore } = useEditorStore();
|
const { threadStore } = useEditorStore();
|
||||||
const [hasThreads, setHasThreads] = useState(
|
const [hasThreads, setHasThreads] = useState(
|
||||||
!!threadStore?.getThreads().size,
|
!!threadStore?.getThreads().size,
|
||||||
);
|
);
|
||||||
|
const { headings } = useHeadingStore();
|
||||||
|
const hasHeadings = headings.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!threadStore) {
|
if (!threadStore) {
|
||||||
@@ -21,7 +25,7 @@ export const RightPanelCollapseButton = () => {
|
|||||||
});
|
});
|
||||||
}, [threadStore]);
|
}, [threadStore]);
|
||||||
|
|
||||||
if (!hasThreads) {
|
if (!hasThreads && !hasHeadings) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +41,7 @@ export const RightPanelCollapseButton = () => {
|
|||||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
{hasHeadings && <TableContentSideBarButton />}
|
||||||
{hasThreads && <CommentSideBarButton />}
|
{hasThreads && <CommentSideBarButton />}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
+13
-6
@@ -1,17 +1,24 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export type RightPanelView = 'tableContent' | 'comments';
|
||||||
|
|
||||||
export interface UseRightPanelStore {
|
export interface UseRightPanelStore {
|
||||||
isPanelOpen: boolean;
|
isPanelOpen: boolean;
|
||||||
|
activePanel: RightPanelView | null;
|
||||||
|
setActivePanel: (panel: RightPanelView | null) => void;
|
||||||
setIsPanelOpen: (isOpen: boolean) => void;
|
setIsPanelOpen: (isOpen: boolean) => void;
|
||||||
togglePanel: () => void;
|
togglePanel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRightPanelStore = create<UseRightPanelStore>((set) => ({
|
export const useRightPanelStore = create<UseRightPanelStore>((set) => ({
|
||||||
isPanelOpen: false,
|
isPanelOpen: false,
|
||||||
setIsPanelOpen: (isPanelOpen) => {
|
activePanel: null,
|
||||||
set(() => ({ isPanelOpen }));
|
setActivePanel: (activePanel) =>
|
||||||
},
|
set(() => ({ activePanel, isPanelOpen: activePanel !== null })),
|
||||||
togglePanel: () => {
|
setIsPanelOpen: (isPanelOpen) =>
|
||||||
set((state) => ({ isPanelOpen: !state.isPanelOpen }));
|
set((state) => ({
|
||||||
},
|
isPanelOpen,
|
||||||
|
activePanel: isPanelOpen ? state.activePanel : null,
|
||||||
|
})),
|
||||||
|
togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
|||||||
import { NextPageWithLayout } from '@/types/next';
|
import { NextPageWithLayout } from '@/types/next';
|
||||||
|
|
||||||
const DocEditor = dynamic(
|
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,
|
ssr: false,
|
||||||
loading: () => <DocEditorSkeleton />,
|
loading: () => <DocEditorSkeleton />,
|
||||||
|
|||||||
Reference in New Issue
Block a user