Skip to content

Commit 661d3e5

Browse files
committed
♻️(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.
1 parent 43c5ab6 commit 661d3e5

8 files changed

Lines changed: 158 additions & 31 deletions

File tree

Lines changed: 26 additions & 0 deletions
Loading

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { useConfig } from '@/core';
2929
import { useCunninghamTheme } from '@/cunningham';
3030
import { Doc } from '@/docs/doc-management';
3131
import { avatarUrlFromName, useAuth } from '@/features/auth';
32+
import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore';
3233
import { useAnalytics } from '@/libs/Analytics';
3334

3435
import { AI_FEATURE_FLAG, DEFAULT_LOCALE } from '../conf';
@@ -134,11 +135,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
134135
user,
135136
);
136137

137-
const {
138-
threadsSidebarTarget,
139-
filter: threadsSidebarFilter,
140-
isSideBarOpen,
141-
} = useCommentSidebarStore();
138+
const { threadsSidebarTarget, filter: threadsSidebarFilter } =
139+
useCommentSidebarStore();
140+
const { activePanel, isPanelOpen } = useRightPanelStore();
141+
const isCommentSideBarOpen = isPanelOpen && activePanel === 'comments';
142142

143143
const currentUserAvatarUrl = useMemo(() => {
144144
if (canSeeComment) {
@@ -300,7 +300,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
300300
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
301301
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
302302
{showComments && <FloatingComposerController />}
303-
{showComments && !isSideBarOpen && (
303+
{showComments && !isCommentSideBarOpen && (
304304
<FloatingThreadController
305305
floatingUIOptions={{
306306
useDismissProps: {

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/CommentSideBar.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,25 +110,28 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
110110

111111
export const CommentSideBarButton = () => {
112112
const { t } = useTranslation();
113-
const { isPanelOpen, togglePanel } = useRightPanelStore();
114-
const { setIsSideBarOpen } = useCommentSidebarStore();
113+
const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } =
114+
useRightPanelStore();
115115

116-
useEffect(() => {
117-
setIsSideBarOpen(isPanelOpen);
118-
}, [isPanelOpen, setIsSideBarOpen]);
119-
120-
const ariaLabel = isPanelOpen
116+
const isActive = isPanelOpen && activePanel === 'comments';
117+
const ariaLabel = isActive
121118
? t('Hide the comments sidebar')
122119
: t('Show the comments sidebar');
123120

124121
return (
125122
<Button
126123
size="small"
127-
onClick={togglePanel}
124+
onClick={() => {
125+
if (isActive) {
126+
setIsPanelOpen(false);
127+
} else {
128+
setActivePanel('comments');
129+
}
130+
}}
128131
aria-label={ariaLabel}
129-
aria-expanded={isPanelOpen}
132+
aria-expanded={isActive}
130133
color="neutral"
131-
variant={isPanelOpen ? 'secondary' : 'tertiary'}
134+
variant={isActive ? 'secondary' : 'tertiary'}
132135
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
133136
></Button>
134137
);

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/useCommentSidebarStore.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,14 @@ import { create } from 'zustand';
22

33
interface CommentSidebarStore {
44
filter: 'open' | 'resolved';
5-
isSideBarOpen: boolean;
6-
setIsSideBarOpen: (isSideBarOpen: boolean) => void;
7-
setThreadsSidebarTarget: (el: HTMLElement | null) => void;
85
setFilter: (filter: 'open' | 'resolved') => void;
6+
setThreadsSidebarTarget: (el: HTMLElement | null) => void;
97
threadsSidebarTarget: HTMLElement | null;
108
}
119

1210
export const useCommentSidebarStore = create<CommentSidebarStore>((set) => ({
1311
filter: 'open',
14-
isSideBarOpen: false,
1512
setFilter: (filter) => set(() => ({ filter })),
16-
setIsSideBarOpen: (isSideBarOpen) => set(() => ({ isSideBarOpen })),
1713
setThreadsSidebarTarget: (threadsSidebarTarget) => {
1814
set(() => ({ threadsSidebarTarget }));
1915
},
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Button } from '@gouvfr-lasuite/cunningham-react';
2+
import { useTranslation } from 'react-i18next';
3+
import { css } from 'styled-components';
4+
5+
import TableContentIcon from '@/assets/icons/ui-kit/bulleted-list.svg';
6+
import { Box, ButtonCloseModal, Text } from '@/components';
7+
import { useRightPanelStore } from '@/features/right-panel/components/useRightPanelStore';
8+
9+
interface TableContentSideBarProps {
10+
onClose: () => void;
11+
}
12+
13+
export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => {
14+
const { t } = useTranslation();
15+
16+
return (
17+
<Box $height="inherit">
18+
<Box
19+
$padding={{ vertical: 'base', horizontal: 'sm' }}
20+
$css={css`
21+
border-bottom: 1px solid
22+
var(--c--contextuals--border--surface--primary);
23+
`}
24+
>
25+
<Box $direction="row" $align="center" $justify="space-between">
26+
<Text $weight="bold">{t('Table of Contents')}</Text>
27+
<ButtonCloseModal
28+
aria-label={t('Close the table of contents sidebar')}
29+
onClick={onClose}
30+
/>
31+
</Box>
32+
</Box>
33+
</Box>
34+
);
35+
};
36+
37+
export const TableContentSideBarButton = () => {
38+
const { t } = useTranslation();
39+
const { isPanelOpen, activePanel, setActivePanel, setIsPanelOpen } =
40+
useRightPanelStore();
41+
42+
const isActive = isPanelOpen && activePanel === 'tableContent';
43+
const ariaLabel = isActive
44+
? t('Hide the table of contents sidebar')
45+
: t('Show the table of contents sidebar');
46+
47+
return (
48+
<Button
49+
size="small"
50+
onClick={() => {
51+
if (isActive) {
52+
setIsPanelOpen(false);
53+
} else {
54+
setActivePanel('tableContent');
55+
}
56+
}}
57+
aria-label={ariaLabel}
58+
aria-expanded={isActive}
59+
color="neutral"
60+
variant={isActive ? 'secondary' : 'tertiary'}
61+
icon={<TableContentIcon width={24} height={24} aria-hidden="true" />}
62+
></Button>
63+
);
64+
};

src/frontend/apps/impress/src/features/right-panel/components/RightPanel.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,56 @@
1+
import { useEffect, useState } from 'react';
12
import { useTranslation } from 'react-i18next';
23
import { css } from 'styled-components';
34

45
import { Box } from '@/components';
56
import { CommentSideBar } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
67
import { useDocStore, useProviderStore } from '@/features/docs/doc-management';
8+
import { TableContentSideBar } from '@/features/docs/doc-table-content/components/TableContentSideBar';
79
import { HEADER_HEIGHT } from '@/features/header';
810

9-
import { useRightPanelStore } from './useRightPanelStore';
11+
import { RightPanelView, useRightPanelStore } from './useRightPanelStore';
1012

1113
export const RightPanel = () => {
1214
const { t } = useTranslation();
1315
const { currentDoc: doc } = useDocStore();
14-
const { setIsPanelOpen, isPanelOpen } = useRightPanelStore();
16+
const { setIsPanelOpen, isPanelOpen, activePanel } = useRightPanelStore();
1517
const { provider, isReady } = useProviderStore();
1618
const isProviderReady =
1719
isReady && provider && provider?.configuration.name === doc?.id;
1820

21+
/**
22+
* Keep rendering the last active panel during the close animation,
23+
* so the content doesn't vanish before the panel finishes sliding out.
24+
* When switching panels, the content swaps instantly.
25+
*/
26+
const [renderedPanel, setRenderedPanel] = useState<RightPanelView | null>(
27+
null,
28+
);
29+
useEffect(() => {
30+
if (activePanel !== null) {
31+
setRenderedPanel(activePanel);
32+
} else {
33+
const timer = setTimeout(() => setRenderedPanel(null), 500);
34+
return () => clearTimeout(timer);
35+
}
36+
}, [activePanel]);
37+
1938
if (!doc || !isProviderReady) {
2039
return null;
2140
}
2241

42+
const ariaLabel = isPanelOpen
43+
? t('Right panel, currently open')
44+
: t('Right panel, currently closed');
45+
46+
const handleClose = () => setIsPanelOpen(false);
47+
2348
return (
2449
<Box
2550
className="--docs--right-panel"
26-
aria-label={t('Right panel')}
51+
aria-label={ariaLabel}
2752
aria-hidden={!isPanelOpen}
53+
aria-expanded={isPanelOpen}
2854
$width="300px"
2955
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
3056
$position="sticky"
@@ -46,7 +72,10 @@ export const RightPanel = () => {
4672
`}
4773
`}
4874
>
49-
<CommentSideBar onClose={() => setIsPanelOpen(false)} />
75+
{renderedPanel === 'tableContent' && (
76+
<TableContentSideBar onClose={handleClose} />
77+
)}
78+
{renderedPanel === 'comments' && <CommentSideBar onClose={handleClose} />}
5079
</Box>
5180
);
5281
};

src/frontend/apps/impress/src/features/right-panel/components/RightPanelCollapseButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { css } from 'styled-components';
22

33
import { Card } from '@/components';
44
import { CommentSideBarButton } from '@/features/docs/doc-editor/components/comments/CommentSideBar';
5+
import { TableContentSideBarButton } from '@/features/docs/doc-table-content/components/TableContentSideBar';
56

67
export const RightPanelCollapseButton = () => {
78
return (
@@ -16,6 +17,7 @@ export const RightPanelCollapseButton = () => {
1617
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
1718
`}
1819
>
20+
<TableContentSideBarButton />
1921
<CommentSideBarButton />
2022
</Card>
2123
);
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { create } from 'zustand';
22

3+
export type RightPanelView = 'tableContent' | 'comments';
4+
35
export interface UseRightPanelStore {
46
isPanelOpen: boolean;
7+
activePanel: RightPanelView | null;
8+
setActivePanel: (panel: RightPanelView | null) => void;
59
setIsPanelOpen: (isOpen: boolean) => void;
610
togglePanel: () => void;
711
}
812

913
export const useRightPanelStore = create<UseRightPanelStore>((set) => ({
1014
isPanelOpen: false,
11-
setIsPanelOpen: (isPanelOpen) => {
12-
set(() => ({ isPanelOpen }));
13-
},
14-
togglePanel: () => {
15-
set((state) => ({ isPanelOpen: !state.isPanelOpen }));
16-
},
15+
activePanel: null,
16+
setActivePanel: (activePanel) =>
17+
set(() => ({ activePanel, isPanelOpen: activePanel !== null })),
18+
setIsPanelOpen: (isPanelOpen) =>
19+
set((state) => ({
20+
isPanelOpen,
21+
activePanel: isPanelOpen ? state.activePanel : null,
22+
})),
23+
togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })),
1724
}));

0 commit comments

Comments
 (0)