From 0a916ff73e1edb01ed12a39c417936b013703770 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Tue, 11 Aug 2026 13:31:23 +0200 Subject: [PATCH] =?UTF-8?q?Retirer=20la=20barre=20lat=C3=A9rale=20au=20pro?= =?UTF-8?q?fit=20d'onglets=20de=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux défauts la condamnaient. Elle rognait la largeur utile au moment précis où la grille de planning en manque le plus. Et elle donnait à la navigation deux grammaires : des onglets sur la fiche salarié, une liste verticale partout ailleurs. Une grammaire se retient, deux se cherchent. Les entrées de la section courante s'affichent donc en onglets, dans la même forme que la fiche salarié. Une entrée sans écran construit reste visible et mène à l'écran d'attente : la masquer ferait croire le produit plus étroit que ce qu'il vise, la laisser inerte ferait croire la navigation cassée. La colonne centrale passe en largeur fixe. Une largeur qui suit la fenêtre fait bouger les colonnes du planning d'un poste à l'autre : la même semaine ne se lit pas au même endroit sur deux écrans, et l'œil doit se réorienter à chaque ouverture. Une largeur arrêtée rend la page reconnaissable ; sur une fenêtre étroite, elle défile horizontalement plutôt que de comprimer la grille. À l'impression, cette largeur n'a plus de sens et déborderait de la feuille : elle y est relâchée, comme les autres largeurs minimales de la grille. Co-Authored-By: Claude Opus 5 --- src/app/globals.css | 7 +++ src/components/shell/AppShell.tsx | 86 ++++++++-------------------- src/components/shell/SectionTabs.tsx | 71 +++++++++++++++++++++++ tests/e2e/apercu.spec.ts | 2 +- tests/e2e/navigation.spec.ts | 3 +- tests/e2e/reglages.spec.ts | 2 +- 6 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 src/components/shell/SectionTabs.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 0bf23b1..35bc503 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -411,6 +411,13 @@ body { min-width: 0 !important; } + /* La colonne centrale est de largeur fixe à l'écran — c'est ce qui rend la + grille reconnaissable d'un poste à l'autre. Sur une feuille, cette largeur + n'a plus de sens et déborderait de la page. */ + .w-\[1280px\] { + width: auto !important; + } + [data-print='hide'], nav, aside { diff --git a/src/components/shell/AppShell.tsx b/src/components/shell/AppShell.tsx index f247234..2e4245b 100644 --- a/src/components/shell/AppShell.tsx +++ b/src/components/shell/AppShell.tsx @@ -4,18 +4,15 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import type { ReactNode } from 'react'; +import { SectionTabs } from '@/components/shell/SectionTabs'; import { ThemeToggle } from '@/components/shell/ThemeToggle'; -import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; import { cx } from '@/lib/cx'; import { signOutAction } from '@/server/auth/actions'; import { - isActive, NAVIGATION, - placeholderHref, sectionForPath, sectionHref, - type NavItem, } from '@/components/shell/navigation'; export interface AppShellProps { @@ -91,64 +88,31 @@ export function AppShell({ -
- + {/* + Colonne centrale de **largeur fixe**, pas fluide. -
{children}
-
+ Une largeur qui suit la fenêtre fait bouger les colonnes du planning + d'un poste à l'autre : la même semaine ne se lit pas au même endroit sur + deux écrans, et l'œil doit se réorienter à chaque ouverture. Une largeur + arrêtée rend la page reconnaissable, et laisse la fenêtre défiler + horizontalement plutôt que de comprimer la grille. + */} +
+
+
+ +
+ + {children} + +

+ Instance auto-hébergée · {accountName} +

+
+
); } - -function SidebarLink({ item, pathname }: { item: NavItem; pathname: string }) { - const current = item.href ? isActive(item.href, pathname) : false; - const className = cx( - 'flex w-full items-center justify-between gap-2 rounded-2 px-2.5 py-1.5 text-left text-sm', - 'transition-colors duration-[var(--d-1)] ease-organic', - current - ? 'bg-accent-soft font-semibold text-accent-soft-ink' - : 'text-ink-2 hover:bg-surface-2', - ); - - const content = ( - <> - {item.label} - {item.badge ? ( - - {item.badge} - - ) : null} - - ); - - if (!item.href) { - return ( - - {content} - - ); - } - - return ( - - {content} - - ); -} diff --git a/src/components/shell/SectionTabs.tsx b/src/components/shell/SectionTabs.tsx new file mode 100644 index 0000000..04371e5 --- /dev/null +++ b/src/components/shell/SectionTabs.tsx @@ -0,0 +1,71 @@ +'use client'; + +import Link from 'next/link'; + +import { Badge } from '@/components/ui/Badge'; +import { + isActive, + placeholderHref, + type NavSection, +} from '@/components/shell/navigation'; +import { cx } from '@/lib/cx'; + +/** + * Entrées de la section courante, en onglets. + * + * Elles vivaient dans une barre latérale. Deux raisons de l'avoir retirée : + * elle rognait la largeur utile au moment précis où la grille de planning en + * manque, et elle donnait à la navigation deux grammaires — des onglets sur la + * fiche salarié, une liste verticale ailleurs. Une seule grammaire se retient ; + * deux se cherchent. + * + * Une entrée sans `href` désigne un écran du périmètre qui n'est pas encore + * construit. Elle reste affichée et mène à un écran d'attente qui le dit : la + * masquer ferait croire le produit plus étroit qu'il n'est visé, et la laisser + * inerte ferait croire la navigation cassée. + */ +export function SectionTabs({ + section, + pathname, +}: { + section: NavSection; + pathname: string; +}) { + if (section.items.length === 0) return null; + + return ( + + ); +} diff --git a/tests/e2e/apercu.spec.ts b/tests/e2e/apercu.spec.ts index f76dc25..0fbf1eb 100644 --- a/tests/e2e/apercu.spec.ts +++ b/tests/e2e/apercu.spec.ts @@ -12,7 +12,7 @@ test('chaque tuile mène à ses lignes sources', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Aperçu RH' })).toBeVisible(); // Les tuiles sont des liens, pas des chiffres décoratifs. On vise celle du - // contenu, pas l'entrée homonyme de la barre latérale. + // contenu, pas l'onglet de section homonyme. const tile = page .locator('main') .getByRole('link', { name: /Profils incomplets/ }) diff --git a/tests/e2e/navigation.spec.ts b/tests/e2e/navigation.spec.ts index 166cdf8..abe3d33 100644 --- a/tests/e2e/navigation.spec.ts +++ b/tests/e2e/navigation.spec.ts @@ -51,7 +51,8 @@ test('le thème bascule et survit à un rechargement', async ({ page }) => { test('une entrée de navigation non construite mène à un écran explicite', async ({ page, }) => { - // La barre latérale n'affiche que la section courante : on l'ouvre d'abord. + // Les onglets n'affichent que les entrées de la section courante : on + // l'ouvre d'abord. await page.goto('/absences/calendrier'); // « Politiques de congés » fait partie du périmètre visé mais n'est pas // encore construit : l'entrée reste, et mène à un écran qui le dit. diff --git a/tests/e2e/reglages.spec.ts b/tests/e2e/reglages.spec.ts index 032b928..c93b6b2 100644 --- a/tests/e2e/reglages.spec.ts +++ b/tests/e2e/reglages.spec.ts @@ -23,7 +23,7 @@ test('un manager ne peut ni voir ni modifier les établissements', async ({ }) => { await signIn(page, 'manager.nantes@example.test'); - // La barre latérale ne propose pas la section, mais c'est un confort : + // Les onglets ne proposent pas la section, mais c'est un confort : // le contrôle qui compte est celui du serveur, testé en accédant à l'URL. await page.goto('/reglages/etablissements');