From 220cf696ac5ef9f5cb6e81d3a7dc5ef553bbbe78 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 25 Jun 2026 12:22:18 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20a=20user=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are now using the UserMenu component from the ui-kit. It is a dropdown displaying the user's name and email, along with a logout button and a language picker. To fit the design of the user menu, we adapted the LanguagePicker adn are now using the ui-kit language picker component. --- CHANGELOG.md | 1 + .../e2e/__tests__/app-impress/auth.setup.ts | 7 +-- .../app-impress/doc-comments.spec.ts | 3 +- .../app-impress/doc-member-create.spec.ts | 8 +-- .../app-impress/doc-visibility.spec.ts | 16 ++---- .../e2e/__tests__/app-impress/header.spec.ts | 27 ---------- .../__tests__/app-impress/language.spec.ts | 49 +++++++++-------- .../__tests__/app-impress/left-panel.spec.ts | 16 ++++++ .../e2e/__tests__/app-impress/utils-common.ts | 8 +-- .../e2e/__tests__/app-impress/utils-signin.ts | 5 ++ .../features/auth/components/ButtonLogin.tsx | 45 +++++----------- .../src/features/auth/hooks/useAuth.tsx | 17 +++--- .../src/features/header/components/Header.tsx | 2 - .../features/home/components/HomeHeader.tsx | 4 +- .../language/components/LanguagePicker.tsx | 52 ++++++++++++++++++- .../left-panel/components/LeftPanelFooter.tsx | 28 +++++++--- 16 files changed, 158 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b148363ac..1d98fa07e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to - ✨(y-provider) preserve callouts, PDFs, page breaks, interlinking links and commented text on HTML/markdown export #2296 +- ✨(frontend) add a user menu #2463 ### Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts b/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts index 5ccee2192e..a28440f446 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/auth.setup.ts @@ -24,10 +24,11 @@ const saveStorageState = async ( await SignIn(page, browserName); + /** + * If the grid is displayed, it means the user is logged in and the storage state can be saved. + */ await expect( - page.locator('header').first().getByRole('button', { - name: 'Logout', - }), + page.getByRole('heading', { name: 'All docs', level: 2 }), ).toBeVisible({ timeout: 10000 }); await page.context().storageState({ diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts index 2b4f746a27..0b7ad3a9da 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts @@ -17,6 +17,7 @@ import { updateRoleUser, updateShareLink, } from './utils-share'; +import { logOut } from './utils-signin'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -312,7 +313,7 @@ test.describe('Doc Comments', () => { await updateShareLink(page, 'Public', 'Editing'); // Anonymous user can see and add comments - await otherPage.getByRole('button', { name: 'Logout' }).click(); + await logOut(otherPage); await expect( otherPage diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index c6c804e8cb..37817bc126 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -9,7 +9,7 @@ import { } from './utils-common'; import { writeInEditor } from './utils-editor'; import { connectOtherUserToDoc, updateRoleUser } from './utils-share'; -import { SignIn } from './utils-signin'; +import { SignIn, logOut } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Document create member', () => { @@ -371,11 +371,7 @@ test.describe('Document create member: Multiple login', () => { const urlDoc = page.url(); - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); + await logOut(page); const otherBrowser = BROWSERS.find((b) => b !== browserName); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index 6dca8b40ba..5488806002 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test'; import { BROWSERS, createDoc, verifyDocName } from './utils-common'; import { getEditor, writeInEditor } from './utils-editor'; import { addNewMember, connectOtherUserToDoc } from './utils-share'; -import { SignIn, expectLoginPage } from './utils-signin'; +import { SignIn, expectLoginPage, logOut } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Doc Visibility', () => { @@ -81,13 +81,7 @@ test.describe('Doc Visibility: Restricted', () => { await verifyDocName(page, docTitle); const urlDoc = page.url(); - - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); - + await logOut(page); await expectLoginPage(page); await page.goto(urlDoc); @@ -112,11 +106,7 @@ test.describe('Doc Visibility: Restricted', () => { const urlDoc = page.url(); - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); + await logOut(page); const otherBrowser = BROWSERS.find((b) => b !== browserName); if (!otherBrowser) { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts index 2036733d8a..cdeacf0dff 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts @@ -1,7 +1,6 @@ import { expect, test } from '@playwright/test'; import { overrideConfig } from './utils-common'; -import { SignIn, expectLoginPage } from './utils-signin'; test.describe('Header', () => { test('checks all the elements are visible', async ({ page }) => { @@ -14,14 +13,6 @@ test.describe('Header', () => { 'font-family', /Roboto/i, ); - - await expect( - header.getByRole('button', { - name: 'Logout', - }), - ).toBeVisible(); - - await expect(header.getByText('English')).toBeVisible(); }); test('checks all the elements are visible with DSFR theme', async ({ @@ -106,21 +97,3 @@ test.describe('Header', () => { await expect(logoImage).toHaveAttribute('alt', ''); }); }); - -test.describe('Header: Log out', () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - // eslint-disable-next-line playwright/expect-expect - test('checks logout button', async ({ page, browserName }) => { - await page.goto('/'); - await SignIn(page, browserName); - - await page - .getByRole('button', { - name: 'Logout', - }) - .click(); - - await expectLoginPage(page); - }); -}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts index 56e20515e2..c7129e5d9b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts @@ -40,9 +40,6 @@ test.describe('Language', () => { }); test('checks language switching', async ({ page }) => { - const header = page.locator('header').first(); - const languagePicker = header.locator('.--docs--language-picker-text'); - await expect(page.locator('html')).toHaveAttribute('lang', 'en-us'); // initial language should be english @@ -58,34 +55,38 @@ test.describe('Language', () => { await expect(page.locator('html')).toHaveAttribute('lang', 'fr'); + await page.getByLabel('Ouvrir le menu utilisateur').click(); await expect( - header.getByRole('button').getByText('Français'), + page.getByRole('button', { name: 'Language: Français' }), ).toBeVisible(); - await expect(page.getByLabel('Se déconnecter')).toBeVisible(); - - // Switch to German using the utility function for consistency - await waitForLanguageSwitch(page, TestLanguage.German); - await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible(); - - await expect(page.getByLabel('Abmelden')).toBeVisible(); - - await expect(page.locator('html')).toHaveAttribute('lang', 'de'); + await expect(page.getByText('Déconnexion')).toBeVisible(); - await languagePicker.click(); + await page.keyboard.press('Escape'); - await expect(page.locator('[role="menu"]')).toBeVisible(); + // Switch to German using the utility function for consistency + await waitForLanguageSwitch( + page, + TestLanguage.German, + 'Ouvrir le menu utilisateur', + ); - const menuItems = page.locator('[role="menuitemradio"]'); - await expect(menuItems.first()).toBeVisible(); + await page.getByLabel('Open user menu').click(); + await expect( + page.getByRole('button', { name: 'Language: Deutsch' }), + ).toBeVisible(); - await menuItems.first().click(); + await expect(page.getByText('Logout', { exact: true })).toBeVisible(); - await expect(page.locator('html')).toHaveAttribute('lang', 'en'); - await expect(languagePicker).toContainText('English'); + await expect(page.locator('html')).toHaveAttribute('lang', 'de'); }); - test('can switch language using only keyboard', async ({ page }) => { + /** + * This test is currently failing due to a known issue with the language picker component. + * A pull request has been created to address this issue: https://github.com/suitenumerique/ui-kit/pull/265 + * TODO: Adapt this test once the PR is merged and the uikit version bumped. + */ + test.skip('can switch language using only keyboard', async ({ page }) => { await page.goto('/'); await waitForLanguageSwitch(page, TestLanguage.English); @@ -171,7 +172,11 @@ test.describe('Language', () => { /** * Swedish is not yet supported in the BlockNote locales, so it should fallback to English */ - await waitForLanguageSwitch(page, TestLanguage.Swedish); + await waitForLanguageSwitch( + page, + TestLanguage.Swedish, + 'Ouvrir le menu utilisateur', + ); await openSuggestionMenu({ page }); await expect( suggestionMenu.getByText('Headings', { exact: true }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts index e3104b4bd5..4ca23ed0c7 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts @@ -7,6 +7,7 @@ import { verifyDocName, } from './utils-common'; import { tryFocusEditorContent } from './utils-editor'; +import { SignIn, expectLoginPage, logOut } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; test.describe('Left panel desktop', () => { @@ -19,6 +20,8 @@ test.describe('Left panel desktop', () => { await expect(page.getByTestId('left-panel-mobile')).toBeHidden(); await expect(page.getByTestId('home-button')).toBeHidden(); await expect(page.getByTestId('new-doc-button')).toBeVisible(); + await expect(page.getByLabel('Open user menu')).toBeVisible(); + await expect(page.getByLabel('Open help menu')).toBeVisible(); await goToGridDoc(page); @@ -309,3 +312,16 @@ test.describe('Left panel responsive', () => { await expect(leftPanel).toBeHidden(); }); }); + +test.describe('Left Panel: Log out', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + // eslint-disable-next-line playwright/expect-expect + test('checks logout button', async ({ page, browserName }) => { + await page.goto('/'); + await SignIn(page, browserName); + await logOut(page); + + await expectLoginPage(page); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 24b2c0b88a..83e85aa4fa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -399,6 +399,7 @@ type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey]; export async function waitForLanguageSwitch( page: Page, lang: TestLanguageValue, + labelUserMenu = 'Open user menu', ) { await page.route(/\**\/api\/v1.0\/users\/\**/, async (route, request) => { if (request.method().includes('PATCH')) { @@ -412,8 +413,8 @@ export async function waitForLanguageSwitch( } }); - const header = page.locator('header').first(); - const languagePicker = header.locator('.--docs--language-picker-text'); + await page.getByLabel(labelUserMenu).click(); + const languagePicker = page.getByRole('button', { name: /Language/ }); const isAlreadyTargetLanguage = await languagePicker .innerText() .then((text) => text.toLowerCase().includes(lang.label.toLowerCase())); @@ -424,7 +425,8 @@ export async function waitForLanguageSwitch( await languagePicker.click(); - await page.getByRole('menuitemradio', { name: lang.label }).click(); + await page.getByRole('menuitem', { name: lang.label }).click(); + await page.keyboard.press('Escape'); } export const clickInEditorShareButton = async (page: Page) => { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts index a3331011f6..2559e05e66 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts @@ -13,6 +13,11 @@ export const SignIn = async ( await keycloakSignIn(page, browserName, fromHome); }; +export const logOut = async (page: Page) => { + await page.getByLabel('Open user menu').click(); + await page.getByText('Logout', { exact: true }).click(); +}; + export const customSignIn = async ( page: Page, browserName: string, diff --git a/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx b/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx index b4666f47a4..2d5e5da18a 100644 --- a/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx +++ b/src/frontend/apps/impress/src/features/auth/components/ButtonLogin.tsx @@ -2,51 +2,30 @@ import { Button } from '@gouvfr-lasuite/cunningham-react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -import { Box, BoxButton } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; +import { BoxButton } from '@/components'; import ProConnectImg from '../assets/button-proconnect.svg'; import { useAuth } from '../hooks'; -import { gotoLogin, gotoLogout } from '../utils'; +import { gotoLogin } from '../utils'; export const ButtonLogin = () => { const { t } = useTranslation(); const { authenticated } = useAuth(); - const { colorsTokens } = useCunninghamTheme(); - if (!authenticated) { - return ( - - ); + if (authenticated) { + return null; } return ( - gotoLogin()} + color="brand" + size="small" + aria-label={t('Sign in')} + className="--docs--button-login" > - - + {t('Sign in')} + ); }; diff --git a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx index 0663a99d9d..a7a6e11d53 100644 --- a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx +++ b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx @@ -8,14 +8,14 @@ import { useAuthQuery } from '../api'; const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g]; export const useAuth = () => { - const { data: user, ...authStates } = useAuthQuery(); + const { data: user, isFetched, isLoading, isSuccess } = useAuthQuery(); const { pathname } = useRouter(); const { trackEvent } = useAnalytics(); - const [hasTracked, setHasTracked] = useState(authStates.isFetched); - const isAuthLoading = - authStates.fetchStatus !== 'idle' || authStates.isLoading; + const [hasTracked, setHasTracked] = useState(isFetched); + const isAuthLoading = isLoading; + const hasInitiallyLoaded = useRef(false); - if (authStates.isFetched) { + if (isFetched) { hasInitiallyLoaded.current = true; } const [pathAllowed, setPathAllowed] = useState( @@ -27,7 +27,7 @@ export const useAuth = () => { }, [pathname]); useEffect(() => { - if (!hasTracked && user && authStates.isSuccess) { + if (!hasTracked && user && isSuccess) { trackEvent({ eventName: 'user', id: user?.id || '', @@ -35,14 +35,13 @@ export const useAuth = () => { }); setHasTracked(true); } - }, [hasTracked, authStates.isSuccess, user, trackEvent]); + }, [hasTracked, isSuccess, user, trackEvent]); return { user, - authenticated: !!user && authStates.isSuccess, + authenticated: !!user && isSuccess, pathAllowed, hasInitiallyLoaded: hasInitiallyLoaded.current, isAuthLoading, - ...authStates, }; }; diff --git a/src/frontend/apps/impress/src/features/header/components/Header.tsx b/src/frontend/apps/impress/src/features/header/components/Header.tsx index 4ce20a0939..ae97f09ccd 100644 --- a/src/frontend/apps/impress/src/features/header/components/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/components/Header.tsx @@ -6,7 +6,6 @@ import { Box, SkipToContent, StyledLink } from '@/components/'; import { useConfig } from '@/core/config'; import { useCunninghamTheme } from '@/cunningham'; import { ButtonLogin } from '@/features/auth'; -import { LanguagePicker } from '@/features/language'; import { LeftPanelToggleMobile } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; @@ -89,7 +88,6 @@ export const Header = () => { $direction="row" > - )} diff --git a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx index 0c854c1bfd..e33746354c 100644 --- a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx +++ b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx @@ -5,7 +5,7 @@ import { Waffle } from '@/components/Waffle'; import { useConfig } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; import { Title } from '@/features/header'; -import { LanguagePicker } from '@/features/language'; +import { LanguagePickerLegacy } from '@/features/language'; import { LeftPanelToggleMobile } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; @@ -80,7 +80,7 @@ export const HomeHeader = () => { {!isSmallMobile && ( - + )} diff --git a/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx b/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx index 59655ca591..75b219fff0 100644 --- a/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx +++ b/src/frontend/apps/impress/src/features/language/components/LanguagePicker.tsx @@ -1,3 +1,7 @@ +import { + LanguagePicker as LanguagePickerUIKit, + LanguagesOption, +} from '@gouvfr-lasuite/ui-kit'; import { announce } from '@react-aria/live-announcer'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,7 +15,13 @@ import { useSynchronizedLanguage, } from '@/features/language'; -export const LanguagePicker = () => { +/** + * LanguagePickerLegacy component for selecting language. + * We still have some legacy code that uses this component, so we keep it for now. + * @deprecated Use LanguagePicker instead. + * @returns JSX.Element + */ +export const LanguagePickerLegacy = () => { const { t, i18n } = useTranslation(); const { data: conf } = useConfig(); const { data: user } = useAuthQuery(); @@ -85,3 +95,43 @@ export const LanguagePicker = () => { ); }; + +export const LanguagePicker = () => { + const { t, i18n } = useTranslation(); + const { data: conf } = useConfig(); + const { data: user } = useAuthQuery(); + const { changeLanguageSynchronized } = useSynchronizedLanguage(); + const language = i18n.language; + + const languages: LanguagesOption[] = useMemo(() => { + const backendOptions = conf?.LANGUAGES ?? [[language, language]]; + return backendOptions.map(([backendLocale, backendLabel]) => ({ + label: backendLabel, + value: backendLocale, + isChecked: getMatchingLocales([backendLocale], [language]).length > 0, + })); + }, [conf?.LANGUAGES, language]); + + const onChange = (value: string) => { + const lang = conf?.LANGUAGES?.find(([code]) => code === value); + const backendLabel = lang?.[1] ?? value; + void changeLanguageSynchronized(value, user).then(() => { + announce( + t('Language changed to {{language}}', { + language: backendLabel, + defaultValue: `Language changed to ${backendLabel}`, + }), + 'polite', + ); + }); + }; + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx index 5c6e762dbd..61525f4384 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx @@ -1,12 +1,24 @@ +import { UserMenu } from '@gouvfr-lasuite/ui-kit'; +import { useTranslation } from 'react-i18next'; + import { Box, SeparatedSection } from '@/components'; import { Waffle } from '@/components/Waffle'; import { ButtonLogin } from '@/features/auth'; +import { useAuth } from '@/features/auth/hooks/useAuth'; +import { gotoLogout } from '@/features/auth/utils'; import { HelpMenu } from '@/features/help'; -import { LanguagePicker } from '@/features/language'; +import { LanguagePicker } from '@/features/language/components/LanguagePicker'; import { useResponsiveStore } from '@/stores'; export const LeftPanelFooter = () => { const { isLargeScreen } = useResponsiveStore(); + const { t } = useTranslation(); + const { user } = useAuth(); + + const userMenu = user || { + full_name: t('Guest'), + email: '', + }; return ( @@ -15,14 +27,14 @@ export const LeftPanelFooter = () => { $justify="space-between" $direction="row" > - + + } + /> - {!isLargeScreen && ( - <> - - - - )} + {!isLargeScreen && } From e223c9bd2eb8a4f164c660b044838db0bc358a95 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 25 Jun 2026 16:22:23 +0200 Subject: [PATCH 2/9] savz --- .../header => }/components/Title.tsx | 0 .../impress/src/core/config/api/useConfig.tsx | 2 +- .../docs/docs-grid/components/DocsGrid.tsx | 10 +- .../docs-grid/components/DocsGridItem.tsx | 7 +- .../docs/docs-grid/components/Draggable.tsx | 10 +- .../docs/docs-grid/components/Droppable.tsx | 13 +- .../impress/src/features/footer/Footer.tsx | 3 +- .../src/features/header/components/Header.tsx | 3 +- .../src/features/header/components/index.ts | 1 - .../apps/impress/src/features/header/index.ts | 1 - .../features/home/components/HomeBottom.tsx | 2 +- .../features/home/components/HomeHeader.tsx | 2 +- .../left-panel/components/LeftPanel.tsx | 3 +- .../components/LeftPanelContent.tsx | 26 +++- .../components/LeftPanelDocContent.tsx | 26 ---- .../components/LeftPanelFavoriteItem.tsx | 65 ---------- .../components/LeftPanelFavorites.tsx | 77 +++++++++++- .../left-panel/components/LeftPanelHeader.tsx | 119 ++++++++++++------ .../features/{header => left-panel}/types.ts | 0 .../apps/impress/src/layouts/MainLayout.tsx | 77 ++---------- 20 files changed, 222 insertions(+), 225 deletions(-) rename src/frontend/apps/impress/src/{features/header => }/components/Title.tsx (100%) delete mode 100644 src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx delete mode 100644 src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx rename src/frontend/apps/impress/src/features/{header => left-panel}/types.ts (100%) diff --git a/src/frontend/apps/impress/src/features/header/components/Title.tsx b/src/frontend/apps/impress/src/components/Title.tsx similarity index 100% rename from src/frontend/apps/impress/src/features/header/components/Title.tsx rename to src/frontend/apps/impress/src/components/Title.tsx diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index a8318aea1c..8f8cd9f6b2 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -7,7 +7,7 @@ import type { LinkHTMLAttributes } from 'react'; import { APIError, errorCauses, fetchAPI } from '@/api'; import type { Theme } from '@/cunningham/'; import type { FooterType } from '@/features/footer'; -import type { HeaderType } from '@/features/header'; +import { HeaderType } from '@/features/left-panel/types'; import type { PostHogConf } from '@/services/PosthogAnalytic'; type Imagetype = React.ComponentProps; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index b9618e9e72..b70f8467e9 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -92,7 +92,7 @@ export const DocsGrid = ({ $height="100%" $width="100%" $css={css` - ${!isDesktop ? 'border: none;' : ''} + border: none; ${isDragOver ? ` border: 2px dashed var(--c--contextuals--border--semantic--brand--primary); @@ -124,7 +124,6 @@ export const DocsGrid = ({ )} @@ -193,12 +192,9 @@ const DocGridTitleBar = ({ target }: { target: DocDefaultFilter }) => { diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx index 8c1ab9ba8e..4677168120 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx @@ -49,11 +49,14 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { $align="center" role="listitem" $gap="20px" - $padding={{ vertical: '4xs', horizontal: isDesktop ? 'base' : 'xs' }} + $padding={{ vertical: '4xs' }} $css={css` cursor: pointer; - border-radius: 4px; + border-block: 1px solid + var(--c--contextuals--border--surface--primary); + &:hover { + border-radius: 4px; background-color: ${dragMode ? 'none' : 'var(--c--contextuals--background--semantic--contextual--primary)'}; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx index bd72a1248c..3f8f1c25fd 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx @@ -1,5 +1,8 @@ import { Data, useDraggable } from '@dnd-kit/core'; import { PropsWithChildren } from 'react'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; type DraggableProps = { id: string; @@ -15,15 +18,18 @@ export const Draggable = (props: PropsWithChildren>) => { }); return ( -
{props.children} -
+
); }; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx index 6678c10aa3..0c373a2036 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx @@ -38,13 +38,12 @@ export const Droppable = ({ role="none" $css={css` border-radius: var(--c--globals--spacings--st); - background-color: ${enableHover - ? 'var(--c--globals--colors--brand-100)' - : 'transparent'}; - border: 1.5px solid - ${enableHover - ? 'var(--c--globals--colors--brand-500)' - : 'transparent'}; + ${enableHover + ? css` + background-color: var(--c--globals--colors--brand-100); + border: 1.5px solid var(--c--globals--colors--brand-500); + ` + : ''} `} className="--docs--grid-droppable" > diff --git a/src/frontend/apps/impress/src/features/footer/Footer.tsx b/src/frontend/apps/impress/src/features/footer/Footer.tsx index 7c1080212b..db3f4ea173 100644 --- a/src/frontend/apps/impress/src/features/footer/Footer.tsx +++ b/src/frontend/apps/impress/src/features/footer/Footer.tsx @@ -4,10 +4,9 @@ import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; import { Box, StyledLink, Text } from '@/components/'; +import { Title } from '@/components/Title'; import { useConfig } from '@/core/config'; -import { Title } from '../header'; - import IconLink from './assets/external-link.svg'; import { ContentType } from './types'; diff --git a/src/frontend/apps/impress/src/features/header/components/Header.tsx b/src/frontend/apps/impress/src/features/header/components/Header.tsx index ae97f09ccd..b00cded00d 100644 --- a/src/frontend/apps/impress/src/features/header/components/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/components/Header.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box, SkipToContent, StyledLink } from '@/components/'; +import { Title } from '@/components/Title'; import { useConfig } from '@/core/config'; import { useCunninghamTheme } from '@/cunningham'; import { ButtonLogin } from '@/features/auth'; @@ -11,8 +12,6 @@ import { useResponsiveStore } from '@/stores'; import { HEADER_HEIGHT } from '../conf'; -import { Title } from './Title'; - export const Header = () => { const { t } = useTranslation(); const { data: config } = useConfig(); diff --git a/src/frontend/apps/impress/src/features/header/components/index.ts b/src/frontend/apps/impress/src/features/header/components/index.ts index 127632913d..266dec8a1b 100644 --- a/src/frontend/apps/impress/src/features/header/components/index.ts +++ b/src/frontend/apps/impress/src/features/header/components/index.ts @@ -1,2 +1 @@ export * from './Header'; -export * from './Title'; diff --git a/src/frontend/apps/impress/src/features/header/index.ts b/src/frontend/apps/impress/src/features/header/index.ts index e32f9ebdb8..ccc053ab45 100644 --- a/src/frontend/apps/impress/src/features/header/index.ts +++ b/src/frontend/apps/impress/src/features/header/index.ts @@ -1,3 +1,2 @@ export * from './components/'; export * from './conf'; -export * from './types'; diff --git a/src/frontend/apps/impress/src/features/home/components/HomeBottom.tsx b/src/frontend/apps/impress/src/features/home/components/HomeBottom.tsx index 38cfafd8b4..5374e63303 100644 --- a/src/frontend/apps/impress/src/features/home/components/HomeBottom.tsx +++ b/src/frontend/apps/impress/src/features/home/components/HomeBottom.tsx @@ -2,10 +2,10 @@ import { useTranslation } from 'react-i18next'; import IconDocs from '@/assets/icons/icon-docs.svg'; import { Box, Text } from '@/components'; +import { Title } from '@/components/Title'; import { useConfig } from '@/core/config'; import { useCunninghamTheme } from '@/cunningham'; import { ProConnectButton } from '@/features/auth'; -import { Title } from '@/features/header'; import { useResponsiveStore } from '@/stores'; export function HomeBottom() { diff --git a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx index e33746354c..7d372e22fb 100644 --- a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx +++ b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx @@ -1,10 +1,10 @@ import Image from 'next/image'; import { Box } from '@/components'; +import { Title } from '@/components/Title'; import { Waffle } from '@/components/Waffle'; import { useConfig } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; -import { Title } from '@/features/header'; import { LanguagePickerLegacy } from '@/features/language'; import { LeftPanelToggleMobile } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index 8770e4f5b7..ce7ffaadd8 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -5,7 +5,6 @@ import { createGlobalStyle, css } from 'styled-components'; import { Box } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { HEADER_HEIGHT } from '@/features/header/conf'; import { useResponsiveStore } from '@/stores'; import { useLeftPanelStore } from '../stores'; @@ -36,7 +35,7 @@ export const LeftPanelDesktop = () => { { @@ -43,3 +47,23 @@ export const LeftPanelContent = () => { return null; }; + +const LeftPanelDocContent = () => { + const tree = useTreeContext(); + const { currentDoc } = useDocStore(); + + return ( + + {tree && currentDoc ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx deleted file mode 100644 index 2ee57e2992..0000000000 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; - -import { Box } from '@/components'; -import { Doc, useDocStore } from '@/docs/doc-management'; -import { DocTree } from '@/docs/doc-tree/'; -import { TreeSkeleton } from '@/features/skeletons/components/TreeSkeleton'; - -export const LeftPanelDocContent = () => { - const tree = useTreeContext(); - const { currentDoc } = useDocStore(); - - return ( - - {tree && currentDoc ? ( - - ) : ( - - )} - - ); -}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx deleted file mode 100644 index 254b23da39..0000000000 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavoriteItem.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { t } from 'i18next'; -import { DateTime } from 'luxon'; -import { css } from 'styled-components'; - -import { Box, StyledLink } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { Doc, SimpleDocItem } from '@/docs/doc-management'; -import { DocsGridActions } from '@/docs/docs-grid'; -import { useResponsiveStore } from '@/stores'; - -type LeftPanelFavoriteItemProps = { - doc: Doc; -}; - -export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => { - const { colorsTokens, spacingsTokens } = useCunninghamTheme(); - const { isLargeScreen } = useResponsiveStore(); - - return ( - - - - - - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx index ac5e1b3b95..3171bccffa 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx @@ -1,10 +1,22 @@ +import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; -import { Box, HorizontalSeparator, InfiniteScroll, Text } from '@/components'; +import { + Box, + HorizontalSeparator, + InfiniteScroll, + StyledLink, + Text, +} from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { useInfiniteDocsFavorite } from '@/docs/doc-management'; - -import { LeftPanelFavoriteItem } from './LeftPanelFavoriteItem'; +import { + Doc, + SimpleDocItem, + useInfiniteDocsFavorite, +} from '@/docs/doc-management'; +import { DocsGridActions } from '@/features/docs/docs-grid'; +import { useResponsiveStore } from '@/stores/useResponsiveStore'; export const LeftPanelFavorites = () => { const { t } = useTranslation(); @@ -59,3 +71,60 @@ export const LeftPanelFavorites = () => { ); }; + +type LeftPanelFavoriteItemProps = { + doc: Doc; +}; + +export const LeftPanelFavoriteItem = ({ doc }: LeftPanelFavoriteItemProps) => { + const { colorsTokens, spacingsTokens } = useCunninghamTheme(); + const { isLargeScreen } = useResponsiveStore(); + const { t } = useTranslation(); + + return ( + + + + + + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx index 72f6c9f23b..c2b51e7bbf 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx @@ -1,21 +1,71 @@ import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { t } from 'i18next'; +import Image from 'next/image'; import { useRouter } from 'next/router'; -import { PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; -import HomeSVG from '@/assets/icons/ui-kit/house-rounded.svg'; -import { Box, SeparatedSection } from '@/components'; +import { Box, SeparatedSection, StyledLink } from '@/components'; +import { Title } from '@/components/Title'; +import { useConfig } from '@/core'; +import { NewDocButton } from '@/docs/doc-management/components/NewDocButton'; +import { DocSearchButtonModal } from '@/docs/doc-search/components/DocSearchButtonModal'; import { useAuth } from '@/features/auth'; -import { NewDocButton } from '@/features/docs/doc-management/components/NewDocButton'; -import { DocSearchButtonModal } from '@/features/docs/doc-search/components/DocSearchButtonModal'; +import HomeSVG from '@/icons/house-rounded.svg'; import { useLeftPanelStore } from '../stores'; -export const LeftPanelHeader = ({ children }: PropsWithChildren) => { +export const LeftPanelHeader = () => { + const { data: config } = useConfig(); + + const icon = config?.theme_customization?.header?.icon; + + return ( + + + + + {icon && ( + rest)(icon)} + /> + )} + + </Box> + </StyledLink> + </Box> + <LeftPanelHeaderActions /> + </Box> + ); +}; +export const LeftPanelHeaderActions = () => { const router = useRouter(); const { authenticated } = useAuth(); - const { togglePanel, closePanel } = useLeftPanelStore(); + const { t } = useTranslation(); const goToHome = () => { void router.push('/'); @@ -23,35 +73,32 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { }; return ( - <Box $width="100%" className="--docs--left-panel-header"> - <SeparatedSection> - <Box - $padding={{ horizontal: 'sm' }} - $width="100%" - $direction="row" - $justify="space-between" - $align="center" - > - {authenticated && ( - <NewDocButton onClose={() => closePanel({ type: 'mobile' })} /> + <SeparatedSection> + <Box + $padding={{ horizontal: 'sm' }} + $width="100%" + $direction="row" + $justify="space-between" + $align="center" + > + {authenticated && ( + <NewDocButton onClose={() => closePanel({ type: 'mobile' })} /> + )} + <Box $direction="row" $gap="2px" $margin={{ left: 'auto' }}> + {router.pathname !== '/' && ( + <Button + data-testid="home-button" + onClick={goToHome} + aria-label={t('Back to homepage')} + size="medium" + color="brand" + variant="tertiary" + icon={<HomeSVG aria-hidden="true" width={24} height={24} />} + /> )} - <Box $direction="row" $gap="2px" $margin={{ left: 'auto' }}> - {router.pathname !== '/' && ( - <Button - data-testid="home-button" - onClick={goToHome} - aria-label={t('Back to homepage')} - size="medium" - color="brand" - variant="tertiary" - icon={<HomeSVG aria-hidden="true" width={24} height={24} />} - /> - )} - <DocSearchButtonModal /> - </Box> + <DocSearchButtonModal /> </Box> - </SeparatedSection> - {children} - </Box> + </Box> + </SeparatedSection> ); }; diff --git a/src/frontend/apps/impress/src/features/header/types.ts b/src/frontend/apps/impress/src/features/left-panel/types.ts similarity index 100% rename from src/frontend/apps/impress/src/features/header/types.ts rename to src/frontend/apps/impress/src/features/left-panel/types.ts diff --git a/src/frontend/apps/impress/src/layouts/MainLayout.tsx b/src/frontend/apps/impress/src/layouts/MainLayout.tsx index 0ae721d2f7..30b91bfe38 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-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'; @@ -14,28 +13,17 @@ import { MAIN_LAYOUT_ID } from './conf'; import { usePanelCoordination } from './usePanelCoordination'; type MainLayoutProps = { - backgroundColor?: 'white' | 'grey'; enableResizablePanel?: boolean; }; export function MainLayout({ children, - backgroundColor = 'white', enableResizablePanel = false, }: PropsWithChildren<MainLayoutProps>) { return ( <Box className="--docs--main-layout"> - <Header /> - <Box - $direction="row" - $margin={{ top: `${HEADER_HEIGHT}px` }} - $width="100%" - $height={`calc(100dvh - ${HEADER_HEIGHT}px)`} - > - <MainLayoutContent - backgroundColor={backgroundColor} - enableResizablePanel={enableResizablePanel} - > + <Box $direction="row" $width="100%" $height="100dvh"> + <MainLayoutContent enableResizablePanel={enableResizablePanel}> {children} </MainLayoutContent> </Box> @@ -44,32 +32,15 @@ export function MainLayout({ } export interface MainLayoutContentProps { - backgroundColor: 'white' | 'grey'; enableResizablePanel: boolean; } export function MainLayoutContent({ children, - backgroundColor, enableResizablePanel, }: PropsWithChildren<MainLayoutContentProps>) { - const { isLargeScreen } = useResponsiveStore(); - if (enableResizablePanel) { - return ( - <MainResizableLayout backgroundColor={backgroundColor}> - {children} - </MainResizableLayout> - ); - } - - if (!isLargeScreen) { - return ( - <> - <LeftPanel /> - <MainContent backgroundColor={backgroundColor}>{children}</MainContent> - </> - ); + return <MainResizableLayout>{children}</MainResizableLayout>; } return ( @@ -83,29 +54,18 @@ export function MainLayoutContent({ > <LeftPanel /> </Box> - <MainContent backgroundColor={backgroundColor}>{children}</MainContent> + <MainContent>{children}</MainContent> </> ); } -interface MainResizableLayoutProps { - backgroundColor: 'white' | 'grey'; -} - -const MainResizableLayout = ({ - children, - backgroundColor, -}: PropsWithChildren<MainResizableLayoutProps>) => { +const MainResizableLayout = ({ children }: PropsWithChildren) => { usePanelCoordination(); return ( <ResizableLeftPanel leftPanel={<LeftPanel />}> <Box $direction="row" $width="100%" $position="relative"> - <MainContent - backgroundColor={backgroundColor} - $flex="auto" - $padding="0" - > + <MainContent $flex="auto" $padding="0"> {children} </MainContent> <RightPanel /> @@ -114,19 +74,9 @@ const MainResizableLayout = ({ ); }; -type MainContentProps = BoxProps & { - backgroundColor: 'white' | 'grey'; -}; - -const MainContent = ({ - children, - backgroundColor, - ...props -}: PropsWithChildren<MainContentProps>) => { +const MainContent = ({ children, ...props }: PropsWithChildren<BoxProps>) => { const { isDesktop } = useResponsiveStore(); - const { t } = useTranslation(); - const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor; return ( <Box @@ -137,14 +87,13 @@ const MainContent = ({ $align="center" $flex={1} $width="100%" - $height={`calc(100dvh - ${HEADER_HEIGHT}px)`} + $height="100dvh" $position="relative" - $padding={isDesktop ? 'base' : '0'} - $background={ - currentBackgroundColor === 'white' - ? 'var(--c--contextuals--background--surface--primary)' - : 'var(--c--contextuals--background--surface--tertiary)' - } + $padding={{ + all: isDesktop ? 'base' : '0', + top: `${HEADER_HEIGHT}px`, + }} + $background="var(--c--contextuals--background--surface--primary)" $css={css` overflow-y: auto; overflow-x: clip; From 963b029575a2c77510b4d15434bd2aec6fd7f440 Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Thu, 25 Jun 2026 17:21:17 +0200 Subject: [PATCH 3/9] save2 --- .../doc-header/components/DocFloatingBar.tsx | 38 +++++++ .../components/DocLeftPanelCollapseButton.tsx | 100 +++++++++++++++++ .../doc-header/components/FloatingBar.tsx | 82 -------------- .../docs/doc-header/components/index.ts | 2 +- .../docs/docs-grid/components/DocsGrid.tsx | 19 ++-- .../docs-grid/components/DocsGridItem.tsx | 17 +-- .../docs-grid/hooks/useResponsiveDocGrid.tsx | 14 +-- .../header/components/FloatingBar.tsx | 47 ++++++++ .../apps/impress/src/features/header/conf.ts | 2 +- .../components/LeftPanelCollapseButton.tsx | 102 +++--------------- .../apps/impress/src/layouts/MainLayout.tsx | 7 -- .../impress/src/pages/docs/[id]/index.tsx | 4 +- .../apps/impress/src/pages/docs/index.tsx | 11 +- 13 files changed, 229 insertions(+), 216 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/components/DocLeftPanelCollapseButton.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx create mode 100644 src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx new file mode 100644 index 0000000000..abae4313e8 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx @@ -0,0 +1,38 @@ +import { css } from 'styled-components'; + +import { Box, Card } from '@/components'; +import { useDocStore } from '@/docs/doc-management/stores/useDocStore'; +import { DocShareButton } from '@/features/docs/doc-share/components/DocShareButton'; +import { FloatingBar } from '@/features/header/components/FloatingBar'; +import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton'; + +import { DocLeftPanelCollapseButton } from './DocLeftPanelCollapseButton'; +import { DocToolBox } from './DocToolBox'; + +export const DocFloatingBar = () => { + const { currentDoc } = useDocStore(); + const isDeletedDoc = !!currentDoc?.deleted_at; + + return ( + <FloatingBar> + <DocLeftPanelCollapseButton /> + <Box $direction="row" $align="center" $gap="2xs"> + {!isDeletedDoc && currentDoc && <DocShareButton doc={currentDoc} />} + <Card + className="--docs--floating-bar-toolbox" + $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); + `} + > + <RightPanelCollapseButton /> + {!isDeletedDoc && currentDoc && <DocToolBox doc={currentDoc} />} + </Card> + </Box> + </FloatingBar> + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocLeftPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocLeftPanelCollapseButton.tsx new file mode 100644 index 0000000000..79b9aa5343 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocLeftPanelCollapseButton.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { CLASS_DOC_TITLE } from '@/docs/doc-header/components/DocTitle'; +import { getEmojiAndTitle, useDocStore, useTrans } from '@/docs/doc-management'; +import { + LeftPanelCollapseButton, + useLeftPanelStore, +} from '@/features/left-panel'; +import { MAIN_LAYOUT_ID } from '@/layouts/conf'; + +export const DocLeftPanelCollapseButton = () => { + const { t } = useTranslation(); + const { isPanelOpen } = useLeftPanelStore(); + const { currentDoc } = useDocStore(); + const [isDocTitleVisible, setIsDocTitleVisible] = useState(true); + const [isDocTitleInDom, setIsDocTitleInDom] = useState(true); + + /** + * CLASS_DOC_TITLE is not every time in the DOM when + * this component is rendered, we need to observe the DOM + * to know when it is added, then we can observe + * its visibility. + */ + useEffect(() => { + setIsDocTitleInDom(false); + + const docTitleEl = document.querySelector(`.${CLASS_DOC_TITLE}`); + if (docTitleEl) { + setIsDocTitleInDom(true); + return; + } + + const mutationObserver = new MutationObserver(() => { + if (document.querySelector(`.${CLASS_DOC_TITLE}`)) { + mutationObserver.disconnect(); + setIsDocTitleInDom(true); + } + }); + + mutationObserver.observe(document.body, { childList: true, subtree: true }); + + return () => { + mutationObserver.disconnect(); + }; + }, [currentDoc?.id]); + + /** + * When the doc title is in the DOM, we observe its + * visibility to show or hide the collapse button accordingly + */ + useEffect(() => { + if (!isDocTitleInDom) { + return; + } + + const mainContent = document.getElementById(MAIN_LAYOUT_ID); + const docTitleEl = document.querySelector(`.${CLASS_DOC_TITLE}`); + + if (!mainContent || !docTitleEl) { + return; + } + + const observer = new IntersectionObserver( + ([entry]) => { + setIsDocTitleVisible(entry.isIntersecting); + }, + { + root: mainContent, + threshold: 0.05, + }, + ); + + observer.observe(docTitleEl); + + return () => { + observer.disconnect(); + setIsDocTitleVisible(true); + }; + }, [isDocTitleInDom]); + + const { untitledDocument } = useTrans(); + + const { emoji, titleWithoutEmoji } = getEmojiAndTitle( + currentDoc?.title ?? '', + ); + const docTitle = titleWithoutEmoji || untitledDocument; + const buttonTitle = emoji ? `${emoji} ${docTitle}` : docTitle; + const shouldShowButtonTitle = !isPanelOpen && !isDocTitleVisible; + const ariaLabel = isPanelOpen + ? t('Hide the side panel for {{title}}', { title: docTitle }) + : t('Show the side panel for {{title}}', { title: docTitle }); + + return ( + <LeftPanelCollapseButton + ariaLabel={ariaLabel} + buttonTitle={shouldShowButtonTitle ? buttonTitle : undefined} + /> + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx deleted file mode 100644 index 24224432d0..0000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/FloatingBar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { css } from 'styled-components'; - -import { Box, Card } from '@/components'; -import { useDocStore } from '@/docs/doc-management/stores/useDocStore'; -import { DocShareButton } from '@/features/docs/doc-share/components/DocShareButton'; -import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPanelCollapseButton'; -import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton'; -import { useResponsiveStore } from '@/stores'; - -import { DocToolBox } from './DocToolBox'; - -const FLOATING_STYLES = css` - position: sticky; - top: 0; - left: 0; - right: 0; - width: 100%; - padding: var(--c--globals--spacings--sm); - padding-bottom: 0; - z-index: 10; // Under editor select box but above other elements (e.g., doc title, suggestion menu) - align-items: flex-start; - isolation: isolate; - - &::before { - content: ''; - position: absolute; - inset: 0; - z-index: -1; - background: linear-gradient(180deg, #fff 0%, rgba(255, 255, 255, 0) 100%); - backdrop-filter: blur(1px); - -webkit-backdrop-filter: blur(1px); - mask-image: linear-gradient(180deg, black 50%, transparent 100%); - -webkit-mask-image: linear-gradient(180deg, black 50%, transparent 100%); - } - - > * { - position: relative; - z-index: 1; - } -`; - -/** - * Sticky bar trick (desktop): - * - MainContent has padding `base`; we extend the bar width and apply - * matching negative margins so it aligns with the scroll area edges. - * - * Mobile: returns null to avoid header overlap. - */ -export const FloatingBar = () => { - const { isLargeScreen } = useResponsiveStore(); - const { currentDoc } = useDocStore(); - const isDeletedDoc = !!currentDoc?.deleted_at; - - return ( - <Box - className="--docs--floating-bar" - data-testid="floating-bar" - $css={FLOATING_STYLES} - $direction="row" - $justify="space-between" - > - {isLargeScreen ? <LeftPanelCollapseButton /> : <Box />} - <Box $direction="row" $align="center" $gap="2xs"> - {!isDeletedDoc && currentDoc && <DocShareButton doc={currentDoc} />} - <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); - `} - > - <RightPanelCollapseButton /> - {!isDeletedDoc && currentDoc && <DocToolBox doc={currentDoc} />} - </Card> - </Box> - </Box> - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts index 1163e9748e..5da7e27827 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/index.ts @@ -1,3 +1,3 @@ export * from './DocHeader'; export * from './DocTitle'; -export * from './FloatingBar'; +export * from './DocFloatingBar'; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index b70f8467e9..5b71b12f46 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -9,6 +9,7 @@ import { Box, Card, Icon, Text } from '@/components'; import { useInfiniteDocs } from '@/docs/doc-management/api/useDocs'; import { useImport } from '@/docs/doc-management/hooks/useImport'; import { DocDefaultFilter } from '@/docs/doc-management/types'; +import { HEADER_HEIGHT } from '@/features/header'; import { useResponsiveStore } from '@/stores'; import { useInfiniteDocsTrashbin } from '../api'; @@ -82,7 +83,7 @@ export const DocsGrid = ({ $position="relative" $width="100%" $maxWidth="960px" - $maxHeight={`calc(100vh - 52px - ${isDesktop ? '2rem' : '0rem'})`} + $maxHeight={`calc(100vh - ${HEADER_HEIGHT}px)`} $align="center" className="--docs--doc-grid" > @@ -133,15 +134,13 @@ export const DocsGrid = ({ {t('Name')} </Text> </Box> - {isDesktop && ( - <Box $flex={flexRight} $padding={{ vertical: '3xs' }}> - <Text $size="xs" $weight="500" $variation="secondary"> - {DocDefaultFilter.TRASHBIN === target - ? t('Days remaining') - : t('Last modified')} - </Text> - </Box> - )} + <Box $flex={flexRight} $padding={{ vertical: '3xs' }}> + <Text $size="xs" $weight="500" $variation="secondary"> + {DocDefaultFilter.TRASHBIN === target + ? t('Days remaining') + : t('Last modified')} + </Text> + </Box> </Box> <Box role="list"> <DocGridContentList docs={docs} /> diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx index 4677168120..6f3f729242 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx @@ -52,11 +52,10 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { $padding={{ vertical: '4xs' }} $css={css` cursor: pointer; - border-block: 1px solid + border-bottom: 1px solid var(--c--contextuals--border--surface--primary); &:hover { - border-radius: 4px; background-color: ${dragMode ? 'none' : 'var(--c--contextuals--background--semantic--contextual--primary)'}; @@ -91,7 +90,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { $flex={flexRight} $direction="row" $align="center" - $justify={isDesktop ? 'space-between' : 'flex-end'} + $justify="space-between" $gap="32px" > <StyledLink @@ -102,11 +101,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { date: dateToDisplay, })} > - <DocsGridItemDate - doc={doc} - isDesktop={isDesktop} - isInTrashbin={isInTrashbin} - /> + <DocsGridItemDate doc={doc} isInTrashbin={isInTrashbin} /> </StyledLink> <Box $direction="row" $align="center" $gap={spacingsTokens.lg}> @@ -226,19 +221,13 @@ const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => { export const DocsGridItemDate = ({ doc, - isDesktop, isInTrashbin, }: { doc: Doc; - isDesktop: boolean; isInTrashbin: boolean; }) => { const dateToDisplay = useDateToDisplay(doc, isInTrashbin); - if (!isDesktop) { - return null; - } - return ( <Text $size="xs" diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useResponsiveDocGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useResponsiveDocGrid.tsx index bedbdd293d..1fcbecc010 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useResponsiveDocGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useResponsiveDocGrid.tsx @@ -3,27 +3,23 @@ import { useMemo } from 'react'; import { useResponsiveStore } from '@/stores'; export const useResponsiveDocGrid = () => { - const { isDesktop, screenWidth } = useResponsiveStore(); + const { screenWidth } = useResponsiveStore(); const flexLeft = useMemo(() => { - if (!isDesktop) { - return 1; - } else if (screenWidth <= 1100) { + if (screenWidth <= 1100) { return 6; } else if (screenWidth < 1200) { return 8; } return 8; - }, [isDesktop, screenWidth]); + }, [screenWidth]); const flexRight = useMemo(() => { - if (!isDesktop) { - return undefined; - } else if (screenWidth <= 1200) { + if (screenWidth <= 1200) { return 5; } return 4; - }, [isDesktop, screenWidth]); + }, [screenWidth]); return { flexLeft, flexRight }; }; diff --git a/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx b/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx new file mode 100644 index 0000000000..c65d551cfb --- /dev/null +++ b/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx @@ -0,0 +1,47 @@ +import { PropsWithChildren } from 'react'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; + +const FLOATING_STYLES = css` + position: sticky; + top: 0; + left: 0; + right: 0; + width: 100%; + padding: var(--c--globals--spacings--sm); + z-index: 10; // Under editor select box but above other elements (e.g., doc title, suggestion menu) + align-items: flex-start; + isolation: isolate; + + &::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient(180deg, #fff 0%, rgba(255, 255, 255, 0) 100%); + backdrop-filter: blur(1px); + -webkit-backdrop-filter: blur(1px); + mask-image: linear-gradient(180deg, black 50%, transparent 100%); + -webkit-mask-image: linear-gradient(180deg, black 50%, transparent 100%); + } + + > * { + position: relative; + z-index: 1; + } +`; + +export const FloatingBar = ({ children }: PropsWithChildren) => { + return ( + <Box + className="--docs--floating-bar" + data-testid="floating-bar" + $css={FLOATING_STYLES} + $direction="row" + $justify="space-between" + > + {children} + </Box> + ); +}; diff --git a/src/frontend/apps/impress/src/features/header/conf.ts b/src/frontend/apps/impress/src/features/header/conf.ts index b50057ad71..5220c17b39 100644 --- a/src/frontend/apps/impress/src/features/header/conf.ts +++ b/src/frontend/apps/impress/src/features/header/conf.ts @@ -1 +1 @@ -export const HEADER_HEIGHT = 52; +export const HEADER_HEIGHT = 66; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx index a76e5667d4..04eeea7b21 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx @@ -1,99 +1,19 @@ import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Card, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { CLASS_DOC_TITLE } from '@/docs/doc-header/components/DocTitle'; -import { getEmojiAndTitle, useDocStore, useTrans } from '@/docs/doc-management'; -import { MAIN_LAYOUT_ID } from '@/layouts/conf'; import LeftPanelIcon from '../assets/left-panel.svg'; import { useLeftPanelStore } from '../stores'; -export const LeftPanelCollapseButton = () => { - const { t } = useTranslation(); - const { colorsTokens } = useCunninghamTheme(); +export const LeftPanelCollapseButton = ({ + ariaLabel, + buttonTitle, +}: { + ariaLabel: string; + buttonTitle?: string; +}) => { const { isPanelOpen, togglePanel } = useLeftPanelStore(); - const { currentDoc } = useDocStore(); - const [isDocTitleVisible, setIsDocTitleVisible] = useState(true); - const [isDocTitleInDom, setIsDocTitleInDom] = useState(true); - - /** - * CLASS_DOC_TITLE is not every time in the DOM when - * this component is rendered, we need to observe the DOM - * to know when it is added, then we can observe - * its visibility. - */ - useEffect(() => { - setIsDocTitleInDom(false); - - const docTitleEl = document.querySelector(`.${CLASS_DOC_TITLE}`); - if (docTitleEl) { - setIsDocTitleInDom(true); - return; - } - - const mutationObserver = new MutationObserver(() => { - if (document.querySelector(`.${CLASS_DOC_TITLE}`)) { - mutationObserver.disconnect(); - setIsDocTitleInDom(true); - } - }); - - mutationObserver.observe(document.body, { childList: true, subtree: true }); - - return () => { - mutationObserver.disconnect(); - }; - }, [currentDoc?.id]); - - /** - * When the doc title is in the DOM, we observe its - * visibility to show or hide the collapse button accordingly - */ - useEffect(() => { - if (!isDocTitleInDom) { - return; - } - - const mainContent = document.getElementById(MAIN_LAYOUT_ID); - const docTitleEl = document.querySelector(`.${CLASS_DOC_TITLE}`); - - if (!mainContent || !docTitleEl) { - return; - } - - const observer = new IntersectionObserver( - ([entry]) => { - setIsDocTitleVisible(entry.isIntersecting); - }, - { - root: mainContent, - threshold: 0.05, - }, - ); - - observer.observe(docTitleEl); - - return () => { - observer.disconnect(); - setIsDocTitleVisible(true); - }; - }, [isDocTitleInDom]); - - const { untitledDocument } = useTrans(); - - const { emoji, titleWithoutEmoji } = getEmojiAndTitle( - currentDoc?.title ?? '', - ); - const docTitle = titleWithoutEmoji || untitledDocument; - const buttonTitle = emoji ? `${emoji} ${docTitle}` : docTitle; - const shouldShowButtonTitle = !isPanelOpen && !isDocTitleVisible; - const ariaLabel = isPanelOpen - ? t('Hide the side panel for {{title}}', { title: docTitle }) - : t('Show the side panel for {{title}}', { title: docTitle }); return ( <Card @@ -117,8 +37,12 @@ export const LeftPanelCollapseButton = () => { icon={<LeftPanelIcon width={24} height={24} aria-hidden="true" />} data-testid="floating-bar-toggle-left-panel" ></Button> - {shouldShowButtonTitle && ( - <Text $size="sm" $weight={700} $color={colorsTokens['gray-1000']}> + {buttonTitle && ( + <Text + $size="sm" + $weight={700} + $color="var(--c--globals--colors--gray-1000)" + > {buttonTitle} </Text> )} diff --git a/src/frontend/apps/impress/src/layouts/MainLayout.tsx b/src/frontend/apps/impress/src/layouts/MainLayout.tsx index 30b91bfe38..76af7cf221 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -3,11 +3,9 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box, BoxProps } from '@/components'; -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'; import { MAIN_LAYOUT_ID } from './conf'; import { usePanelCoordination } from './usePanelCoordination'; @@ -75,7 +73,6 @@ const MainResizableLayout = ({ children }: PropsWithChildren) => { }; const MainContent = ({ children, ...props }: PropsWithChildren<BoxProps>) => { - const { isDesktop } = useResponsiveStore(); const { t } = useTranslation(); return ( @@ -89,10 +86,6 @@ const MainContent = ({ children, ...props }: PropsWithChildren<BoxProps>) => { $width="100%" $height="100dvh" $position="relative" - $padding={{ - all: isDesktop ? 'base' : '0', - top: `${HEADER_HEIGHT}px`, - }} $background="var(--c--contextuals--background--surface--primary)" $css={css` overflow-y: auto; 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 f074f20d9f..097e69a61d 100644 --- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx @@ -17,7 +17,7 @@ import { useTrans, } from '@/docs/doc-management/'; import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth'; -import { FloatingBar } from '@/features/docs/doc-header/components/FloatingBar'; +import { DocFloatingBar } from '@/features/docs/doc-header/components/DocFloatingBar'; import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/'; import { DocEditorSkeleton, useSkeletonStore } from '@/features/skeletons'; import { MainLayout } from '@/layouts'; @@ -65,7 +65,7 @@ export function DocLayout() { }} > <MainLayout enableResizablePanel={true}> - <FloatingBar /> + <DocFloatingBar /> <DocPage id={id} /> </MainLayout> </TreeProvider> diff --git a/src/frontend/apps/impress/src/pages/docs/index.tsx b/src/frontend/apps/impress/src/pages/docs/index.tsx index 35ed6a6c9b..40993c5bb9 100644 --- a/src/frontend/apps/impress/src/pages/docs/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/index.tsx @@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'; import { DocDefaultFilter, useTrans } from '@/docs/doc-management'; import { DocsGrid } from '@/docs/docs-grid'; +import { FloatingBar } from '@/features/header/components/FloatingBar'; +import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPanelCollapseButton'; import { MainLayout } from '@/layouts'; import { NextPageWithLayout } from '@/types/next'; @@ -33,7 +35,14 @@ const Page: NextPageWithLayout = () => { }; Page.getLayout = function getLayout(page: ReactElement) { - return <MainLayout backgroundColor="grey">{page}</MainLayout>; + return ( + <MainLayout> + <FloatingBar> + <LeftPanelCollapseButton ariaLabel="Toggle left panel" /> + </FloatingBar> + {page} + </MainLayout> + ); }; export default Page; From 0c3fa10b4f94f76d77d278dd734980de921620c6 Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Fri, 26 Jun 2026 09:38:55 +0200 Subject: [PATCH 4/9] save --- .../docs/docs-grid/components/DocsGrid.tsx | 3 +- .../header/components/FloatingBar.tsx | 1 + .../left-panel/components/LeftPanel.tsx | 97 ++++--------------- .../apps/impress/src/layouts/MainLayout.tsx | 10 +- .../apps/impress/src/pages/docs/index.tsx | 16 +-- 5 files changed, 30 insertions(+), 97 deletions(-) diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index 5b71b12f46..db3f07401f 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -9,7 +9,6 @@ import { Box, Card, Icon, Text } from '@/components'; import { useInfiniteDocs } from '@/docs/doc-management/api/useDocs'; import { useImport } from '@/docs/doc-management/hooks/useImport'; import { DocDefaultFilter } from '@/docs/doc-management/types'; -import { HEADER_HEIGHT } from '@/features/header'; import { useResponsiveStore } from '@/stores'; import { useInfiniteDocsTrashbin } from '../api'; @@ -83,7 +82,7 @@ export const DocsGrid = ({ $position="relative" $width="100%" $maxWidth="960px" - $maxHeight={`calc(100vh - ${HEADER_HEIGHT}px)`} + $minHeight="0" $align="center" className="--docs--doc-grid" > diff --git a/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx b/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx index c65d551cfb..9745f330da 100644 --- a/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx @@ -13,6 +13,7 @@ const FLOATING_STYLES = css` z-index: 10; // Under editor select box but above other elements (e.g., doc title, suggestion menu) align-items: flex-start; isolation: isolate; + min-height: 66px; &::before { content: ''; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index ce7ffaadd8..e92a4c2323 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -1,48 +1,40 @@ -import { usePathname } from 'next/navigation'; -import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { createGlobalStyle, css } from 'styled-components'; +import { css } from 'styled-components'; import { Box } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { useResponsiveStore } from '@/stores'; - -import { useLeftPanelStore } from '../stores'; +import { useResponsiveStore } from '@/stores/useResponsiveStore'; import { LeftPanelContent } from './LeftPanelContent'; import { LeftPanelFooter } from './LeftPanelFooter'; import { LeftPanelHeader } from './LeftPanelHeader'; -const MobileLeftPanelStyle = createGlobalStyle` - body { - overflow: hidden; - } -`; - export const LeftPanel = () => { - const { isLargeScreen } = useResponsiveStore(); - if (isLargeScreen) { - return <LeftPanelDesktop />; - } - - return <LeftPanelMobile />; -}; - -export const LeftPanelDesktop = () => { const { t } = useTranslation(); + const { isMobile } = useResponsiveStore(); return ( <Box - data-testid="left-panel-desktop" + as="nav" + className="--docs--left-panel" + data-testid="left-panel" + aria-label={t('Left panel')} $css={css` height: 100vh; - width: 100%; overflow: hidden; background-color: var(--c--contextuals--background--surface--primary); + width: 300px; + border-right: 1px solid var(--c--contextuals--border--surface--primary); + + ${isMobile + ? ` + position: fixed; + z-index: 1000; + top: 0; + left: 0; + height: 100dvh; + ` + : ''} `} - className="--docs--left-panel-desktop" - as="nav" - aria-label={t('Document sections')} > <Box $css={css` @@ -56,54 +48,3 @@ export const LeftPanelDesktop = () => { </Box> ); }; - -const LeftPanelMobile = () => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - const { closePanel, isPanelOpenMobile } = useLeftPanelStore(); - const pathname = usePathname(); - - useEffect(() => { - closePanel({ type: 'mobile' }); - }, [pathname, closePanel]); - - return ( - <> - {isPanelOpenMobile && <MobileLeftPanelStyle />} - <Box - $hasTransition - $height="100vh" - inert={!isPanelOpenMobile} - $css={css` - z-index: 999; - width: 100dvw; - height: calc(100dvh - 52px); - border-right: 1px solid var(--c--globals--colors--gray-200); - position: fixed; - transform: translateX(${isPanelOpenMobile ? '0' : '-100dvw'}); - background-color: var(--c--contextuals--background--surface--primary); - overflow-y: auto; - overflow-x: hidden; - `} - className="--docs--left-panel-mobile" - > - <Box - data-testid="left-panel-mobile" - as="nav" - aria-label={t('Document sections')} - $css={css` - width: 100%; - justify-content: center; - align-items: center; - gap: ${spacingsTokens['base']}; - `} - $height="inherit" - > - <LeftPanelHeader /> - <LeftPanelContent /> - <LeftPanelFooter /> - </Box> - </Box> - </> - ); -}; diff --git a/src/frontend/apps/impress/src/layouts/MainLayout.tsx b/src/frontend/apps/impress/src/layouts/MainLayout.tsx index 76af7cf221..f4748a948f 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -43,15 +43,7 @@ export function MainLayoutContent({ return ( <> - <Box - $css={css` - width: 300px; - border-right: 1px solid - var(--c--contextuals--border--surface--primary); - `} - > - <LeftPanel /> - </Box> + <LeftPanel /> <MainContent>{children}</MainContent> </> ); diff --git a/src/frontend/apps/impress/src/pages/docs/index.tsx b/src/frontend/apps/impress/src/pages/docs/index.tsx index 40993c5bb9..9dc1dc9f08 100644 --- a/src/frontend/apps/impress/src/pages/docs/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/index.tsx @@ -8,6 +8,7 @@ import { DocsGrid } from '@/docs/docs-grid'; import { FloatingBar } from '@/features/header/components/FloatingBar'; import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPanelCollapseButton'; import { MainLayout } from '@/layouts'; +import { useResponsiveStore } from '@/stores/useResponsiveStore'; import { NextPageWithLayout } from '@/types/next'; const Page: NextPageWithLayout = () => { @@ -18,6 +19,7 @@ const Page: NextPageWithLayout = () => { (searchParams.get('target') as DocDefaultFilter) ?? DocDefaultFilter.ALL_DOCS; const pageTitle = transFilter(target); + const { isMobile } = useResponsiveStore(); return ( <> @@ -29,20 +31,18 @@ const Page: NextPageWithLayout = () => { key="title" /> </Head> + <FloatingBar> + {isMobile && ( + <LeftPanelCollapseButton ariaLabel={t('Toggle left panel')} /> + )} + </FloatingBar> <DocsGrid target={target} /> </> ); }; Page.getLayout = function getLayout(page: ReactElement) { - return ( - <MainLayout> - <FloatingBar> - <LeftPanelCollapseButton ariaLabel="Toggle left panel" /> - </FloatingBar> - {page} - </MainLayout> - ); + return <MainLayout>{page}</MainLayout>; }; export default Page; From 11c9a51c89a8121e20b9f871b2dc6d586d6b8a05 Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Fri, 26 Jun 2026 10:13:22 +0200 Subject: [PATCH 5/9] save --- .../ButtonCloseModal.tsx => ButtonClose.tsx} | 2 +- .../apps/impress/src/components/index.ts | 1 + .../impress/src/components/modal/index.ts | 1 - .../components/CommentSideBar.tsx | 4 +- .../doc-export/components/ModalExport.tsx | 4 +- .../components/ModalRemoveDoc.tsx | 4 +- .../doc-search/components/DocSearchModal.tsx | 4 +- .../components/ConfirmationLeaveModal.tsx | 4 +- .../doc-share/components/DocShareModal.tsx | 4 +- .../components/TableContentSideBar.tsx | 4 +- .../doc-tree/components/DocSubPageItem.tsx | 4 +- .../components/ModalSelectVersion.tsx | 4 +- .../docs-grid/components/DocMoveModal.tsx | 5 +- .../__tests__/DocsGridItemDate.test.tsx | 4 -- .../components/LefPanelTargetFilters.tsx | 2 +- .../left-panel/components/LeftPanel.tsx | 23 +++++--- .../components/LeftPanelCollapseButton.tsx | 2 +- .../left-panel/components/LeftPanelHeader.tsx | 23 +++++--- .../components/LeftPanelToggleMobile.tsx | 15 ++---- .../left-panel/stores/useLeftPanelStore.tsx | 53 +++---------------- .../src/layouts/usePanelCoordination.tsx | 4 +- src/frontend/yarn.lock | 5 -- 22 files changed, 71 insertions(+), 105 deletions(-) rename src/frontend/apps/impress/src/components/{modal/ButtonCloseModal.tsx => ButtonClose.tsx} (85%) diff --git a/src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx b/src/frontend/apps/impress/src/components/ButtonClose.tsx similarity index 85% rename from src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx rename to src/frontend/apps/impress/src/components/ButtonClose.tsx index 63cc8bc29e..ecea8d54a5 100644 --- a/src/frontend/apps/impress/src/components/modal/ButtonCloseModal.tsx +++ b/src/frontend/apps/impress/src/components/ButtonClose.tsx @@ -2,7 +2,7 @@ import { Button, type ButtonProps } from '@gouvfr-lasuite/cunningham-react'; import CloseIcon from '@/assets/icons/ui-kit/x-mark.svg'; -export const ButtonCloseModal = (props: ButtonProps) => { +export const ButtonClose = (props: ButtonProps) => { return ( <Button type="button" diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts index 9075311abf..1a8a4c5009 100644 --- a/src/frontend/apps/impress/src/components/index.ts +++ b/src/frontend/apps/impress/src/components/index.ts @@ -1,5 +1,6 @@ export * from './Box'; export * from './BoxButton'; +export * from './ButtonClose'; export * from './Card'; export * from './DropButton'; export * from './dropdown-menu/DropdownMenu'; diff --git a/src/frontend/apps/impress/src/components/modal/index.ts b/src/frontend/apps/impress/src/components/modal/index.ts index c16e0e99d1..11055cbacb 100644 --- a/src/frontend/apps/impress/src/components/modal/index.ts +++ b/src/frontend/apps/impress/src/components/modal/index.ts @@ -1,2 +1 @@ export * from './AlertModal'; -export * from './ButtonCloseModal'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-comments/components/CommentSideBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-comments/components/CommentSideBar.tsx index 903941de31..1068bf0a35 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-comments/components/CommentSideBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-comments/components/CommentSideBar.tsx @@ -11,7 +11,7 @@ import { css } from 'styled-components'; import CommentsIcon from '@/assets/icons/ui-kit/bubble-text.svg'; import SortingResolvedSVG from '@/assets/icons/ui-kit/filter-notification.svg'; import SortingOpenSVG from '@/assets/icons/ui-kit/filter_list.svg'; -import { Box, ButtonCloseModal, Text } from '@/components/'; +import { Box, ButtonClose, Text } from '@/components/'; import { useRightPanelStore } from '@/features/right-panel/stores/useRightPanelStore'; import { useFocusStore } from '@/stores'; @@ -99,7 +99,7 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => { </Tooltip> </DropdownMenu> </Box> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the comments sidebar')} onClick={onClose} /> 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 b504c9fc9f..2b26107c73 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 @@ -24,7 +24,7 @@ import { import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -import { Box, ButtonCloseModal, Text } from '@/components'; +import { Box, ButtonClose, Text } from '@/components'; import { useMediaUrl } from '@/core'; import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; import { Doc, useTrans } from '@/docs/doc-management'; @@ -274,7 +274,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { {t('Export')} </Text> <Box $position="absolute" $css="top: 4px; right: 4px;"> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the download modal')} onClick={() => onClose()} disabled={isExporting} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx index 32e7de28ce..0297d962fa 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx @@ -10,7 +10,7 @@ import { useRouter } from 'next/router'; import { useEffect, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Box, ButtonCloseModal, Text, TextErrors } from '@/components'; +import { Box, ButtonClose, Text, TextErrors } from '@/components'; import { useConfig } from '@/core'; import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid'; import { useKeyboardAction } from '@/hooks'; @@ -129,7 +129,7 @@ export const ModalRemoveDoc = ({ {t('Delete a doc')} </Text> <Box $position="absolute" $css="top: 4px; right: 4px;"> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the delete modal')} onClick={handleClose} onKeyDown={handleCloseKeyDown} diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx index 68b12a33b2..30200b1fd7 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { createGlobalStyle } from 'styled-components'; import { useDebouncedCallback } from 'use-debounce'; -import { Box, ButtonCloseModal, Text } from '@/components'; +import { Box, ButtonClose, Text } from '@/components'; import { QuickSearch } from '@/components/quick-search'; import { Doc, useDocUtils } from '@/docs/doc-management'; import { useAuth } from '@/features/auth/hooks/useAuth'; @@ -110,7 +110,7 @@ const DocSearchModalGlobal = ({ )} </Text> <Box $position="absolute" $css="top: 4px; right: 4px;"> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the search modal')} onClick={modalProps.onClose} /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx index 9052fc9386..dd450401c9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { createGlobalStyle, css } from 'styled-components'; -import { Box, ButtonCloseModal, Text } from '@/components'; +import { Box, ButtonClose, Text } from '@/components'; import { useAuth } from '@/features/auth'; import { Doc } from '../../doc-management'; @@ -81,7 +81,7 @@ export const ConfirmationLeaveModal = ({ {t('Leave a doc')} </Text> <Box $position="absolute" $css="top: 4px; right: 4px;"> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the leave modal')} onClick={onClose} /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx index 01f1c729a1..c469f71f03 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { createGlobalStyle, css } from 'styled-components'; import { useDebouncedCallback } from 'use-debounce'; -import { Box, ButtonCloseModal, HorizontalSeparator, Text } from '@/components'; +import { Box, ButtonClose, HorizontalSeparator, Text } from '@/components'; import { QuickSearch, QuickSearchData, @@ -196,7 +196,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => { > {t('Share the document')} </Text> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the share modal')} onClick={onClose} /> 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 index c23b3047ed..2cde9b9962 100644 --- 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 @@ -4,7 +4,7 @@ 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 { Box, ButtonClose, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; import { useHeadingStore } from '@/docs/doc-editor/stores/useHeadingStore'; @@ -104,7 +104,7 @@ export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => { > {t('Table of Contents')} </Text> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the table of contents sidebar')} onClick={onClose} /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx index 3f21328c5b..8aa5f614b8 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx @@ -127,7 +127,7 @@ const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => { node.data.value.id, allChildren as TreeViewDataType<Doc>[], ); - togglePanel({ type: 'mobile' }); + togglePanel(); }) .catch(console.error); } else { @@ -140,7 +140,7 @@ const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => { treeContext?.treeData.addChild(node.data.value.id, newDoc); node.open(); void router.push(`/docs/${createdDoc.id}`); - togglePanel({ type: 'mobile' }); + togglePanel(); } }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx index df4a282738..bba2963e9c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalSelectVersion.tsx @@ -9,7 +9,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { createGlobalStyle, css } from 'styled-components'; -import { Box, ButtonCloseModal, Text } from '@/components'; +import { Box, ButtonClose, Text } from '@/components'; import { Doc } from '@/docs/doc-management'; import { Versions } from '../types'; @@ -139,7 +139,7 @@ export const ModalSelectVersion = ({ <Text $size="h6" $weight="bold"> {t('History')} </Text> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the version history modal')} autoFocus onClick={onClose} diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx index 7954fd7d2e..5b6e930e8a 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { createGlobalStyle, css } from 'styled-components'; import { useDebouncedCallback } from 'use-debounce'; -import { Box, ButtonCloseModal, Text } from '@/components'; +import { Box, ButtonClose, Text } from '@/components'; import { QuickSearch } from '@/components/quick-search'; import { Doc, useMoveDoc, useTrans } from '@/docs/doc-management'; import { DocSearchContent } from '@/docs/doc-search'; @@ -178,7 +178,7 @@ export const DocMoveModal = ({ {t('Choose a new parent doc')} </Text> <Box $position="absolute" $css="top: 4px; right: 4px;"> - <ButtonCloseModal + <ButtonClose aria-label={t('Close the move modal')} onClick={onClose} /> @@ -257,7 +257,6 @@ export const DocMoveModal = ({ /> <DocsGridItemDate doc={docSearch} - isDesktop={isModal} isInTrashbin={false} /> </Box> diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx index 8c1d6d42e8..0a42a937c0 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx @@ -21,7 +21,6 @@ describe('DocsGridItemDate', () => { doc={ { updated_at: DateTime.now().minus({ minutes: 1 }).toISO() } as Doc } - isDesktop={false} isInTrashbin={false} />, { @@ -62,7 +61,6 @@ describe('DocsGridItemDate', () => { updated_at, } as Doc } - isDesktop={true} isInTrashbin={false} />, { wrapper: AppWrapper }, @@ -84,7 +82,6 @@ describe('DocsGridItemDate', () => { updated_at: DateTime.now().minus({ days: 5 }).toISO(), } as Doc } - isDesktop={true} isInTrashbin={false} />, { wrapper: AppWrapper }, @@ -132,7 +129,6 @@ describe('DocsGridItemDate', () => { updated_at, } as Doc } - isDesktop={true} isInTrashbin={true} />, { wrapper: AppWrapper }, diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx index 2df9012982..46043d6187 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx @@ -50,7 +50,7 @@ export const LeftPanelTargetFilters = () => { }; const handleFilterClick = () => { - closePanel({ type: 'mobile' }); + closePanel(); }; return ( diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index e92a4c2323..110a339660 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -4,6 +4,8 @@ import { css } from 'styled-components'; import { Box } from '@/components'; import { useResponsiveStore } from '@/stores/useResponsiveStore'; +import { useLeftPanelStore } from '../stores'; + import { LeftPanelContent } from './LeftPanelContent'; import { LeftPanelFooter } from './LeftPanelFooter'; import { LeftPanelHeader } from './LeftPanelHeader'; @@ -11,6 +13,7 @@ import { LeftPanelHeader } from './LeftPanelHeader'; export const LeftPanel = () => { const { t } = useTranslation(); const { isMobile } = useResponsiveStore(); + const { isPanelOpen } = useLeftPanelStore(); return ( <Box @@ -26,13 +29,19 @@ export const LeftPanel = () => { border-right: 1px solid var(--c--contextuals--border--surface--primary); ${isMobile - ? ` - position: fixed; - z-index: 1000; - top: 0; - left: 0; - height: 100dvh; - ` + ? css` + transition: transform 0.2s ease-in-out; + position: fixed; + z-index: 1000; + top: 0; + left: 0; + height: 100dvh; + ${!isPanelOpen + ? css` + transform: translateX(-100%); + ` + : ''} + ` : ''} `} > diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx index 04eeea7b21..52ff670345 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx @@ -29,7 +29,7 @@ export const LeftPanelCollapseButton = ({ > <Button size="small" - onClick={() => togglePanel({ type: 'desktop' })} + onClick={() => togglePanel()} aria-label={ariaLabel} aria-expanded={isPanelOpen} color="neutral" diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx index c2b51e7bbf..764d429a0c 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx @@ -5,23 +5,31 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box, SeparatedSection, StyledLink } from '@/components'; +import { ButtonClose } from '@/components/ButtonClose'; import { Title } from '@/components/Title'; import { useConfig } from '@/core'; import { NewDocButton } from '@/docs/doc-management/components/NewDocButton'; import { DocSearchButtonModal } from '@/docs/doc-search/components/DocSearchButtonModal'; import { useAuth } from '@/features/auth'; import HomeSVG from '@/icons/house-rounded.svg'; +import { useResponsiveStore } from '@/stores'; import { useLeftPanelStore } from '../stores'; export const LeftPanelHeader = () => { const { data: config } = useConfig(); - + const { isMobile } = useResponsiveStore(); + const { togglePanel } = useLeftPanelStore(); const icon = config?.theme_customization?.header?.icon; return ( <Box $width="100%" className="--docs--left-panel-header"> - <Box $padding={{ horizontal: 'xxs', vertical: 'xxs' }}> + <Box + $padding={{ horizontal: 'xxs', vertical: 'xxs' }} + $direction="row" + $align="center" + $gap="2xs" + > <StyledLink href="/" data-testid="header-logo-link" @@ -56,6 +64,11 @@ export const LeftPanelHeader = () => { /> </Box> </StyledLink> + {isMobile && ( + <Box $margin={{ left: 'auto' }}> + <ButtonClose onClick={() => togglePanel()} /> + </Box> + )} </Box> <LeftPanelHeaderActions /> </Box> @@ -69,7 +82,7 @@ export const LeftPanelHeaderActions = () => { const goToHome = () => { void router.push('/'); - togglePanel({ type: 'mobile' }); + togglePanel(); }; return ( @@ -81,9 +94,7 @@ export const LeftPanelHeaderActions = () => { $justify="space-between" $align="center" > - {authenticated && ( - <NewDocButton onClose={() => closePanel({ type: 'mobile' })} /> - )} + {authenticated && <NewDocButton onClose={() => closePanel()} />} <Box $direction="row" $gap="2px" $margin={{ left: 'auto' }}> {router.pathname !== '/' && ( <Button diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelToggleMobile.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelToggleMobile.tsx index 0bd20857fb..0bf5e68f47 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelToggleMobile.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelToggleMobile.tsx @@ -6,24 +6,19 @@ import { useLeftPanelStore } from '@/features/left-panel'; export const LeftPanelToggleMobile = () => { const { t } = useTranslation(); - const { isPanelOpenMobile, togglePanel } = useLeftPanelStore(); + const { isPanelOpen, togglePanel } = useLeftPanelStore(); return ( <Button size="medium" - onClick={() => togglePanel({ type: 'mobile' })} + onClick={() => togglePanel()} aria-label={ - isPanelOpenMobile - ? t('Close the header menu') - : t('Open the header menu') + isPanelOpen ? t('Close the header menu') : t('Open the header menu') } - aria-expanded={isPanelOpenMobile} + aria-expanded={isPanelOpen} variant="tertiary" icon={ - <Icon - $withThemeInherited - iconName={isPanelOpenMobile ? 'close' : 'menu'} - /> + <Icon $withThemeInherited iconName={isPanelOpen ? 'close' : 'menu'} /> } className="--docs--button-toggle-panel-mobile" data-testid="header-menu-toggle" diff --git a/src/frontend/apps/impress/src/features/left-panel/stores/useLeftPanelStore.tsx b/src/frontend/apps/impress/src/features/left-panel/stores/useLeftPanelStore.tsx index 3820d76e00..5f15ac8b75 100644 --- a/src/frontend/apps/impress/src/features/left-panel/stores/useLeftPanelStore.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/stores/useLeftPanelStore.tsx @@ -1,65 +1,26 @@ import { create } from 'zustand'; -type TogglePanelType = { type: 'desktop' | 'mobile' }; - -type TogglePanelArgs = { - value?: boolean; -} & Partial<TogglePanelType>; - interface LeftPanelState { isPanelOpen: boolean; - isPanelOpenMobile: boolean; /** * Depending on the responsive breakpoint, the panel can be auto-closed, and so * auto-opened later on. */ wasAutoClosed: boolean; - togglePanel: (args?: TogglePanelArgs) => void; - closePanel: (args?: TogglePanelType) => void; + togglePanel: () => void; + closePanel: () => void; autoClose: () => void; } export const useLeftPanelStore = create<LeftPanelState>((set, get) => ({ isPanelOpen: true, - isPanelOpenMobile: false, wasAutoClosed: false, - togglePanel: ({ value, type }: TogglePanelArgs = {}) => { - set({ wasAutoClosed: false }); - if (typeof value === 'boolean') { - if (type === 'mobile') { - set({ isPanelOpenMobile: value }); - return; - } - if (type === 'desktop') { - set({ isPanelOpen: value }); - return; - } - set({ isPanelOpen: value, isPanelOpenMobile: value }); - return; - } - - const { isPanelOpen, isPanelOpenMobile } = get(); - if (type === 'mobile') { - set({ isPanelOpenMobile: !isPanelOpenMobile }); - return; - } - if (type === 'desktop') { - set({ isPanelOpen: !isPanelOpen }); - return; - } - set({ isPanelOpen: !isPanelOpen, isPanelOpenMobile: !isPanelOpenMobile }); + togglePanel: () => { + const { isPanelOpen } = get(); + set({ isPanelOpen: !isPanelOpen, wasAutoClosed: false }); }, - closePanel: ({ type }: Partial<TogglePanelType> = {}) => { - set({ wasAutoClosed: false }); - if (type === 'mobile') { - set({ isPanelOpenMobile: false }); - return; - } - if (type === 'desktop') { - set({ isPanelOpen: false }); - return; - } - set({ isPanelOpen: false, isPanelOpenMobile: false }); + closePanel: () => { + set({ isPanelOpen: false, wasAutoClosed: false }); }, autoClose: () => set({ isPanelOpen: false, wasAutoClosed: true }), })); diff --git a/src/frontend/apps/impress/src/layouts/usePanelCoordination.tsx b/src/frontend/apps/impress/src/layouts/usePanelCoordination.tsx index 50c0c715cc..54f32af3bf 100644 --- a/src/frontend/apps/impress/src/layouts/usePanelCoordination.tsx +++ b/src/frontend/apps/impress/src/layouts/usePanelCoordination.tsx @@ -48,7 +48,7 @@ export function usePanelCoordination(): void { // Case 2 – leaving tablet if (screenSize !== 'tablet' && prevScreenSize === 'tablet') { if (useLeftPanelStore.getState().wasAutoClosed) { - toggleLeftPanel({ type: 'desktop', value: true }); + toggleLeftPanel(); } return; } @@ -64,7 +64,7 @@ export function usePanelCoordination(): void { // Case 4 – right closes on tablet if (screenSize === 'tablet' && !isRightPanelOpen && prevRightOpen) { if (useLeftPanelStore.getState().wasAutoClosed) { - toggleLeftPanel({ type: 'desktop', value: true }); + toggleLeftPanel(); } return; } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index fb673957bb..0bd81194fd 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -8323,11 +8323,6 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -crisp-sdk-web@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/crisp-sdk-web/-/crisp-sdk-web-1.1.2.tgz#e5b31ee784cef678a711170f54eca68e28968a22" - integrity sha512-qxSpiT4EGHnVEO6J/t1UURY6fg1ojgquLU0IIr7RNjZxXZs1R2e6AhAiFqxiGZ13Vb4o5VoUpATFU2TzX/ppOQ== - cross-env@10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-10.1.0.tgz#cfd2a6200df9ed75bfb9cb3d7ce609c13ea21783" From 17b190a376c4fac4be37372efddda75993a688f2 Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Fri, 26 Jun 2026 10:45:03 +0200 Subject: [PATCH 6/9] save --- .../left-panel/components/LeftPanel.tsx | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index 110a339660..f0409723e5 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -10,50 +10,68 @@ import { LeftPanelContent } from './LeftPanelContent'; import { LeftPanelFooter } from './LeftPanelFooter'; import { LeftPanelHeader } from './LeftPanelHeader'; +export const CLASS_NAME_LEFT_PANEL = '--docs--left-panel'; + export const LeftPanel = () => { const { t } = useTranslation(); const { isMobile } = useResponsiveStore(); - const { isPanelOpen } = useLeftPanelStore(); + const { isPanelOpen, closePanel } = useLeftPanelStore(); return ( - <Box - as="nav" - className="--docs--left-panel" - data-testid="left-panel" - aria-label={t('Left panel')} - $css={css` - height: 100vh; - overflow: hidden; - background-color: var(--c--contextuals--background--surface--primary); - width: 300px; - border-right: 1px solid var(--c--contextuals--border--surface--primary); - - ${isMobile - ? css` - transition: transform 0.2s ease-in-out; - position: fixed; - z-index: 1000; - top: 0; - left: 0; - height: 100dvh; - ${!isPanelOpen - ? css` - transform: translateX(-100%); - ` - : ''} - ` - : ''} - `} - > + <> + {isMobile && ( + <Box + $css={css` + position: fixed; + inset: 0; + z-index: 999; + background-color: rgba(0, 0, 0, 0.3); + transition: opacity 0.2s ease-in-out; + opacity: ${isPanelOpen ? 1 : 0}; + pointer-events: ${isPanelOpen ? 'auto' : 'none'}; + `} + onClick={closePanel} + /> + )} <Box + as="nav" + className={CLASS_NAME_LEFT_PANEL} + data-testid="left-panel" + aria-label={t('Left panel')} + $width="300px" $css={css` - flex: 0 0 auto; + height: 100dvh; + overflow: hidden; + background-color: var(--c--contextuals--background--surface--primary); + border-right: 1px solid + var(--c--contextuals--border--surface--primary); + + ${isMobile + ? css` + transition: transform 0.2s ease-in-out; + position: fixed; + z-index: 1000; + top: 0; + left: 0; + ${!isPanelOpen + ? css` + transform: translateX(-100%); + ` + : ''} + ` + : ''} `} > - <LeftPanelHeader /> + <Box + $css={css` + flex: 0 0 auto; + `} + > + <LeftPanelHeader /> + </Box> + <LeftPanelContent /> + <LeftPanelFooter /> </Box> - <LeftPanelContent /> - <LeftPanelFooter /> - </Box> + </> ); }; From b092d7fcfcb038fda59388d8008087a8cbf7d537 Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Fri, 26 Jun 2026 10:47:07 +0200 Subject: [PATCH 7/9] save --- .../components/ResizableLeftPanel.tsx | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx index dc7b2c1d61..bc91b91b55 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx @@ -6,11 +6,18 @@ import { PanelGroup, PanelResizeHandle, } from 'react-resizable-panels'; - -import { useResponsiveStore } from '@/stores'; +import { createGlobalStyle } from 'styled-components'; import { useLeftPanelStore } from '../stores'; +import { CLASS_NAME_LEFT_PANEL } from './LeftPanel'; + +export const ResizableLeftPanelStyle = createGlobalStyle` + .${CLASS_NAME_LEFT_PANEL} { + width: 100%; + } +`; + // Convert a target pixel width to a percentage of the current viewport width. const pxToPercent = (px: number) => { return (px / window.innerWidth) * 100; @@ -51,7 +58,6 @@ export const ResizableLeftPanel = ({ maxPanelSizePx = 450, }: ResizableLeftPanelProps) => { const { t } = useTranslation(); - const { isLargeScreen } = useResponsiveStore(); const { isPanelOpen } = useLeftPanelStore(); const ref = useRef<ImperativePanelHandle>(null); const savedWidthPxRef = useRef<number>(minPanelSizePx); @@ -89,7 +95,7 @@ export const ResizableLeftPanel = ({ * to either expand/collapse */ useEffect(() => { - if (!ref.current || !isLargeScreen) { + if (!ref.current) { return; } if (isPanelOpen) { @@ -97,14 +103,10 @@ export const ResizableLeftPanel = ({ } else { ref.current.collapse(); } - }, [isPanelOpen, isLargeScreen]); + }, [isPanelOpen]); // Keep pixel width constant on window resize useEffect(() => { - if (!isLargeScreen) { - return; - } - const handleResize = () => { const newPercent = pxToPercent(savedWidthPxRef.current); setPanelSizePercent(newPercent); @@ -117,7 +119,7 @@ export const ResizableLeftPanel = ({ return () => { window.removeEventListener('resize', handleResize); }; - }, [isLargeScreen]); + }, []); /** * Workaround: NVDA does not enter focus mode for role="separator" @@ -145,6 +147,7 @@ export const ResizableLeftPanel = ({ return ( <PanelGroup direction="horizontal" keyboardResizeBy={1}> + <ResizableLeftPanelStyle /> <Panel ref={ref} className="--docs--resizable-left-panel" @@ -158,16 +161,12 @@ export const ResizableLeftPanel = ({ : 'flex var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out)', }} order={0} - defaultSize={ - isLargeScreen - ? Math.max( - minPanelSizePercent, - Math.min(panelSizePercent, maxPanelSizePercent), - ) - : 0 - } - minSize={isLargeScreen ? minPanelSizePercent : 0} - maxSize={isLargeScreen ? maxPanelSizePercent : 0} + defaultSize={Math.max( + minPanelSizePercent, + Math.min(panelSizePercent, maxPanelSizePercent), + )} + minSize={minPanelSizePercent} + maxSize={maxPanelSizePercent} onResize={handleResize} > {leftPanel} @@ -194,7 +193,6 @@ export const ResizableLeftPanel = ({ cursor: 'col-resize', }} onDragging={setIsDragging} - disabled={!isLargeScreen} /> )} <Panel order={1}>{children}</Panel> From 982a598e03b5946e30c1a3359e04651ce8816967 Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Fri, 26 Jun 2026 11:18:49 +0200 Subject: [PATCH 8/9] save --- .../left-panel/components/LeftPanel.tsx | 17 ++++++++++------- .../components/ResizableLeftPanel.tsx | 14 ++------------ .../apps/impress/src/layouts/MainLayout.tsx | 8 ++++++-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index f0409723e5..6fad84fa8a 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -10,9 +10,7 @@ import { LeftPanelContent } from './LeftPanelContent'; import { LeftPanelFooter } from './LeftPanelFooter'; import { LeftPanelHeader } from './LeftPanelHeader'; -export const CLASS_NAME_LEFT_PANEL = '--docs--left-panel'; - -export const LeftPanel = () => { +export const LeftPanel = ({ isResizable }: { isResizable?: boolean }) => { const { t } = useTranslation(); const { isMobile } = useResponsiveStore(); const { isPanelOpen, closePanel } = useLeftPanelStore(); @@ -35,16 +33,21 @@ export const LeftPanel = () => { )} <Box as="nav" - className={CLASS_NAME_LEFT_PANEL} + className="--docs--left-panel" data-testid="left-panel" aria-label={t('Left panel')} - $width="300px" + $width={isResizable ? '100%' : '300px'} $css={css` height: 100dvh; overflow: hidden; background-color: var(--c--contextuals--background--surface--primary); - border-right: 1px solid - var(--c--contextuals--border--surface--primary); + + ${!isResizable + ? css` + border-right: 1px solid + var(--c--contextuals--border--surface--primary); + ` + : ''} ${isMobile ? css` diff --git a/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx index bc91b91b55..189d7eb1a6 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx @@ -6,17 +6,10 @@ import { PanelGroup, PanelResizeHandle, } from 'react-resizable-panels'; -import { createGlobalStyle } from 'styled-components'; import { useLeftPanelStore } from '../stores'; -import { CLASS_NAME_LEFT_PANEL } from './LeftPanel'; - -export const ResizableLeftPanelStyle = createGlobalStyle` - .${CLASS_NAME_LEFT_PANEL} { - width: 100%; - } -`; +import { LeftPanel } from './LeftPanel'; // Convert a target pixel width to a percentage of the current viewport width. const pxToPercent = (px: number) => { @@ -45,14 +38,12 @@ const getValueLabel = ( }; type ResizableLeftPanelProps = { - leftPanel: React.ReactNode; children: React.ReactNode; minPanelSizePx?: number; maxPanelSizePx?: number; }; export const ResizableLeftPanel = ({ - leftPanel, children, minPanelSizePx = 300, maxPanelSizePx = 450, @@ -147,7 +138,6 @@ export const ResizableLeftPanel = ({ return ( <PanelGroup direction="horizontal" keyboardResizeBy={1}> - <ResizableLeftPanelStyle /> <Panel ref={ref} className="--docs--resizable-left-panel" @@ -169,7 +159,7 @@ export const ResizableLeftPanel = ({ maxSize={maxPanelSizePercent} onResize={handleResize} > - {leftPanel} + <LeftPanel isResizable /> </Panel> {isPanelOpen && ( <PanelResizeHandle diff --git a/src/frontend/apps/impress/src/layouts/MainLayout.tsx b/src/frontend/apps/impress/src/layouts/MainLayout.tsx index f4748a948f..dff11f472c 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -6,6 +6,7 @@ import { Box, BoxProps } from '@/components'; 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/useResponsiveStore'; import { MAIN_LAYOUT_ID } from './conf'; import { usePanelCoordination } from './usePanelCoordination'; @@ -37,7 +38,9 @@ export function MainLayoutContent({ children, enableResizablePanel, }: PropsWithChildren<MainLayoutContentProps>) { - if (enableResizablePanel) { + const { isMobile } = useResponsiveStore(); + + if (enableResizablePanel && !isMobile) { return <MainResizableLayout>{children}</MainResizableLayout>; } @@ -45,6 +48,7 @@ export function MainLayoutContent({ <> <LeftPanel /> <MainContent>{children}</MainContent> + <RightPanel /> </> ); } @@ -53,7 +57,7 @@ const MainResizableLayout = ({ children }: PropsWithChildren) => { usePanelCoordination(); return ( - <ResizableLeftPanel leftPanel={<LeftPanel />}> + <ResizableLeftPanel> <Box $direction="row" $width="100%" $position="relative"> <MainContent $flex="auto" $padding="0"> {children} From 8d4305d24916a2c9338ca0b1e541e88e60950e0d Mon Sep 17 00:00:00 2001 From: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr> Date: Fri, 26 Jun 2026 12:05:54 +0200 Subject: [PATCH 9/9] save6 --- .../header => }/components/FloatingBar.tsx | 31 +++++++++++++++++-- .../doc-header/components/DocFloatingBar.tsx | 20 +++--------- .../components/DocSearchButtonModal.tsx | 5 +-- .../doc-share/components/DocShareButton.tsx | 17 ++-------- .../header/components/HeaderFloatingBar.tsx | 22 +++++++++++++ .../components/LeftPanelCollapseButton.tsx | 18 +++-------- .../apps/impress/src/pages/docs/index.tsx | 11 ++----- 7 files changed, 67 insertions(+), 57 deletions(-) rename src/frontend/apps/impress/src/{features/header => }/components/FloatingBar.tsx (60%) create mode 100644 src/frontend/apps/impress/src/features/header/components/HeaderFloatingBar.tsx diff --git a/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx b/src/frontend/apps/impress/src/components/FloatingBar.tsx similarity index 60% rename from src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx rename to src/frontend/apps/impress/src/components/FloatingBar.tsx index 9745f330da..906d233bf3 100644 --- a/src/frontend/apps/impress/src/features/header/components/FloatingBar.tsx +++ b/src/frontend/apps/impress/src/components/FloatingBar.tsx @@ -1,7 +1,8 @@ import { PropsWithChildren } from 'react'; import { css } from 'styled-components'; -import { Box } from '@/components'; +import { Box, BoxType } from './Box'; +import { Card } from './Card'; const FLOATING_STYLES = css` position: sticky; @@ -33,7 +34,10 @@ const FLOATING_STYLES = css` } `; -export const FloatingBar = ({ children }: PropsWithChildren) => { +export const FloatingBar = ({ + children, + ...props +}: PropsWithChildren<BoxType>) => { return ( <Box className="--docs--floating-bar" @@ -41,8 +45,31 @@ export const FloatingBar = ({ children }: PropsWithChildren) => { $css={FLOATING_STYLES} $direction="row" $justify="space-between" + {...props} > {children} </Box> ); }; + +export const CardFloatingBar = ({ + children, + ...props +}: PropsWithChildren<BoxType>) => { + return ( + <Card + className="--docs--card-floating-bar" + $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); + `} + {...props} + > + {children} + </Card> + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx index abae4313e8..648caefed2 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx @@ -1,9 +1,7 @@ -import { css } from 'styled-components'; - -import { Box, Card } from '@/components'; +import { Box } from '@/components'; +import { CardFloatingBar, FloatingBar } from '@/components/FloatingBar'; import { useDocStore } from '@/docs/doc-management/stores/useDocStore'; import { DocShareButton } from '@/features/docs/doc-share/components/DocShareButton'; -import { FloatingBar } from '@/features/header/components/FloatingBar'; import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton'; import { DocLeftPanelCollapseButton } from './DocLeftPanelCollapseButton'; @@ -18,20 +16,10 @@ export const DocFloatingBar = () => { <DocLeftPanelCollapseButton /> <Box $direction="row" $align="center" $gap="2xs"> {!isDeletedDoc && currentDoc && <DocShareButton doc={currentDoc} />} - <Card - className="--docs--floating-bar-toolbox" - $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); - `} - > + <CardFloatingBar> <RightPanelCollapseButton /> {!isDeletedDoc && currentDoc && <DocToolBox doc={currentDoc} />} - </Card> + </CardFloatingBar> </Box> </FloatingBar> ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx index 6fcb612e6e..becb127b27 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchButtonModal.tsx @@ -1,4 +1,4 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; +import { Button, ButtonProps } from '@gouvfr-lasuite/cunningham-react'; import { t } from 'i18next'; import dynamic from 'next/dynamic'; import { useCallback, useState } from 'react'; @@ -16,7 +16,7 @@ const DocSearchModal = dynamic( { ssr: false }, ); -export const DocSearchButtonModal = () => { +export const DocSearchButtonModal = ({ ...props }: ButtonProps) => { const { currentDoc } = useDocStore(); const { authenticated } = useAuth(); const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); @@ -52,6 +52,7 @@ export const DocSearchButtonModal = () => { variant="tertiary" aria-label={t('Search docs')} icon={<SearchSVG aria-hidden="true" width={24} height={24} />} + {...props} /> {isSearchModalOpen && ( <DocSearchModal diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx index 078db9dc8c..bdba2a761a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx @@ -2,10 +2,9 @@ import { Button, useModal } from '@gouvfr-lasuite/cunningham-react'; import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; import dynamic from 'next/dynamic'; import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; import SharedSVG from '@/assets/icons/ui-kit/shared.svg'; -import { Card } from '@/components/Card'; +import { CardFloatingBar } from '@/components/FloatingBar'; import { Doc } from '@/docs/doc-management/types'; import { useAuth } from '@/features/auth'; import { useFocusStore } from '@/stores/useFocusStore'; @@ -54,17 +53,7 @@ export const DocShareButton = ({ return ( <> - <Card - className="--docs--card--share" - $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); - `} - > + <CardFloatingBar className="--docs--card--share"> <Button color={hasAccesses ? 'brand' : 'neutral'} size="small" @@ -84,7 +73,7 @@ export const DocShareButton = ({ > {hasAccesses ? t('Shared') : t('Share')} </Button> - </Card> + </CardFloatingBar> {modalShare.isOpen && ( <DocShareModal onClose={() => { diff --git a/src/frontend/apps/impress/src/features/header/components/HeaderFloatingBar.tsx b/src/frontend/apps/impress/src/features/header/components/HeaderFloatingBar.tsx new file mode 100644 index 0000000000..a4876b312f --- /dev/null +++ b/src/frontend/apps/impress/src/features/header/components/HeaderFloatingBar.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from 'react-i18next'; + +import { CardFloatingBar, FloatingBar } from '@/components/FloatingBar'; +import { DocSearchButtonModal } from '@/features/docs/doc-search/components/DocSearchButtonModal'; +import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPanelCollapseButton'; +import { useResponsiveStore } from '@/stores/useResponsiveStore'; + +export const HeaderFloatingBar = () => { + const { isMobile } = useResponsiveStore(); + const { t } = useTranslation(); + + return ( + <FloatingBar> + {isMobile && ( + <LeftPanelCollapseButton ariaLabel={t('Toggle left panel')} /> + )} + <CardFloatingBar> + <DocSearchButtonModal size="small" color="neutral" /> + </CardFloatingBar> + </FloatingBar> + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx index 52ff670345..c0df08fe58 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelCollapseButton.tsx @@ -1,7 +1,7 @@ import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { css } from 'styled-components'; -import { Card, Text } from '@/components'; +import { Text } from '@/components'; +import { CardFloatingBar } from '@/components/FloatingBar'; import LeftPanelIcon from '../assets/left-panel.svg'; import { useLeftPanelStore } from '../stores'; @@ -16,17 +16,7 @@ export const LeftPanelCollapseButton = ({ const { isPanelOpen, togglePanel } = useLeftPanelStore(); return ( - <Card - className="--docs--left-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); - `} - > + <CardFloatingBar className="--docs--left-panel-collapse-button"> <Button size="small" onClick={() => togglePanel()} @@ -46,6 +36,6 @@ export const LeftPanelCollapseButton = ({ {buttonTitle} </Text> )} - </Card> + </CardFloatingBar> ); }; diff --git a/src/frontend/apps/impress/src/pages/docs/index.tsx b/src/frontend/apps/impress/src/pages/docs/index.tsx index 9dc1dc9f08..a8afdc29db 100644 --- a/src/frontend/apps/impress/src/pages/docs/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/index.tsx @@ -5,10 +5,8 @@ import { useTranslation } from 'react-i18next'; import { DocDefaultFilter, useTrans } from '@/docs/doc-management'; import { DocsGrid } from '@/docs/docs-grid'; -import { FloatingBar } from '@/features/header/components/FloatingBar'; -import { LeftPanelCollapseButton } from '@/features/left-panel/components/LeftPanelCollapseButton'; +import { HeaderFloatingBar } from '@/features/header/components/HeaderFloatingBar'; import { MainLayout } from '@/layouts'; -import { useResponsiveStore } from '@/stores/useResponsiveStore'; import { NextPageWithLayout } from '@/types/next'; const Page: NextPageWithLayout = () => { @@ -19,7 +17,6 @@ const Page: NextPageWithLayout = () => { (searchParams.get('target') as DocDefaultFilter) ?? DocDefaultFilter.ALL_DOCS; const pageTitle = transFilter(target); - const { isMobile } = useResponsiveStore(); return ( <> @@ -31,11 +28,7 @@ const Page: NextPageWithLayout = () => { key="title" /> </Head> - <FloatingBar> - {isMobile && ( - <LeftPanelCollapseButton ariaLabel={t('Toggle left panel')} /> - )} - </FloatingBar> + <HeaderFloatingBar /> <DocsGrid target={target} /> </> );