Supprimer l'ascenseur des onglets, et rendre les réglages à un rail

Les onglets de section défilaient horizontalement dès que la liste dépassait la
largeur. Une barre de défilement cache la moitié des entrées derrière un geste
que rien n'annonce : on ne sait pas ce qu'on ne voit pas. Ils passent à la ligne
— deux rangées coûtent seize pixels et montrent tout.

Les réglages sortent des onglets pour un rail vertical, et eux seuls. Ce n'est
pas un retour de la barre latérale : partout ailleurs la section tient en
quelques entrées et le contenu garde toute la largeur. Les réglages en comptent
une vingtaine ; en onglets, ils occuperaient trois rangées au-dessus de chaque
écran et changeraient de forme d'une page à l'autre selon la longueur des
libellés.

Le rail les groupe dans l'ordre d'un paramétrage — société, organisation,
travail, paie, accès, conformité — et non par ordre alphabétique : quelqu'un qui
installe l'instance descend la liste. Une entrée ajoutée sans groupe reste
visible en fin de rail plutôt que de disparaître.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-11 15:50:38 +02:00
1 parent 9356c6e3f4
commit dd01fb58f4
3 files changed
+138 -5

No files matched your search

+27 -4
View File
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
import type { ReactNode } from 'react';
import { SectionTabs } from '@/components/shell/SectionTabs';
import { SettingsRail } from '@/components/shell/SettingsRail';
import { ThemeToggle } from '@/components/shell/ThemeToggle';
import { Button } from '@/components/ui/Button';
import { cx } from '@/lib/cx';
@@ -32,6 +33,7 @@ export function AppShell({
}: AppShellProps) {
const pathname = usePathname();
const section = sectionForPath(pathname);
const isSettings = section.id === 'reglages';
return (
<div className="flex min-h-dvh flex-col bg-canvas text-ink-1">
@@ -127,11 +129,32 @@ export function AppShell({
*/}
<main className="min-h-0 flex-1 overflow-auto">
<div className="mx-auto flex min-h-full w-[1280px] flex-col">
<div className="px-6 pt-5">
<SectionTabs section={section} pathname={pathname} />
</div>
{/*
Deux navigations de section, pour deux usages.
{children}
Partout, des onglets : la section tient en quelques entrées et le
contenu garde toute la largeur. Aux réglages, un rail vertical : une
vingtaine d'entrées en onglets prendraient trois rangées au-dessus de
chaque écran, et changeraient de forme d'une page à l'autre selon la
longueur des libellés. On traverse les réglages, on ne fait pas
d'aller-retour entre eux pendant qu'on travaille.
*/}
{isSettings ? (
// Le rail porte la marge gauche ; le contenu garde la sienne, celle
// de `PageBody`. Les additionner ferait dériver la colonne des
// réglages par rapport à toutes les autres pages.
<div className="flex min-h-full flex-1 gap-2 pl-6">
<SettingsRail section={section} pathname={pathname} />
<div className="min-w-0 flex-1">{children}</div>
</div>
) : (
<>
<div className="px-6 pt-6">
<SectionTabs section={section} pathname={pathname} />
</div>
{children}
</>
)}
<p
data-print="hide"
+5 -1
View File
@@ -34,10 +34,14 @@ export function SectionTabs({
if (section.items.length === 0) return null;
return (
// Les onglets **passent à la ligne**, ils ne défilent pas. Une barre de
// défilement horizontale cache la moitié des entrées derrière un geste que
// rien n'annonce : on ne sait pas ce qu'on ne voit pas. Deux rangées
// prennent seize pixels de plus et montrent tout.
<nav
aria-label={`Section ${section.label}`}
data-print="hide"
className="flex gap-1 overflow-x-auto rounded-3 border border-line-1 bg-surface p-1.5"
className="flex flex-wrap gap-1 rounded-3 border border-line-1 bg-surface p-1.5"
>
{section.items.map((item) => {
const current = item.href ? isActive(item.href, pathname) : false;
+106
View File
@@ -0,0 +1,106 @@
'use client';
import Link from 'next/link';
import {
isActive,
placeholderHref,
type NavSection,
} from '@/components/shell/navigation';
import { cx } from '@/lib/cx';
/**
* Rail latéral des réglages — et **seulement** des réglages.
*
* Partout ailleurs, la section tient en une poignée d'entrées et se présente en
* onglets, qui laissent toute la largeur au contenu. Les réglages en comptent
* une vingtaine : en onglets, ils occuperaient trois rangées au-dessus de
* chaque écran, et la liste changerait de forme d'une page à l'autre selon la
* longueur des libellés.
*
* Une liste verticale les tient dans un ordre stable, groupés par domaine. Ce
* n'est pas la même navigation qu'ailleurs parce que ce n'est pas le même
* usage : on traverse les réglages, on ne fait pas d'aller-retour entre eux
* pendant qu'on travaille.
*/
/**
* Regroupement des entrées, dans l'ordre d'un paramétrage.
*
* L'identité de l'entreprise, puis ce qui organise le travail, puis ce qui le
* paie, puis ce qui l'encadre. Un client qui installe l'instance descend la
* liste ; c'est cet ordre-là, pas l'alphabet.
*/
const GROUPS: ReadonlyArray<{ label: string; ids: readonly string[] }> = [
{ label: 'Société', ids: ['compte', 'sites', 'convention'] },
{ label: 'Organisation', ids: ['emplois', 'etiquettes', 'types-absence'] },
{ label: 'Travail', ids: ['preferences', 'impression', 'productivite'] },
{ label: 'Paie', ids: ['paie-reglages'] },
{ label: 'Accès', ids: ['roles', 'securite', 'email'] },
{
label: 'Conformité',
ids: ['registre', 'conservation', 'rgpd', 'modeles'],
},
];
export function SettingsRail({
section,
pathname,
}: {
section: NavSection;
pathname: string;
}) {
const byId = new Map(section.items.map((item) => [item.id, item]));
// Une entrée ajoutée à la navigation sans être rangée dans un groupe reste
// visible, en fin de liste : la perdre serait pire que de mal la classer.
const grouped = new Set(GROUPS.flatMap((group) => group.ids));
const rest = section.items.filter((item) => !grouped.has(item.id));
const groups = [
...GROUPS.map((group) => ({
label: group.label,
items: group.ids
.map((id) => byId.get(id))
.filter((item) => item !== undefined),
})),
...(rest.length > 0 ? [{ label: 'Autres', items: rest }] : []),
].filter((group) => group.items.length > 0);
return (
<nav
aria-label="Réglages"
data-print="hide"
className="flex w-56 flex-none flex-col gap-5 pt-6 pb-4"
>
{groups.map((group) => (
<div key={group.label} className="flex flex-col gap-0.5">
<p className="eyebrow px-2.5 pb-1.5">{group.label}</p>
{group.items.map((item) => {
const current = item.href ? isActive(item.href, pathname) : false;
return (
<Link
key={item.id}
href={item.href ?? placeholderHref(item.label)}
aria-current={current ? 'page' : undefined}
className={cx(
'rounded-2 border-l-2 px-2.5 py-1.5 text-sm',
'transition-colors duration-[var(--d-2)] ease-organic',
current
? 'border-accent bg-accent-soft font-semibold text-accent-soft-ink'
: item.href
? 'border-transparent text-ink-2 hover:bg-surface-2 hover:text-ink-1'
: 'border-transparent text-ink-3 hover:bg-surface-2',
)}
>
{item.label}
</Link>
);
})}
</div>
))}
</nav>
);
}