From 75e1fe32a10d396b820779a8f60706717821ebe2 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 23 Jul 2026 01:14:57 +0200 Subject: [PATCH] =?UTF-8?q?uix(sidebar):=20review=20adversarial=20=C2=B7?= =?UTF-8?q?=2031=20hallazgos=20confirmados,=20arreglados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review de 6 dimensiones × 3 verificadores escépticos (135 agentes): 43 hallazgos brutos → 31 confirmados, 12 rechazados. Todos los confirmados arreglados y verificados en navegador real. Estado (los dos ejes) - `collapsible='none'` ya no deja el sidebar INALCANZABLE en móvil: la inercia del modo se limita al escritorio, que es donde «nunca colapsa» significa algo. En móvil el panel es un Drawer que arranca cerrado, el raíl está oculto y el drawer no tiene trigger propio: con el toggle inerte no había forma humana de abrir la navegación en un teléfono. - `'none'` PINTA el estado a expandido: antes `data-state`, `aria-expanded` y `useSidebar().open` podían decir «colapsado» sobre un panel plenamente visible, y ninguna regla del recipe ni el toggle podían reconciliarlo. - El estado del drawer móvil se DESCARTA al salir de la presentación móvil: abrirlo en un teléfono y rotar dos veces remontaba el Drawer ya abierto, con overlay, foco atrapado y scroll bloqueado sin tocar nada. Submenú flotante (era el nudo con más hallazgos) - Cierra también por PUNTERO: se abría en `pointerenter` de la fila, pero el único cierre por puntero vivía en el propio flyout — salir hacia la página sin cruzarlo lo dejaba pintado para siempre. - Escape funciona desde donde el foco ESTÁ (la fila), no solo dentro del sub. - Entrar/salir se resuelve en el `menu-item`, que contiene fila Y sub, así que viajar entre ambos no lo cierra. - El flag se limpia al cambiar de presentación: expandir y volver a colapsar reabría un flyout que nadie había tocado. - El motor flotante se engancha solo cuando el sub está ABIERTO, no por modo: antes `autoUpdate` (rAF + observers) corría por cada fila del raíl. - `hasSub` deja de latir a true: se limpia en el teardown del sub. A11y - Las filas del raíl recuperan NOMBRE: un tooltip solo describe (`aria-describedby`), así que con la etiqueta oculta el texto de `tooltip` pasa también a `aria-label` — incluidas las filas con submenú, que no llevan tooltip visible. - El panel off-canvas colapsado sale del tab order y del árbol de accesibilidad (`visibility: hidden` con la transición retrasada para que el deslizamiento siga animando). - El diálogo móvil recibe el mismo nombre que el landmark. - El raíl se llama «Contraer barra lateral» (lo que hace) en vez de «Redimensionar», que prometía un arrastre fuera de alcance; cursor de puntero en vez de `ew-resize`. Recipe - El signo del off-canvas se deriva de side × dirección: en RTL la fila flex se invierte, así que el negativo fijo barría el panel POR ENCIMA de la página en vez de sacarlo por su borde. - El flyout declara banda `z-index` (nuevo token `--sidebar-sub-z`); era la única superficie flotante del ecosistema sin ella. - La transición de `inline-size` (layout por frame) queda SOLO bajo el modo icono; la base anima únicamente `transform`. - El `Drawer` es el único dueño del ancho móvil (dos dueños dejaban una banda de cromo en dos tonos) y se compone con `dragToDismiss`. - Las reglas de modo icono ya no alcanzan el subárbol flotante: sus filas conservan badges y acciones. - Bloque `prefers-reduced-motion` — la traslación del panel es la mayor del sistema. Demo - Copias corregidas donde afirmaban de más: el tooltip ahora sí NOMBRA (y se explica por qué), y los anchos son rem fijos a propósito (no los alcanza el escalado; sí el interior). - Tabla de eventos y de teclado en la pestaña morfo; `onOpenChange` y una tabla de props por parte en API. - Los chips de estado se inhabilitan en `collapsible='none'`, que es lo que el componente hace; botón «show me» que pone icon + colapsado de una vez. - El trigger pasa a ser solo icono: el morfo le da `aria-label`, que pisaba cualquier texto visible. Verificado en navegador: nombres accesibles en el raíl · flyout que cierra por puntero, por foco y por Escape (con retorno de foco) y que no resucita al cambiar de modo · off-canvas fuera del tab order · RTL sacando el panel por su propio borde · z-index 80 en el flyout · drawer móvil nombrado, a ancho completo y con el panel a ras · `collapsible='none'` abrible en un teléfono. Gates: audit PASS 0E/0W · eidos-lint 47 morfo-backed / 0 invalid · svelte-check 0 errores propios · vitest src/uix/eidos 353/353 · contracts.test solo con los 3 fallos ajenos conocidos. Co-Authored-By: Claude Opus 4.8 --- src/uix/eidos/components/sidebar/README.md | 34 +++- .../sidebar/sidebar-menu-button.svelte | 26 ++- .../components/sidebar/sidebar-panel.svelte | 19 ++- src/uix/eidos/components/sidebar/sidebar.css | 94 ++++++++--- src/uix/eidos/generated/base.css | 1 + src/uix/eidos/lib/recipes/base.ts | 3 + src/uix/langs/components/sidebar.ts | 4 +- src/uix/morfo/components/sidebar.ts | 4 +- src/uix/soma/components/sidebar/README.md | 20 ++- src/uix/soma/components/sidebar/langs.ts | 2 +- .../sidebar/sidebar-provider.svelte.ts | 127 ++++++++++---- .../uix/components/sidebar/+page.svelte | 155 ++++++++++++++++-- 12 files changed, 403 insertions(+), 86 deletions(-) diff --git a/src/uix/eidos/components/sidebar/README.md b/src/uix/eidos/components/sidebar/README.md index 10e16ff4a..432f3a369 100644 --- a/src/uix/eidos/components/sidebar/README.md +++ b/src/uix/eidos/components/sidebar/README.md @@ -46,13 +46,20 @@ real `sidebar.tsx`). Headless behavior: - **`data-collapsible` siempre estampado** (shadcn solo lo pone colapsado, lo que obliga a cada regla del recipe a guardar por estado). - **`rail` alcanzable por teclado**: es un `

+ {#if collapsible === 'none'} +

+ none pins the panel open: the state chips are inert because the component + itself refuses to collapse in this mode, and reporting «collapsed» would make + data-state and aria-expanded lie about a visible panel. +

+ {/if}
open (state)
- - +
@@ -369,8 +406,12 @@

The recipe is logical throughout (border-inline-end, padding-inline), so RTL mirrors the whole shell — including which edge the rail - sits on. Widths are tokens (--sidebar-width, - --sidebar-width-icon), so density and scaling reach them too. + sits on and which way the off-canvas panel slides out (a physical translate sign would sweep + it across the page instead). The three widths (--sidebar-width, + --sidebar-width-icon, --sidebar-width-mobile) are deliberately + FIXED rem: a rail whose width drifted with density would reflow the page every time the user + changed a foundation knob. Density and scaling do reach the interior — row height, paddings, + gaps and the rail's own thickness all ride the spacing scale.

@@ -412,7 +453,7 @@ '#?components.sidebar.label|Sidebar' )}», «{uix.langs.ts('#?components.sidebar.nav|Main navigation')}», «{uix.langs.ts( '#?components.sidebar.trigger|Toggle sidebar' - )}», «{uix.langs.ts('#?components.sidebar.rail|Resize sidebar')}» — so a rail with no visible + )}», «{uix.langs.ts('#?components.sidebar.rail|Collapse sidebar')}» — so a rail with no visible text is still announced in the reader's language. adom supplies the breakpoint tracker that decides data-mobile (uix.dom.isAtLeast) and the floating engine that anchors a sub-menu to its row in icon mode. sema @@ -460,6 +501,52 @@ Resolved by the system tracker, never a local matchMedia. + + onOpenChange + (open: boolean) => void + + Fires for the DESKTOP state — the layout preference an app persists. The mobile + drawer's open/closed is a transient overlay, not a preference, so it stays quiet. + + + + +
+
Part props
+
+ + + + + + + + + + + + + + + + + + + + + + +
PartPropNotes
Sidebar.Panelaria-label + Name of the complementary landmark (and of the mobile dialog). Falls back to the + localized «{uix.langs.ts('#?components.sidebar.label|Sidebar')}». +
Sidebar.Contentaria-label + Name of the <nav> landmark. Falls back to «{uix.langs.ts( + '#?components.sidebar.nav|Main navigation' + )}». +
Sidebar.MenuActionaria-labelRequired in practice: the action is a glyph with no text of its own.
every partid · child + Explicit id (ids are generated otherwise) and the asChild snippet, which receives the + part's resolved props. +
@@ -490,8 +577,9 @@ tooltip string - Name shown in a composed Tooltip while the panel is an icon rail (where - the label is hidden). Eidos-only. + The row's name while the panel is an icon rail (where the label is hidden): it + becomes the aria-label AND, unless the row owns a sub-menu, the text of + a composed Tooltip. Eidos-only. @@ -539,6 +627,40 @@
+
Events
+
+ + + + {#each sidebarMorfo.events as event (event.name)} + + + + + + + {/each} + +
eventfamily · verbtargetsequence
{event.name}{event.semantic.family} · {event.semantic.verb}panel{event.semantic.sequence}
+
+
Keyboard
+
+ + + + + + + + + +
partkeyaction
menu-subEscape + close-sub — dismisses the floating sub-menu and returns focus to its + row. Handled at the menu-item, which contains both the row and the sub: + focus normally sits on the ROW, and the runtime's shared action map cannot tell which + sub fired. +
+

separator and the search input are deliberately NOT parts: they compose the canonical Separator and Field. Two partRefs @@ -630,9 +752,14 @@ Icon mode - The label is hidden visually, so the row's name comes from the composed - Tooltip (tooltip prop). Sub-menus float beside the rail - instead of disappearing, and Escape closes one returning focus to its row. + The label is hidden visually, so the tooltip text becomes the row's + aria-label — a tooltip only describes + (aria-describedby), it never names, so relying on it alone would leave + the control anonymous. The visible tooltip is the sighted half of the same fact, and + it is suppressed on a row that owns a sub-menu (the flyout opens on the same hover; + the name still comes through the label). Sub-menus float beside the rail instead of + disappearing; they close on pointer-leave, on focus leaving the row, and on Escape, + which returns focus to the row.