La navigation latérale
Trois défauts, une refonte
La barre latérale servait une liste de liens. Trois choses l'empêchaient de servir une vraie application de gestion.
Réduite, elle enfermait ses sous-entrées. Une seule règle en était responsable :
.sia-sidebar--collapsed .sia-sidebar__panel {
grid-template-rows: 0fr;
}Le panneau des enfants était forcé à une hauteur nulle quel que soit son état d'ouverture. Une rubrique à sous-menus devenait donc inatteignable dès qu'on repliait la barre : le rail ne servait plus qu'aux entrées sans enfant.
L'arbre s'arrêtait à deux niveaux, et c'était le type lui-même qui l'interdisait :
items?: Array<Omit<SidebarItem, "items">>;Rien ne filtrait selon les droits. Chaque application réécrivait son propre tri avant de passer les entrées.
Ce que la barre fait maintenant
Le rail n'a pas la largeur de déplier quoi que ce soit : ses sous-menus sortent en volet, hors du flux. À l'intérieur du volet, on est de nouveau dans une liste normale — les sous-sous-menus s'y déplient en ligne, à toute profondeur.
Les droits
permission sur une entrée, can sur le composant :
const acces = createAccessLayer({ granted: () => session.permissions });
<Sidebar items={NAVIGATION} can={acces.can} />;Le filtrage est en cascade : un parent dont toute la descendance est filtrée disparaît avec elle. Une rubrique « Facturation » qui n'ouvre sur rien est pire qu'une rubrique absente — elle promet une page qui n'existe pas pour cette session. keepWhenEmpty: true pour un parent qui est aussi une destination à part entière.
Ceci masque une entrée de menu; ça ne protège pas la route. Un écran caché reste atteignable tant que le serveur ne refuse pas l'appel.
Le filtrage est fait une fois, en haut du composant. Chaque nœud recevrait sinon la règle à réévaluer, et l'arbre serait parcouru autant de fois qu'il a de branches.
Le comportement vit dans headless
filterNavTree, navPath, navContains, navLeaves, navDepth, navSome : rien là-dedans ne connaît le DOM. Une barre latérale web et une barre d'onglets native posent exactement les mêmes questions à ces données — navLeaves existe précisément pour la seconde, qui n'a pas la place de déplier.
Les fonctions sont génériques : elles ne lisent que key, items et les deux champs de filtrage, et rendent les entrées de l'appelant intactes, avec leur libellé, leur icône et leur lien.
Brancher un routeur sans en dépendre
<Sidebar
items={NAVIGATION}
renderLink={({ item, children, props }) => (
<Link to={item.href!} {...props}>
{children}
</Link>
)}
/>Les attributs sont regroupés dans props plutôt qu'éparpillés dans les arguments : aria-current en fait partie, et passé à côté il se perdait sans bruit — l'entrée courante n'était plus annoncée aux lecteurs d'écran.
Sans renderLink, un <a href> ordinaire est rendu : la navigation s'affiche hors de tout contexte de routage, en story comme en test.
À stabiliser avec useCallback : les nœuds sont mémoïsés, et une fonction recréée à chaque rendu annule cette mémoïsation.
Ce qui a été factorisé au passage
Deux choses vivaient au mauvais endroit, et la refonte les a déplacées plutôt que de les recopier.
Overlay recalculait sa propre géométrie, en vertical seulement, alors que computeFloatingPosition existe dans @sia-ui/headless et sait basculer et recadrer dans les deux axes. Il n'exposait donc que quatre placements sur douze — et un volet latéral était impossible. Il délègue désormais, ce qui débloque les côtés pour tous les flottants : Select, Popover, DropdownMenu, Tooltip, HoverCard.
L'intention au survol — les deux délais qui empêchent un panneau de clignoter quand la souris traverse — vivait dans HoverCard. Elle est passée dans useHoverIntent, que la barre et la fiche partagent : deux réglages qui divergent donnent deux sensations différentes dans la même application.
Un défaut de mise en page corrigé
Les sous-entrées s'indentaient en margin-left alors qu'elles sont en width: 100%. La marge s'ajoute à la largeur au lieu de s'y prendre : la ligne débordait de son conteneur d'exactement son indentation, et la pastille de droite se retrouvait coupée. Mesuré dans le navigateur : dix puis vingt pixels hors du volet, selon la profondeur.
L'indentation se fait maintenant en padding-left. La zone cliquable y gagne aussi : elle va jusqu'au bord.
La hiérarchie repose uniquement sur cette indentation. Le filet vertical continu a été retiré : sur les longues listes, il ressemblait à une barre collée aux entrées et alourdissait particulièrement le tiroir mobile. La barre de défilement conserve de l'espace avec le contenu et utilise un curseur fin, arrondi et adapté aux couleurs du thème global de SIA UI.
Variantes
| Prop | Valeurs | Effet |
|---|---|---|
variant | default, filled, floating | le fond et le cadre |
size | sm, md, lg | la densité |
tone | tons sémantiques | la couleur de l'entrée courante |
side | left, right | de quel côté sortent les volets |
collapsed | booléen | le rail |
accordion | booléen | une seule branche ouverte par niveau |
openKeys, defaultOpenKeys et onOpenKeysChange pilotent les branches ouvertes. Sans elles, le composant les gère seul — et la branche de activeKey s'ouvre d'elle-même : arriver par un lien profond, un rechargement ou le bouton retour doit montrer où l'on est. On n'ajoute jamais de fermeture automatique, qui annulerait un dépliage volontaire à chaque navigation.
Prévu
AppShell consomme cette barre plutôt que d'en redessiner une, et navLeaves alimente sa variante mobile à onglets.