| **S-4** | `rail` (`<buttonarchetype=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
| **`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`;
- **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.
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). **Nota**: al mover el `font-size` del `.css` al TOKEN, el guard SÍ vio el único primitivo crudo que escribí (`label-font-size-xs`) y lo rechazó — la deuda es que sólo ve tokens, no reglas. |
| **T-6** | `--control-height-*` / `--space-*` en `:root` | Un `data-density` o un `--scaling` puestos en un SUBÁRBOL no re-derivan las alturas de control: medido 2026-08-19, con `data-density='compact'` sobre la escena el hijo ve `--density-control-scale: 0.9` pero `--control-height-md` sigue valiendo `calc(36px * 1 * 1)`, porque un custom property hereda su valor YA SUSTITUIDO desde donde se declaró. Idéntico para la fila del sidebar, el `Trigger` y un `Button` canónico (36px los tres en todos los casos) | No es defecto del sidebar —es paridad exacta con el canon— pero sí una propiedad del eje de theming que ninguna doc enuncia: la densidad y el zoom sólo son per-documento, no per-subárbol. Eje de eidos/theming. |