✨(frontend) add right panel

Add a right panel, it will in a first time be used
to display the comment side panel, but it will be
used in the future for other features as well.
This commit is contained in:
Anthony LC
2026-05-27 16:49:21 +02:00
parent 980d57fe60
commit 896444aa13
8 changed files with 141 additions and 39 deletions
@@ -1,3 +1,2 @@
export * from './useEditorStore';
export * from './useHeadingStore';
export * from './usePanelEditorStore';
@@ -1,19 +0,0 @@
import { create } from 'zustand';
export interface UsePanelEditorStore {
isPanelOpen: boolean;
setIsPanelOpen: (isOpen: boolean) => void;
isPanelTableContentOpen: boolean;
setIsPanelTableContentOpen: (isOpen: boolean) => void;
}
export const usePanelEditorStore = create<UsePanelEditorStore>((set) => ({
isPanelOpen: false,
isPanelTableContentOpen: true,
setIsPanelTableContentOpen: (isPanelTableContentOpen) => {
set(() => ({ isPanelTableContentOpen }));
},
setIsPanelOpen: (isPanelOpen) => {
set(() => ({ isPanelOpen }));
},
}));
@@ -24,6 +24,7 @@ const PRINT_ONLY_CONTENT_CSS = `
.--docs--table-content,
.--docs--doc-footer,
.--docs--footer,
.--docs--right-panel,
footer,
[role="contentinfo"],
div[data-is-empty-and-focused="true"],
@@ -4,6 +4,7 @@ import { css } from 'styled-components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham/useCunninghamTheme';
import { LeftPanelCollapseButton } from '@/features/left-panel';
import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton';
import { useResponsiveStore } from '@/stores';
/**
@@ -22,19 +23,15 @@ export const FloatingBar = () => {
const sm = spacingsTokens['sm'];
return css`
position: sticky;
top: calc(-${base});
top: 0;
left: 0;
right: 0;
width: calc(100% + ${base} + ${base});
width: 100%;
min-height: 64px;
padding: ${sm};
margin-left: calc(-${base});
margin-right: calc(-${base});
margin-top: calc(-${base});
z-index: 21; // Under editor select box but above other elements (e.g., doc title, suggestion menu)
display: flex;
align-items: flex-start;
justify-content: flex-start;
isolation: isolate;
&::before {
@@ -64,17 +61,16 @@ export const FloatingBar = () => {
`;
}, [spacingsTokens]);
if (!isDesktop) {
return null;
}
return (
<Box
className="--docs--floating-bar"
data-testid="floating-bar"
$css={FLOATING_STYLES}
$direction="row"
$justify="space-between"
>
<LeftPanelCollapseButton />
{isDesktop ? <LeftPanelCollapseButton /> : <Box />}
<RightPanelCollapseButton />
</Box>
);
};
@@ -0,0 +1,52 @@
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box } from '@/components';
import { useDocStore, useProviderStore } from '@/features/docs/doc-management';
import { HEADER_HEIGHT } from '@/features/header';
import { useResponsiveStore } from '@/stores';
import { useRightPanelStore } from './useRightPanelStore';
export const RightPanel = () => {
const { t } = useTranslation();
const { currentDoc: doc } = useDocStore();
const { _, isPanelOpen } = useRightPanelStore();
const { isMobile } = useResponsiveStore();
const { provider, isReady } = useProviderStore();
const isProviderReady =
isReady && provider && provider?.configuration.name === doc?.id;
if (!doc || !isProviderReady) {
return null;
}
return (
<Box
className="--docs--right-panel"
aria-label={t('Right panel')}
$width="300px"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$position={isMobile ? 'absolute' : 'sticky'}
$zIndex={25}
$hasTransition
$background="var(--c--contextuals--background--surface--secondary)"
$css={css`
flex-shrink: 0;
border-left: 1px solid var(--c--contextuals--border--surface--primary);
transform: translateX(0%);
top: 0;
right: 0;
align-self: flex-start;
opacity: 1;
${!isPanelOpen &&
css`
transform: translateX(200%);
opacity: 0;
margin-left: 0rem;
width: 0;
`}
`}
></Box>
);
};
@@ -0,0 +1,42 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import CommentsIcon from '@/assets/icons/ui-kit/bubble-text.svg';
import { Card } from '@/components';
import { useRightPanelStore } from './useRightPanelStore';
export const RightPanelCollapseButton = () => {
const { t } = useTranslation();
const { isPanelOpen, togglePanel } = useRightPanelStore();
const ariaLabel = isPanelOpen
? t('Hide the right side panel')
: t('Show the right side panel');
return (
<Card
className="--docs--right-panel-collapse-button"
$direction="row"
$css={css`
padding: var(--c--globals--spacings--xxxs);
align-items: center;
gap: var(--c--globals--spacings--xxxs);
border-radius: var(--c--globals--spacings--xs);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
`}
>
<Button
size="small"
onClick={togglePanel}
aria-label={ariaLabel}
aria-expanded={isPanelOpen}
color="neutral"
variant={isPanelOpen ? 'secondary' : 'tertiary'}
icon={<CommentsIcon width={24} height={24} aria-hidden="true" />}
data-testid="floating-bar-toggle-right-panel"
></Button>
</Card>
);
};
@@ -0,0 +1,17 @@
import { create } from 'zustand';
export interface UseRightPanelStore {
isPanelOpen: boolean;
setIsPanelOpen: (isOpen: boolean) => void;
togglePanel: () => void;
}
export const useRightPanelStore = create<UseRightPanelStore>((set) => ({
isPanelOpen: false,
setIsPanelOpen: (isPanelOpen) => {
set(() => ({ isPanelOpen }));
},
togglePanel: () => {
set((state) => ({ isPanelOpen: !state.isPanelOpen }));
},
}));
@@ -2,10 +2,11 @@ import { PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box } from '@/components';
import { Box, BoxProps } from '@/components';
import { Header } from '@/features/header';
import { HEADER_HEIGHT } from '@/features/header/conf';
import { LeftPanel, ResizableLeftPanel } from '@/features/left-panel';
import { RightPanel } from '@/features/right-panel/components/RightPanel';
import { DocEditorSkeleton, Skeleton } from '@/features/skeletons';
import { useResponsiveStore } from '@/stores';
@@ -43,20 +44,29 @@ export function MainLayout({
export interface MainLayoutContentProps {
backgroundColor: 'white' | 'grey';
enableResizablePanel?: boolean;
enableResizablePanel: boolean;
}
export function MainLayoutContent({
children,
backgroundColor,
enableResizablePanel = false,
enableResizablePanel,
}: PropsWithChildren<MainLayoutContentProps>) {
const { isDesktop } = useResponsiveStore();
if (enableResizablePanel) {
return (
<ResizableLeftPanel leftPanel={<LeftPanel />}>
<MainContent backgroundColor={backgroundColor}>{children}</MainContent>
<Box $direction="row" $width="100%" $position="relative">
<MainContent
backgroundColor={backgroundColor}
$flex="auto"
$padding="0"
>
{children}
</MainContent>
<RightPanel />
</Box>
</ResizableLeftPanel>
);
}
@@ -86,10 +96,15 @@ export function MainLayoutContent({
);
}
type MainContentProps = BoxProps & {
backgroundColor: 'white' | 'grey';
};
const MainContent = ({
children,
backgroundColor,
}: PropsWithChildren<MainLayoutContentProps>) => {
...props
}: PropsWithChildren<MainContentProps>) => {
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
@@ -106,9 +121,7 @@ const MainContent = ({
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$position="relative"
$padding={{
all: isDesktop ? 'base' : '0',
}}
$padding={isDesktop ? 'base' : '0'}
$background={
currentBackgroundColor === 'white'
? 'var(--c--contextuals--background--surface--primary)'
@@ -118,6 +131,7 @@ const MainContent = ({
overflow-y: auto;
overflow-x: clip;
`}
{...props}
>
<Skeleton>
<DocEditorSkeleton />