You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-sidebar.md

430 lines
33 KiB

uix(sidebar): la fila que navega decía «te sentí» a nadie, y el libro dice cómo se dice «Navegar una fila es nativo y no suena» era una decisión de la fase 1 tomada en silencio (el plan la dejó como «decidir en fase 1») sobre una premisa falsa — «item = Link puro»—: la fila es `<button>` cuando no tiene `href`, la acción es un botón pelado y el `MenuItem` posee un flyout. Medido en navegador: pulsar fila, sub-fila o acción estampaba CERO; el Trigger sí acusaba recibo, porque compone `Button`. El libro no deja margen: cap. 22 §9 compone el enlace como «contact.press seguido de shift.navigate: la presión no es la navegación», cap. 27 §8 lo repite y cap. 22 §12 llama antipatrón al contacto mudo. Así que la fila declara las dos ocurrencias, y la aparición del flyout —cap. 26 §5-§6, un menú anclado— deja de ser un cambio de estado sin evento. Dónde se estampa cada una lo decidió la doctrina, no la comodidad: el gesto en la mano (la fila, anclado por instancia: es parte REPETIDA) y el cruce en la superficie que cruza como unidad (el provider). Eso evita la quinta pareja `queue` que yo iba a declarar: calendar ya movió su `shift-navigate` fuera del botón porque las dos ocurrencias se comían la única ranura y el press-squeeze no llegaba a pintarse. Voz: contact y shift toman la de su familia (`touch`, `slide`) sin escribir nada; los dos `emerge-*-sub` van SILENT como el tooltip — el flyout se abre al pasar el puntero, y un barrido por el raíl serían ocho apariciones sonando (cap. 26 §7, Criterio 12). El `trigger` NO declara contacto: lo trae por composición de `Button`. El `rail` sí, porque es una franja de cromo y nadie más lo estampa. Medido en el navegador tras el cambio: fila con href → `contact-activate` (press-squeeze corriendo) en ESA fila + `shift-navigate` en el shell; fila deshabilitada → nada; sub-fila → el mismo par; rail → contacto propio + `emerge-collapse` del panel; en modo icono, hover → `emerge-open-sub` (present-rise), hover repetido → nada, salir → `emerge-close-sub` (dismiss-fade). Suite nueva de 8 contratos, `check` con delta CERO contra la base medida con stash (72/72), morfo:check PASS, contracts sin fallos nuevos. Plan y decisiones firmadas: docs/process/PLAN-sidebar.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
# PLAN — `Sidebar`: la fila habla lo que es (sema por el libro) y viste la talla del sistema (eje `size`)
> **Estado: PLAN ENTREGADO 2026-08-19 — NADA CONSTRUIDO, NADA FIRMADO.** Falta
> que el autor firme las decisiones D-SB de §4 (una por mensaje, con sus cinco
> preguntas) y ordene el arranque de F1.
>
> **Origen (conversación 2026-08-19)**: el autor pidió analizar por qué
> `Sidebar.MenuButton` / `Sidebar.MenuItem` no emiten semántica y contrastarlo
> con el libro, y añadió: «el componente sidebar está mal construido, la
> tipografía y los tamaños no responden a los del sistema». Su veredicto,
> textual, manda sobre este documento: **«emitir o no emitir no es una
> decisión, ya que el libro es el canon, y la naturaleza de los eventos es la
> que es; por lo tanto tienen que estar acorde al tipo de eventos que emite. Hay
> que corregirlo.»**
>
> **Kickoff para sesión nueva**: _«Lee `docs/process/PLAN-sidebar.md`; si las
> D-SB de §4 están firmadas, ejecuta la fase que toque de §5 con sus gates; si
> no, PARA y preséntalas una a una.»_ Cada fase lista qué leer antes, qué
> producir y con qué guard se verifica.
>
> Fuentes de doctrina que este plan cita y NO copia: el libro
> (`docs/Disenando_lo_que_ocurre_FINAL.pdf`, anclas en `docs/book-map.md`),
> [`docs/CANON.md`](../CANON.md), [`docs/architecture/sema.md`](../architecture/sema.md),
> [`docs/theming/reference.md`](../theming/reference.md) §5,
> [`docs/guides/component-guide.md`](../guides/component-guide.md) §Build contract.
---
## 0. Veredicto corto (la valoración que pidió el autor)
1. **La fila muda contradice el libro, no lo aplica.** Cap. 22 §9 compone el
enlace como «Link → contact.press seguido de shift.navigate: la presión no
es la navegación»; cap. 27 §8 repite «Navegación → contact.press ·
shift.navigate»; cap. 22 §12 nombra el antipatrón «Contacto mudo»; CANON §8
regla 1 hace obligatorio el contact «when there is direct action». La fila
del sidebar es un control de navegación POR DECLARACIÓN (`<a href>` dentro
del landmark `<nav>`, `aria-current="page"` del mismo prop que
`data-active`): sabe que navega, luego declara las dos ocurrencias. Lo que
NO sabe —la orientación del destino: foco, título, landmarks (cap. 27 §9)—
sigue siendo deber del shell/app y no excusa a la fila.
2. **«Nativo y sin cue» era una decisión de fase 1 tomada en silencio**, no una
firma: el plan dejó el punto abierto («activación de item si el item es
más que un `Link` — decidir en fase 1», `PLAN-blocks.md:394-397`), la fase 0
firmó sólo el `emerge` del panel (`PLAN-blocks.md:1659-1664`), y la premisa
«item = Link puro» no se cumple: `MenuButton` es `<button>` sin `href`,
`MenuAction` es un `<button>` pelado, `MenuItem` POSEE un flyout.
3. **El framework ya se contradice a sí mismo** sobre el mismo gesto:
`navigation-menu` emite `commit-select + affirm` al pulsar su `link`
(«Navigating IS the evaluable act... it had no voice»,
`navigation-menu-provider.svelte.ts:715-728`), mientras `link` /
`anchor-nav` / `breadcrumb` / `nav-tree` / `sidebar` callan «por contrato
escrito» (A-50 del ledger de blocks). Por el libro ninguna de las dos es
exacta: «shift no es commit» (cap. 27 §2). Este plan arregla el sidebar;
los demás son deuda transversal (§6) que se firma aparte, sin cascada.
4. **El silencio sólo es respuesta válida como CANAL, nunca como evento sin
declarar**: CANON §7 («where no base exists, silence — a valid answer») y el
pack `SILENT` del tooltip hablan de la VOZ; Apéndice A del libro exige el
evento declarado («el runtime no debe adivinar»). La corrección es declarar
los eventos que la naturaleza de cada parte dicta y dejar que el pack decida
cuánto se oye.
5. **Tallas**: el sidebar se construyó fuera del canon de tamaño (sin `size`,
un token de ESPACIO como altura de control, `sm` clavado, fuente del UA en la
acción). La corrección es mecánica y tiene precedentes en el propio repo
(`breadcrumb` para el eje `data-size`, `dialog/context.ts` para la
derivación container→part).
---
## 1. Inventario de defectos — medidos, con evidencia y canon
Todo lo de abajo está **medido en navegador** (dev server `verify`, ruta
`/uix/components/sidebar`, 2026-08-19): MutationObserver sobre `data-event*`
en todo el documento; control positivo el `Trigger` (compone `Button`), que
estampó `contact-activate` (sig-0) y `emerge-collapse` en el `panel` (sig-1).
### 1.1 Sema — el libro es el canon
| Id | Parte | Hoy (medido) | Canon que incumple | Evidencia en código |
|---|---|---|---|---|
| **S-1** | `menu-button` (`<a href>` — la fila que navega) | clic → **0 estampas**, 0 sonido | Cap. 22 §9 «Link → contact.press · shift.navigate»; cap. 27 §5/§8; cap. 22 §12 «Contacto mudo»; CANON §8 r.1; cap. 36 §8 («un botón puede participar en… shift.navigate») | morfo `sidebar.ts:48-52` («Navigating a menu item is native and unsonified»), `:319-342` (0 eventos, sin archetype); provider `sidebar-provider.svelte.ts:584-589` (props sin `onclick`); pack `sema/components/sidebar.ts:16-18` |
| **S-1b** | `menu-button` (`<button>` — sin `href`, «only opens a sub-menu») | clic → 0 estampas y **0 comportamiento** | Cap. 22 §9 «Botón → contact.press»; A-62 del ledger («el contacto inicia y nada resuelve» — aquí ni inicia) | `sidebar-menu-button.svelte:48-51` |
| **S-2** | `menu-sub-button` | clic → 0 estampas | = S-1 («Same active contract as MenuButton», `sidebar.ts:399`) | `sidebar-provider.svelte.ts:732-735` |
| **S-3** | `menu-action` (`<button archetype=trigger>`) | clic → 0 estampas; **fuente 13,33px / `line-height: normal` del UA** | Cap. 22 §9 «Botón → contact.press»; Build contract «Composition» (componer `Button`, no reimplementar) | morfo `sidebar.ts:347-363`; eidos `sidebar-menu-action.svelte:3-6` («Native button… not the `Button` component») |
| **S-4** | `rail` (`<button archetype=trigger>`) | clic → `emerge-*` en `panel`, **nada en el propio rail** (el `Trigger` sí acusa recibo porque compone `Button`) | Cap. 22 §9; CANON §8 r.1 — el mismo toggle, dos voces distintas según el botón que se pulse | `sidebar-provider.svelte.ts:267-273`; eidos `sidebar-rail.svelte:3-6` |
| **S-5** | `menu-item` — dueño del **flyout** de modo icono (`openSub`/`closeSub`) | hover/foco → el sub-menú flotante aparece y desaparece con **0 eventos** | Cap. 26 §5 (`emerge.open/close`: «algo se abre o se cierra con marco propio: menú, popover, panel»), §6 «aparición anclada — dropdown»; precedente `dropdown-menu` `emerge-open-sub`/`-close-sub` («Not a wiring bug: there was no event», `dropdown-menu.ts:62-96`) | `sidebar-provider.svelte.ts:484-496`, `:470-475`; morfo `menu-sub` `:381-397` ya declara `states: ['open','closed']` |
`menu-item` como `<li>` posicionador es marco estructural (CANON §5): 0
eventos es correcto **en esa faceta**. Su defecto es S-5: posee una aparición
y no la declara.
### 1.2 Eidos — el canon de tamaño y el Build contract
| Id | Qué | Hoy (medido / leído) | Canon que incumple | Evidencia |
|---|---|---|---|---|
| **E-1** | Eje `size` inexistente | filas `font-size: var(--font-size-sm)` (14px medido) + `min-block-size: var(--space-8)` (32px) — un token de ESPACIO (escala `density-space`) como altura de CONTROL (escala `density-control`); `data-size` ausente; `group-label` clava `xs` | theming/reference §5: bundle `--size-{k}-*` obligatorio; «Nav controls xs..lg»; size→font 1:1 y «compact md=14» REVOCADO 2026-06-17; Build contract «Size (controls)» («consuming none of the bundle» es la deriva nombrada) y «Density / spacing» | `sidebar.css:209-231`, `:173-181`; tokens `recipes/base.ts:4725-4741`; eidos `types.ts:35-42` (sin `size`) |
| **E-2** | El guard no lo ve | `recipe-css-contract.test.ts:613-639` escanea `RECIPE_TOKENS`; el `font-size` va escrito en el `.css` y `--space-8` no casa con `RAW_COORDINATE` | memoria `a-guard-that-inspects-nothing-passes` — verde y ciego | `sidebar.css:221` |
| **E-3** | `MenuAction` sin fuente | 13,33px `line-height: normal` (UA) — fuera de la escala | theming §5 1:1; Build contract «Composition» | `sidebar.css:260-273` |
| **E-4** | Hover a mano | `background: var(--color-surface-raised)` en `:hover` | Build contract «State (hover/active)»: capa `--state-{hover,press,selected}`, no `--x-hover-bg` por componente (`archetypes.css:59-83`) | `sidebar.css:233-237`, `:275-278` |
| **E-5** | Tallas clavadas en las composiciones | `Trigger` `size="sm"`; `MenuBadge` `size="xs"` | theming §5 «Container→part derivation: capped at md» (precedente `dialog/context.ts:6-13` + `Dialog.Close`) | `sidebar-trigger.svelte:15`; `sidebar-menu-badge.svelte:16` |
| **E-6** | Ancho del raíl de iconos fijo | `--sidebar-width-icon: 3rem` = 32px de fila + 2×8px; en cuanto la fila siga al bundle (md = 36px) el raíl **no cabe** | derivación, no constante (la propia justificación del token: «widths are TOKENS, never TS constants») | `recipes/base.ts:4727`; `sidebar.css:102-104` |
| **E-7** | Iconos de la fila | la demo pasa `size="sm"` a cada `Icon` a mano; la fila no dimensiona su glifo | bundle `--size-{k}-icon-size` (como `Button`) | `web/routes/uix/components/sidebar/+page.svelte:158-220` |
**Corrección a mi propio análisis inicial**: dije que `--sidebar-width` /
`--sidebar-width-mobile` (16rem / 18rem) «ignoran `--scaling`». Son anchos de
LAYOUT, y el sistema fija sus propios anchos de layout en rem
(`--content-width-*`, `base.css:146-152`) — no siguen `--scaling` por
diseño. **No son defecto.** Sólo `width-icon` (E-6) lo es, porque es una
medida de CONTROL (fila + padding).
---
## 2. Contrato objetivo — sema
Los eventos que el morfo del sidebar declara tras la corrección. Nombres bajo
la gramática `{family}-{verb}[-{nuance}]` (CANON §6); anclaje por instancia
porque `menu-button`, `menu-sub-button` y `menu-sub` son partes REPETIDAS
(memoria `repeated-part-cannot-be-the-subject`; `SomaRuntimePart.trigger` anclado,
`runtime.svelte.ts:243-258`; `partInstance` `:299`).
| Evento | Familia · verbo | `target` (+ `allowedTargets`) | `sequence` | Cuándo lo emite el soma | Voz por defecto (pack) |
|---|---|---|---|---|---|
| `emerge-expand` / `emerge-collapse` | (sin cambio) | `panel` | post | `toggle()` | `emerge.soft` / `emerge.exit.soft` (como hoy) |
| **`contact-activate`** | contact · activate — el verbo de `Button` (`button.ts:56-71`); cap. 22 §6: los verbos de contact «no son gestos distintos que exijan semánticas distintas» | `menu-button` + `allowedTargets: [menu-sub-button, rail]` | **pre** | `onclick` de la fila / sub-fila (`<a>` y `<button>`, ratón y Enter/Espacio nativos) y del rail ANTES de `toggle()`; guardado por `disabled`; el `mergeProps` de la casa ya deja al consumidor vetar con `preventDefault` (`props.ts:20-27`) | familia: `touch` + háptico `tick` (`sema-map.ts:411-424`); firma visual `press-squeeze` GRATIS por regla de familia (`base.css:6636`) — sin CSS nuevo |
| **`shift-navigate`** | shift · navigate — cap. 27 §5 «Navegación: página A → página B» | **`provider`** (el shell que cruza como unidad — D-SB.1bis; antes decía `menu-button` + `queue`) | **post**, sin `regime` | mismo `onclick`, emitido SIN anclar (`runtime.trigger`), sólo si hay `href` y no está `disabled` (la fila es un control de navegación; el `<button>` sin `href` NO lo emite — su consecuencia la pone lo que el consumidor compone). Sin `direction` (CANON §9: «an invented sense is worse than none»); sin intent | familia: `slide` (`sema-map.ts:472-482`); sin firma visual (sin `direction` no hay cruce que dibujar, `presets/css.ts:387-412`) — el cruce lo pinta la página |
| **`emerge-open-sub`** / **`emerge-close-sub`** | emerge · open / close — cap. 26 §5-§6 (menú anclado) | `menu-sub` (repetida → anclada) | post / pre; `commits` `data-state` open/closed sobre `menu-sub` (patrón `dropdown-menu.ts:62-96`) | `openSub()` / `closeSub()` — los ÚNICOS mutadores (pointerenter, focusin, focusout, pointerleave, Escape, y el `watch` de `floating`); sólo en modo icono (`floating`) y sólo cuando el estado CAMBIA (nada de emisiones fantasma, lección de nav-menu) | **D-SB.3** (recomiendo `SILENT` como el tooltip: barrido del puntero por el raíl = una aparición por fila; cap. 26 §7 «el sonido, normalmente, sobra»; cap. 7 §13 Criterio 12) |
Dos hechos que fijan la forma:
- **Dos ocurrencias, dos nodos — sin `queue`.** La superficie `data-event-*` es
UNA RANURA por elemento (memoria `perceptual-surface-one-slot`) y `sema.md`
manda «reach for `regime` last»: el gesto se estampa donde está la mano (la
fila) y el cruce en la superficie que cruza como unidad (el `provider`) —
el precedente de `calendar` (el sello de shift se movió del botón porque la
compresión no se pintaba) y de `field-langs` (`provider`). ~~La fila que
navega es la quinta pareja `queue`~~ — enmendado por D-SB.1bis.
- **`contact` de `Trigger` y `MenuAction` NO va en el morfo**: lo aporta
`Button` por composición (regla de nav-menu, `navigation-menu.ts:64-68`:
«re-declaring it here would put the same verb twice on the same node»). El
`rail` sí se declara porque NO compone `Button` (es una franja de cromo, no
un control de acción — razón válida del wrapper). La FILA tampoco compone
`Button`: su cromo es de fila (`data-active`, `aria-current`, glifo+badge) y
su segundo evento (`shift`) es del sidebar; declarar ambos en el mismo morfo
es lo que hace `queue` verificable (una sola runtime).
Precedentes que este contrato copia: `toggle` (contact + consecuencia en un
nodo con `queue`), `field-langs` / `carousel` / `calendar` (shift emitido por
el componente que sabe que cruza), `dropdown-menu` (`emerge-*-sub` desde el
mutador único), `navigation-menu` (anclaje por instancia en una parte
repetida), «CTA que navega» del README de `Button` (un `<a>` que navega SÍ
lleva contact en esta casa).
## 3. Contrato objetivo — eidos (`size`)
- **Prop `size`** en el wrapper eidos `<Sidebar>`: `xs | sm | md | lg`
(«Nav controls», theming §5), `ResponsiveProp` resuelto con
`eidos.resolve(size, 'md')` y estampado como **`data-size` en el provider**
— atributo VISUAL del wrapper, **no del morfo** (`render-css.ts:2122-2127`;
memoria memoria `visual-wrapper-attrs-not-in-morfo`); patrón exacto
`breadcrumb.svelte:11-17`. Default **`md`** (theming §5: «`md` is the
default»; 1:1 → fila 36px / 16px). Quien quiera la densidad de hoy pasa
`size="sm"` (30px / 14px).
- **Tokens por talla** en `recipes/base.ts` (patrón `breadcrumb`
`base.ts:519-529`), todos referencias al bundle — así el guard del bundle
por fin los VE: `row-height-{k}: var(--size-{k}-control-height)` ·
`font-size-{k}: var(--size-{k}-font-size)` · `line-height-{k}` ·
`letter-spacing-{k}` · `icon-size-{k}` · `row-padding-inline-{k}` ·
`row-gap-{k}` · `row-radius-{k}`. Internos `--_sidebar-*` conmutados por
`[data-sidebar][data-size]`; el `.css` sólo consume internos (E-2 deja de ser
ciego porque el valor vive en el token). Se retiran `row-height`,
`row-radius`, `row-gap` (sustituidos por su forma por talla).
- **`group-label`**: **D-SB.4** — recomiendo un paso por debajo de la fila
(`--size-{k-1}-font-size`), porque ES una etiqueta (nombra al `menu` por
`aria-labelledby`) y la doctrina de etiqueta (theming §5 «Fields: input 1:1
+ the label one step below») se funda en la jerarquía etiqueta↔control, no
en que sea un `<label>`. A `sm` reproduce la proporción de shadcn (xs/sm).
- **`width-icon` derivado**, con `scope: 'host'` (TSC: nada derivado en
`:root`): `calc(var(--_sidebar-row-height) + 2 * var(--sidebar-padding))`.
- **Derivación container→part, capada en `md`** (theming §5; contexto eidos
como `dialog/context.ts`): `Trigger` (compone `Button`), `MenuAction`
(pasa a componer **`IconButton variant="ghost"`** — contact + talla + capa
de estado gratis; el recipe de la acción desaparece), `MenuBadge`
(`Badge`, recortado a su subset `xs..lg`). Un `size` explícito en la parte
gana. Recogido en **D-SB.5**.
- **Capa de estado**: hover de fila y sub-fila por
`background-image: linear-gradient(var(--state-hover), var(--state-hover))`
(las filas no llevan archetype a propósito — precedente `anchor-nav.ts:68-69`
— así que el recipe aplica la capa, como documenta `archetypes.css:59-83`);
el acento `[data-active]` (`--color-primary-track/-text`) se queda: «per-variant
accent stays in the recipe». La pulsación NO se escribe: la pone la firma de
familia `contact`.
- **Glifo de fila**: la fila dimensiona su primer hijo `svg` con
`--_sidebar-icon-size` (como `Button`); la demo deja de pasar `size="sm"`.
- **Se conservan** `width`, `width-mobile` (layout), `padding`, `gap`,
`sub-indent`, `sub-z`, `bg`, `border-color`, `rail-width`.
- **A verificar en F3, no detectado**: hit-area táctil de las filas bajo
`pointer: coarse` (Build contract §37) — sin archetype, la regla de
`archetypes.css` no las alcanza; si falta, `::before` slop.
---
## 4. Decisiones que sólo el autor firma (D-SB)
Una por mensaje, con las cinco preguntas. Todas traen mi recomendación; el
resto del plan asume la recomendada salvo firma en contra.
| Id | Decisión | Recomendación y por qué |
|---|---|---|
| **D-SB.1** | ¿La fila declara **`contact-activate` + `shift-navigate`** (libro) — o sólo `contact` y el `shift` lo emite app-land en `afterNavigate`? | **Las dos en la fila.** El libro compone el enlace así (cap. 22 §9, 27 §8); la fila es control de navegación por declaración; `field-langs`/`carousel` ya emiten `shift` desde el componente que sabe que cruza. App-land conserva `shift` para las consecuencias que ningún componente conoce (el `Select` de idioma) y la orientación del destino (cap. 27 §9). |
| **D-SB.2** | Verbo de contact: `activate` (Button) o `press` (toggle/switch) | **`activate`** — misma gramática que `Trigger` (Button) y que el rail; el sonido es idéntico (`touch`, verbo indiferente). |
| **D-SB.3** | Voz del flyout `emerge-open-sub` / `-close-sub` | **`SILENT`** en el pack (como tooltip): hover por el raíl = una aparición por fila; cap. 26 §7; Criterio 12. La firma visual (`present-rise`) se queda. |
| **D-SB.4** | Talla del `group-label` | **Un paso por debajo de la fila** (doctrina de etiqueta, §3). Alternativa: misma talla que la fila. |
| **D-SB.5** | Derivación de `Trigger` / `MenuAction` / `MenuBadge` | **La regla que ya existe** (container→part capada en `md`), sin inventar un «un paso por debajo» para controles anidados. `MenuAction` = `IconButton ghost`. |
| **D-SB.6** | Default de `size` | **`md`** (canon). Cambia el aspecto de la demo y del `app-shell` (filas 32/14 → 36/16): se mide y se enseña. |
| **D-SB.7** | Rama de trabajo y ejecutor | Rama del eje a nombrar por el autor; construye un agente Opus con brief de §5, Fable supervisa (patrón `PLAN-background.md` §10-11). |
### 4.1 D-SB.1 — la contra-lectura `commit.select` (app-shell README, Gaps) y por qué el libro no la sostiene
La sesión que destapó el defecto lo dejó escrito en la fila de Gaps de
`src/uix/blocks/app-shell/README.md` («`Sidebar.MenuButton` es MUDO y
`NavigationMenu.Link` no») con un matiz: «en un shell la fila del raíl casi
nunca navega: SELECCIONA la sección (`aria-current="page"`), que es exactamente
el caso de `commit.select`. No se decide desde el tier». La pregunta es la
correcta —¿qué queda FIJADO o qué MARCO cambia?— y el libro la responde en
contra de `commit.select` para esta fila:
1. **El libro nombra el acto**: cap. 8 (TABLA de reubicación) «selection →
commit.select» y, aparte, «navigation → shift.navigate»; «context → shift si
cambia marco; emerge si sólo aparece superficie». Cap. 27 §1: «ir a otra
pantalla, avanzar en un flujo, **cambiar de vista**… todo eso es shift»; §5
«Navegación — shift.navigate: lista → detalle, **página A → página B**»; §3
«lista frente a detalle: cambia qué puedo hacer, qué significa cada acción,
qué es relevante y **cómo vuelvo**». Inbox → Drafts → Settings cambia las
tres cosas y el botón «atrás» del navegador lo prueba.
2. **`aria-current="page"` es HUELLA, no evento** — y es huella de NAVEGACIÓN.
Cap. 1 §7 (TABLA plano/pregunta): «seleccionado» es ESTADO, «seleccionar» es
EVENTO; cap. 12 §6 la huella. La fila no fija nada al pulsar: `active` es un
PROP que el app le pasa DESPUÉS de que el router cambió de ruta; en `Tabs`
el componente posee `value` y lo fija — ahí `commit.select` es honesto,
aquí sería atribuirle a la fila un estado que no es suyo. Y ARIA reserva la
selección a `aria-selected` (tabs, listbox, grid); `aria-current="page"`
significa «la página actual dentro de un conjunto de páginas» — el
vocabulario de la navegación, elegido a propósito por el dossier §P4
(APG Disclosure Navigation + enlaces + `<nav>`). Un lector anuncia «enlace,
página actual»; sema no puede decir «elegido» sobre el mismo nodo.
3. **La lectura «selecciona la sección» ya tiene componente**: `Tabs`
(vertical) — que existe y habla `commit-select`. El morfo del sidebar sólo
conoce dos formas de fila: `<a>` «when it navigates», `<button>` «when it
only opens a sub-menu». No hay tercera («selecciona sin navegar»); si un
shell la necesita, compone `Tabs`, no re-semantiza el `Sidebar`.
4. **«casi nunca navega» confunde persistencia del cromo con ausencia de
navegación**: en la demo (`href="/app/drafts"`) cambian URL, historial y
título; que el shell persista es la DEFINICIÓN de app shell, no la negación
de «página A → página B».
5. **`commit-select + affirm` sobre un enlace es un antipatrón con nombre**:
cap. 10 §12 (TABLA de antipatrones) «Success de navegación — cambio de
contexto con logro → shift + commit.fulfill si procede». Es exactamente lo
que hace hoy `navigation-menu` (T-1): el precedente que hay que reabrir,
no el que hay que copiar.
Consecuencia: D-SB.1 se mantiene (contact + shift en la fila); la fila de Gaps
del `app-shell` se cierra desde este eje cuando F1 aterrice, y T-1 gana la cita
del antipatrón.
---
## 5. Fases y gates
Cada fase: **qué leer ANTES** (entero, no a medias — memoria `read-full-doctrine-before-auditing`),
**qué producir**, **guard**. Ninguna fase arranca sin la anterior en verde.
### F0 — Lectura obligatoria (sin producir nada)
Libro cap. 22 (§6, §9, §11-12), cap. 26 (§5-§7, §9), cap. 27 (§2, §5, §8-§10),
cap. 30 §4 (regla 1), Apéndice A · `docs/CANON.md` entero ·
`docs/architecture/sema.md` (cascada, pack, `regime`) ·
`docs/architecture/morfo.md` (partes repetidas, `allowedTargets`) ·
`docs/theming/reference.md` §5 y §23 · `docs/guides/component-guide.md`
§Build contract + §2 (audit Morfo/Sema) · `src/uix/eidos/components/README.md`
· los tres precedentes: `toggle.ts`, `dropdown-menu.ts`, `breadcrumb.*` ·
`CONTINUE-perceptual-surface.md` §3.0 (una ranura, `queue`, anclaje) ·
memorias: memoria `perceptual-surface-one-slot`, memoria `repeated-part-cannot-be-the-subject`,
memoria `hidden-pane-suspends-raf`, memoria `hmr-stale-tab-phantom-findings`,
memoria `a-guard-that-inspects-nothing-passes`.
### F1 — Morfo + sema (el contrato)
- Producir: `sidebar.ts` — los 5 eventos nuevos de §2 con sus docblocks
citando el libro (borrar «native and unsonified»); `sema/components/sidebar.ts`
— docblock nuevo + reglas sólo donde se DIFIERE del default (D-SB.3 →
`sound: SILENT` en los dos `emerge-*-sub`; nada para contact/shift si se
acepta la familia).
- Guard: `npx vitest run src/uix/morfo src/uix/sema src/uix/contracts.test.ts --project=server`
(validateMorfo: nombres/verbos/`intentRequirement`; pack-census:
selectores sobre `target`/`allowedTargets`) · `npm run morfo:check` ·
`npm run check` delta 0 contra base medida con **stash** (comparar por
MÉTODO, no por cifra).
### F2 — Soma (la emisión)
- Producir: `SidebarMenuButtonProvider.props.onclick` y
`SidebarMenuSubButtonProvider.props.onclick` → `runtimePart.trigger('contact-activate')`
y, si `href && !disabled`, `runtimePart.trigger('shift-navigate')`;
`SidebarToggleProvider` (rail): contact anclado antes de `toggle()` — sólo
la instancia `rail`, el `trigger` sigue callado en soma porque el eidos le
compone `Button`; `SidebarMenuItemProvider.openSub/closeSub` →
`runtime.partInstance('menu-sub', subRef)?.trigger('emerge-open-sub' | 'emerge-close-sub')`
sólo en `floating` y sólo si el estado cambia; `events` del runtime sin
handler para los nuevos (no mutan estado del sidebar).
- Guard: suite browser del componente (`*.svelte.test.ts` — crear si no
existe, con `createActiveUix` REAL, nunca fakes): clic en fila →
`contact-activate` y después `shift-navigate` con `data-event-id` distintos
y el segundo tras el hold; `<button>` sin href → sólo contact; disabled →
nada; rail → contact en rail + emerge en panel; icono+hover →
`emerge-open-sub` en ESA `menu-sub`, leave → `-close-sub`, expandido → nada.
### F3 — Eidos (`size` + capa de estado + composición)
- Producir: `types.ts` (`size`), `sidebar.svelte` (`resolvedSize` +
`data-size` + contexto eidos de talla), tokens por talla en
`recipes/base.ts` (+ regenerar `generated/base.css` por el script, NUNCA a
mano), `sidebar.css` (internos `--_sidebar-*`, capa `--state-hover`, glifo,
`width-icon` derivado), `sidebar-menu-action.svelte` → `IconButton`,
`sidebar-trigger.svelte` / `sidebar-menu-badge.svelte` → talla derivada.
- Guard: `npx vitest run src/uix/eidos` (recipe-css-contract: bundle, sin
literales px/rem en `font-size-*`; elevation; TSC) · `npx tsx scripts/eidos-lint.ts sidebar`
(0 invalid) · `npm run rtl:check` · `npm run component:audit -- --only sidebar`
PASS · **navegador**: computed `min-block-size` de la fila =
`--size-md-control-height` (36px) y `font-size` = `--size-md-font-size`
(16px) por defecto; con `size="sm"` 30/14; `MenuAction` = `IconButton`
con fuente de la escala; `--sidebar-width-icon` = fila + 2·padding en modo
icono; hover pinta `--state-hover`; `data-size` responsive cambia al
redimensionar; densidad `compact` mueve fila y padding a la vez.
### F4 — Docs + demo
- Producir: README soma (§Sema events: tabla completa con `Cuándo` y
«anclado por instancia») · README eidos (eventos + `size` + tokens) ·
demo `+page.svelte`: control `size` (paridad demos↔subset,
`demo-authoring.md` §6), texto de la pestaña sema, tabla de tokens ·
`PLAN-blocks.md` §F1.7: una línea de registro que apunte aquí ·
memoria del proyecto actualizada.
- Guard: `npm run docs:check` 0 errores · `npm run lint`.
### F5 — Verificación final en navegador (Chrome REAL, panel visible)
- Con `sound: true` (BootUix): clic en fila → `touch` y luego `slide` (osc +2
y +2, separados por el hold); rail → `touch` + `emerge.exit.soft`; hover en
raíl → 0 osciladores (SILENT) y `present-rise` visible; nada al hacer hover
en filas expandidas. `press-squeeze` corriendo en la fila durante la ventana
de contact. RTL (`side="right"` + `:dir(rtl)`) sin regresión del
off-canvas. 375px: Drawer intacto. Reduced motion: sin transiciones.
- Trampas conocidas: pane oculto congela rAF y el sello nunca cae
(memoria `hidden-pane-suspends-raf`); pestaña con HMR encima = hallazgos fantasma;
`performance` desde `javascript_tool` miente; el `once:true` ya enganchó.
- Cierre: commit(s) por fase con `-F`, sin `--no-verify`; el `check` final
comparado con la base por stash.
---
## 6. Deuda transversal detectada — NO se ejecuta en este eje
Cada una necesita su propia firma y su propia sesión (memoria `no-cascade-changes`).
Se listan para que no se pierdan, con la contradicción delante del autor.
| Id | Dónde | Qué | Por qué es del libro |
|---|---|---|---|
| **T-1** | `navigation-menu` `link` | emite `commit-select + affirm` al navegar (`navigation-menu.ts:16-32`; provider `:715-728`; pack con `haptic tap`) | cap. 27 §2 «shift no es commit»; cap. 22 §9; cap. 10 §12 antipatrón «Success de navegación» (cambio de contexto con logro). Mismo gesto que la fila del sidebar → misma familia. Decidido en la auditoría sema del 2026-08-05: hay que REABRIRLO, no pisarlo. La fila de Gaps de `src/uix/blocks/app-shell/README.md` («`Sidebar.MenuButton` es MUDO y `NavigationMenu.Link` no») se cierra con este eje + T-1 (ver §4.1). |
| **T-2** | `nav-tree` link (F1.8) · `anchor-nav` link (F1.6) · `breadcrumb` link | «links nativos → 0 eventos» (`nav-tree.ts:27-39`, `anchor-nav.ts:25-30`, `breadcrumb.ts:8-11`) | = S-1. Misma forma de arreglo (contact + shift en la parte `link`, anclado). Cada uno en su sesión; `nav-tree.css:17` arrastra además el pin `sm` (= E-1). |
| **T-3** | `Link` (primitivo) | eidos-only (`scope: ['eidos']`, `link.ts:21`): sin runtime, no PUEDE emitir; la casa cubre el caso con «CTA que navega» (`Button` + `child`) | Darle voz exige membresía soma (decisión de autor). Mientras, la ficha **A-62** del ledger de blocks se equivoca al suponer que un `Link` «emite `shift-navigate`»: hoy no emite nada — corregir la ficha en el eje de blocks. |
| **T-4** | Ledger blocks **A-50** (REFUTADO) | se sostuvo en «el silencio de `Link` es contrato escrito, no deriva» | Bajo el veredicto del autor, ese contrato escrito es el defecto. Reabrir en el eje de blocks. |
| **T-5** | Guard del bundle de talla | `recipe-css-contract` no lee `font-size:` escritos en `.css` (E-2) | Un guard que inspecciona el conjunto equivocado pasa en verde; endurecerlo es un eje de eidos (probar por mutación). |
---
## 7. Riesgos y trampas ya pagadas (memoria)
- **`queue` medido con timers falsos pasa y está mal** — sólo el navegador
real dice si el `shift` llega tras el hold y no 1,6 s tarde.
- **Partes repetidas**: registrar/anclar por instancia; nunca `runtime.trigger`
pelado desde una fila — caería en la última registrada.
- **Un hijo que se registra desde `$effect` mata el efecto raíz** (A30) — el
`hasSub` del sub ya lo hace desde `$effect`; no añadir más registros ahí.
- **`display:none` corta la firma de cierre**: si `emerge-close-sub` (pre) no
se ve porque el flyout se oculta en el mismo tick, gatear la ocultación en el
`Presence` que el shell flotante ya crea (dropdown lo hace); se decide
midiendo en F5, no antes.
- **Instrumento antes que código**: dev server sirviendo pre-HEAD por caché de
Vite; contar nodos WebAudio no es medir la salida.
- **`mergeProps`**: el consumidor que hace `preventDefault` en su `onclick`
apaga la emisión de la fila (semántica de la casa, `props.ts:20-27`) — es
la vía para «yo poseo este gesto», y se documenta en el README.
---
## 8. Registro
- 2026-08-19 — Análisis + medición en navegador (esta sesión). Plan
entregado. **Nada construido, nada firmado.**
- 2026-08-19 — **D-SB.1 FIRMADA** («ok, seguimos»): la fila declara
`contact-activate` + `shift-navigate` (queue); la contra-lectura
`commit.select` del app-shell README queda refutada en §4.1.
- 2026-08-19 — **D-SB.2 FIRMADA**: verbo `activate` (`contact-activate`)
para fila, sub-fila y rail.
- 2026-08-19 — **D-SB.3 FIRMADA**: `emerge-open-sub` / `emerge-close-sub`
declarados sobre `menu-sub`, `SILENT` en el pack (como tooltip).
- 2026-08-19 — **D-SB.4 FIRMADA**: `group-label` un paso por debajo de la
fila (`label-font-size-{k}` = `--size-{k-1}-font-size`).
- 2026-08-19 — **D-SB.5 FIRMADA**: derivación container→part capada en `md`
vía contexto eidos; `Trigger` = `Button`, `MenuAction` = `IconButton ghost`,
`MenuBadge` = `Badge` recortado a `xs..lg`; `size` explícito gana.
- 2026-08-19 — **D-SB.6 FIRMADA**: default `md` (36px / 16px); `sm` para la
densidad actual; se mide antes/después en demo y `app-shell`.
- 2026-08-19 — **D-SB.7 FIRMADA**: en la rama actual (`alpha-0.1-background`),
ejecuta esta sesión, un commit por fase. **Las 7 firmadas → arranca F0/F1.**
uix(sidebar): un token de ESPACIO no es la altura de un control, y el raíl de iconos no es una constante El componente vivía fuera del canon de talla: la fila clavaba `min-block-size: var(--space-8)` —un token de la escala de ESPACIO haciendo de altura de CONTROL, que es otra escala de densidad— y `font-size: var(--font-size-sm)` escrito en el recipe, donde el guard del bundle no lo ve. El rótulo clavaba `xs`, el trigger `sm`, el badge `xs`, y la acción no componía `Button`: se quedaba con los 13,33px del user-agent y sin acuse perceptivo propio. Ahora el sidebar tiene eje `size` (xs..lg, default `md`, `data-size` en el WRAPPER —nunca en el morfo, o la resolución de soma lo pisaría con undefined—) y cada coordenada de la fila sale del bundle `--size-{k}-*`. Medido: 26/12 · 30/14 · 36/16 · 44/20, con el glifo siguiendo (14/16/18/20) por `--icon-size`, que es la costura que ya usa `Button` porque un `<Icon>` pinta su tamaño INLINE y gana a cualquier regla de hoja. El raíl de iconos deja de ser `3rem`: se DERIVA (fila + 2×padding), así que da 42 · 46 · 52 · 60 y a `md` ya no recorta la fila que tiene que contener. Tres composiciones más, por la regla que ya existía (container→part capada en `md`, precedente `Dialog.Close`): el `Trigger` deriva su talla, la `MenuAction` pasa a ser `IconButton ghost` —y con ella llegan su cromo, su anillo, su capa de estado y su `contact-activate`—, y el `MenuBadge` deriva con UNA excepción firmada: baja un paso, porque con la regla al pie el chip sale a 36/16 junto a una fila de 36/16 e iguala al control en vez de anotarlo. El hover deja de ser un `--color-surface-raised` a mano y pasa a la capa canónica `--state-hover`. Al medirlo salió un defecto que no buscaba: la fila activa no daba NINGUNA respuesta al hover, porque su acento usaba el shorthand `background`, que resetea el longhand `background-image` donde vive la capa. Con `background-color` la capa compone encima, que es justo para lo que existe. Verificado con Playwright headless (la pantalla del navegador estaba oculta y ahí rAF se congela: las medidas de layout salían falsas). Gates: eidos-lint 49 morfo-backed / 0 invalid · component:audit PASS 0E/0W · vitest src/uix/eidos 434/435 (el rojo es `skin-media-player`, ajeno, idéntico en base) · rtl:check 0 · docs:check 0/639 · `check` con delta CERO contra la base medida con stash (72/72). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- 2026-08-19 — **D-SB.5bis FIRMADA** (salió de F3, medida en navegador): el
`MenuBadge` es la ÚNICA excepción a la derivación de D-SB.5 — baja un paso
(`xs→xs · sm→xs · md→sm · lg→md`). Con la letra de D-SB.5 el chip mide
36px/16px con la fila en 36px/16px: iguala el control en vez de anotarlo, y
su cifra pesa lo mismo que la etiqueta. Misma razón que D-SB.4 (jerarquía
anotación↔control) y que el propio tipo de `Badge` («etiqueta hermana del
texto; una talla mayor competiría»). Queda escrita como excepción en el
README de eidos con su medida.
uix(sidebar): la fila que navega decía «te sentí» a nadie, y el libro dice cómo se dice «Navegar una fila es nativo y no suena» era una decisión de la fase 1 tomada en silencio (el plan la dejó como «decidir en fase 1») sobre una premisa falsa — «item = Link puro»—: la fila es `<button>` cuando no tiene `href`, la acción es un botón pelado y el `MenuItem` posee un flyout. Medido en navegador: pulsar fila, sub-fila o acción estampaba CERO; el Trigger sí acusaba recibo, porque compone `Button`. El libro no deja margen: cap. 22 §9 compone el enlace como «contact.press seguido de shift.navigate: la presión no es la navegación», cap. 27 §8 lo repite y cap. 22 §12 llama antipatrón al contacto mudo. Así que la fila declara las dos ocurrencias, y la aparición del flyout —cap. 26 §5-§6, un menú anclado— deja de ser un cambio de estado sin evento. Dónde se estampa cada una lo decidió la doctrina, no la comodidad: el gesto en la mano (la fila, anclado por instancia: es parte REPETIDA) y el cruce en la superficie que cruza como unidad (el provider). Eso evita la quinta pareja `queue` que yo iba a declarar: calendar ya movió su `shift-navigate` fuera del botón porque las dos ocurrencias se comían la única ranura y el press-squeeze no llegaba a pintarse. Voz: contact y shift toman la de su familia (`touch`, `slide`) sin escribir nada; los dos `emerge-*-sub` van SILENT como el tooltip — el flyout se abre al pasar el puntero, y un barrido por el raíl serían ocho apariciones sonando (cap. 26 §7, Criterio 12). El `trigger` NO declara contacto: lo trae por composición de `Button`. El `rail` sí, porque es una franja de cromo y nadie más lo estampa. Medido en el navegador tras el cambio: fila con href → `contact-activate` (press-squeeze corriendo) en ESA fila + `shift-navigate` en el shell; fila deshabilitada → nada; sub-fila → el mismo par; rail → contacto propio + `emerge-collapse` del panel; en modo icono, hover → `emerge-open-sub` (present-rise), hover repetido → nada, salir → `emerge-close-sub` (dismiss-fade). Suite nueva de 8 contratos, `check` con delta CERO contra la base medida con stash (72/72), morfo:check PASS, contracts sin fallos nuevos. Plan y decisiones firmadas: docs/process/PLAN-sidebar.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
- 2026-08-19 — **D-SB.1bis FIRMADA** (salió de F0): `shift-navigate` se
estampa en el **`provider`** (el shell que cruza como unidad), post, **sin
`queue`**, emitido sin anclar desde el `onclick` de la fila; `contact-activate`
queda en la fila (la mano), anclado. Precedentes: `calendar` (el sello de
shift se movió del botón a la vista que cruza — la compresión no se pintaba)
y `field-langs` (`provider`). «Reach for `regime` last» (`sema.md`). §2 del
plan queda enmendado por esta línea.

Powered by TurnKey Linux.