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

40 KiB

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.

⚠️ AL DÍA (2026-08-19): las 9 decisiones están firmadas y las 5 fases EJECUTADAS — tres commits (f6d5fa159 · 63b2de03c · bae03e55c). El handoff con el resumen de una pantalla y lo que queda (nada en este eje; seis deudas en otros) es CONTINUE-sidebar.md.

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, docs/architecture/sema.md, docs/theming/reference.md §5, docs/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 EJECUTADA 2026-08-19 navigation-menu link emitía commit-select + affirm al navegar; ahora declara el mismo par que la fila del sidebar — contact-activate anclado en el enlace + shift-navigate en el <nav>. Enmienda D.6 (PROJECT_CANON). Medido en Chromium: contacto con press-squeeze EN el enlace + shift en el <nav>, 4 osciladores, cero familia commit. La cita correcta del antipatrón es TABLA 9.3, no cap. 10 §12 cap. 27 §2 «shift no es commit»; cap. 22 §9; TABLA 9.3 antipatrón «Success de navegación» (cambio de contexto con logro) — verificada en el PDF; el ancla «cap. 10 §12» que escribió este plan es errónea (ahí están «semáforo redivivo / celebración prematura / intent decorativo»), aunque la apertura de cap. 10 sí dice «una navegación no es exitosa por cambiar de pantalla». Mismo gesto que la fila del sidebar → misma familia. Reabierta, no pisada, la decisión de la auditoría sema del 2026-08-05 (ledger A-54, que movió el evento fuera del marco: mover estaba bien, la familia no). 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.

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.
  • 2026-08-19 — F1+F2 EJECUTADAS (commit f6d5fa159) · F3+F4 EJECUTADAS (commit 63b2de03c) · F5 MEDIDA. Verificación perceptual con Playwright headless (la pantalla del navegador estaba oculta, y ahí rAF se congela: las medidas de layout salían falsas — trampa conocida, pagada otra vez): · fila con href → contact-activate con press-squeeze corriendo EN la fila + shift-navigate en el shell, 4 osciladores (dos sonidos) · fila deshabilitada → 0 eventos, 0 osciladores · raíl → contacto propio (press-squeeze) + emerge-collapse del panel (collapse-conceal), 4 osc · flyout en modo icono: abrir → emerge-open-sub con present-rise y 0 osciladores, cerrar → emerge-close-sub con dismiss-fade y 0 osc — el SILENT del pack demostrado por diferencial con la misma maquinaria · reduced-motion → transiciones a 0s · 375px → data-mobile, raíl oculto, sin desbordamiento horizontal · 0 errores de consola. Tallas medidas end-to-end desde el prop: 26/12 · 30/14 · 36/16 · 44/20, con glifo 14/16/18/20, rótulo 10/12/14/16, raíl de iconos 42/46/52/60 y las composiciones capadas en md (a lg el Trigger y la acción se quedan en 36px).
  • 2026-08-19 — REVISIÓN ADVERSARIAL del eje entero (6 lentes × verificador escéptico por hallazgo, 30 agentes): 21 hallazgos, 17 refutados, 4 reales, los cuatro arreglados:
    1. El MenuBadge no llegaba a lg — indexaba la tabla de la excepción por part, que clampToPartSize ya había capado en md, así que la rama lg→md era código muerto y size="lg" daba un chip un paso pequeño. Ahora indexa por la talla del SIDEBAR. Medido: 26 · 26 · 30 · 36.
    2. La fila de Gaps del README de app-shell afirmaba lo contrario de lo que ahora hay («sus morfos sólo declaran emerge-expand/collapse»): la reescribí, dejando vivo sólo lo que sigue siéndolo (T-1).
    3. La pestaña sema de la demo clavaba las SEIS previsualizaciones en el panel: con cuatro eventos apuntando a otras partes, el ▶ de contact-activate estampaba la familia en el panel de 16rem y press-squeeze encogía el panel ENTERO, y los dos emerge-*-sub no podían encontrarse con la regla que los silencia (está atada a [data-sidebar-menu-sub]). SemaPanel recibe ahora la acción y la página resuelve la parte desde el contrato compilado. Medido: contacto → una fila con press-squeeze, open-sub → el sub con present-rise, shift → el provider.
    4. emerge-close-sub se sellaba en un nodo que el mismo tick ocultaba: display: none mataba dismiss-fade antes de pintar un frame, y como el pack lo silencia a propósito, la retirada se quedaba SIN NINGÚN canal. La regla se guarda ahora con :not([data-event^='emerge-close']). Medido en Chromium: opacidad 0,85 → 0,35 → 0,03 con la animación running y el nodo oculto sólo a los ~516 ms, cuando el canal visual des-sella. ⚠️ Mi primera sonda dijo que dismiss-fade corría, y era mentira: leía animationName DENTRO del callback del MutationObserver, o sea antes del flush que ocultaba el elemento. El instrumento midió el instante equivocado, no el código.
  • 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.
  • 2026-08-19 — T-1 EJECUTADA (deuda de OTRO eje, §6, firmada aparte por el autor: «adóptalo con las cinco preguntas»). navigation-menu habla ahora el mismo par que la fila del sidebar. Lo que las cinco preguntas destaparon y el plan NO tenía: (a) la cita del antipatrón es TABLA 9.3, no cap. 10 §12; (b) D.6 de book-deviations.md es PROJECT_CANON y prescribía commit.subtle + tap para este componente por nombre — enmendada en el mismo commit, con dos cláusulas suyas que ya estaban caducadas (el emerge-open/close declarado desde el 2026-08-05, y un commit.subtle que el tier de verbos había dejado sin implementar); (c) el README de soma describía el estado ANTERIOR al 2026-08-05 (target item, emisión en openNow); (d) el pack se quedaba sin una sola firma y propuse BORRARLO — el autor paró y pidió releer con el canon y el libro: la pregunta no era «qué escribe el fichero» sino «qué debe decir la aparición del megamenú», y la respuesta es SILENT en emerge-open / emerge-close (se abre por HOVER; medido con el default de familia, un barrido por dos triggers y salir sonaba open · open · close, 6 osciladores, sin un clic) — cap. 17 §3, cap. 26 §7, cap. 32 §10-§11, la postura de tooltip y de D-SB.3. Firmado SILENT. Con eso el pack DIFIERE del default y vuelve a ser un pack; expression: 'pack' se queda. Medido en Chromium headless: contact-activate (familia contact, sin intent) sobre [data-navigation-menu-link] con press-squeeze corriendo EN el enlace a +8 y +30 ms y retirado a +120 ms · shift-navigate sobre [data-navigation-menu] · 4 osciladores (dos sonidos) · cero estampados de familia commit · el par emerge-open/emerge-close intacto en el trigger (2 osc cada uno) · 0 errores de consola.
  • 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.