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.MenuItemno 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) esCONTINUE-sidebar.md.Fuentes de doctrina que este plan cita y NO copia: el libro (
docs/Disenando_lo_que_ocurre_FINAL.pdf, anclas endocs/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)
- 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 quedata-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. - «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 elemergedel panel (PLAN-blocks.md:1659-1664), y la premisa «item = Link puro» no se cumple:MenuButtones<button>sinhref,MenuActiones un<button>pelado,MenuItemPOSEE un flyout. - El framework ya se contradice a sí mismo sobre el mismo gesto:
navigation-menuemitecommit-select + affirmal pulsar sulink(«Navigating IS the evaluable act... it had no voice»,navigation-menu-provider.svelte.ts:715-728), mientraslink/anchor-nav/breadcrumb/nav-tree/sidebarcallan «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. - 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
SILENTdel 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. - Tallas: el sidebar se construyó fuera del canon de tamaño (sin
size, un token de ESPACIO como altura de control,smclavado, fuente del UA en la acción). La corrección es mecánica y tiene precedentes en el propio repo (breadcrumbpara el ejedata-size,dialog/context.tspara 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 superficiedata-event-*es UNA RANURA por elemento (memoriaperceptual-surface-one-slot) ysema.mdmanda «reach forregimelast»: el gesto se estampa donde está la mano (la fila) y el cruce en la superficie que cruza como unidad (elprovider) — el precedente decalendar(el sello de shift se movió del botón porque la compresión no se pintaba) y defield-langs(provider).La fila que navega es la quinta pareja— enmendado por D-SB.1bis.queue contactdeTriggeryMenuActionNO va en el morfo: lo aportaButtonpor 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»). Elrailsí se declara porque NO componeButton(es una franja de cromo, no un control de acción — razón válida del wrapper). La FILA tampoco componeButton: 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 hacequeueverificable (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
sizeen el wrapper eidos<Sidebar>:xs | sm | md | lg(«Nav controls», theming §5),ResponsivePropresuelto coneidos.resolve(size, 'md')y estampado comodata-sizeen el provider — atributo VISUAL del wrapper, no del morfo (render-css.ts:2122-2127; memoria memoriavisual-wrapper-attrs-not-in-morfo); patrón exactobreadcrumb.svelte:11-17. Defaultmd(theming §5: «mdis the default»; 1:1 → fila 36px / 16px). Quien quiera la densidad de hoy pasasize="sm"(30px / 14px). - Tokens por talla en
recipes/base.ts(patrónbreadcrumbbase.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.csssólo consume internos (E-2 deja de ser ciego porque el valor vive en el token). Se retiranrow-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 almenuporaria-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>. Asmreproduce la proporción de shadcn (xs/sm).
- the label one step below») se funda en la jerarquía etiqueta↔control, no
en que sea un
width-iconderivado, conscope: '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 comodialog/context.ts):Trigger(componeButton),MenuAction(pasa a componerIconButton variant="ghost"— contact + talla + capa de estado gratis; el recipe de la acción desaparece),MenuBadge(Badge, recortado a su subsetxs..lg). Unsizeexplí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 — precedenteanchor-nav.ts:68-69— así que el recipe aplica la capa, como documentaarchetypes.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 familiacontact. - Glifo de fila: la fila dimensiona su primer hijo
svgcon--_sidebar-icon-size(comoButton); la demo deja de pasarsize="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 dearchetypes.cssno las alcanza; si falta,::beforeslop.
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:
- 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.
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:activees un PROP que el app le pasa DESPUÉS de que el router cambió de ruta; enTabsel componente poseevaluey lo fija — ahícommit.selectes honesto, aquí sería atribuirle a la fila un estado que no es suyo. Y ARIA reserva la selección aaria-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.- La lectura «selecciona la sección» ya tiene componente:
Tabs(vertical) — que existe y hablacommit-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, componeTabs, no re-semantiza elSidebar. - «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». commit-select + affirmsobre 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 hoynavigation-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: SILENTen los dosemerge-*-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 sobretarget/allowedTargets) ·npm run morfo:check·npm run checkdelta 0 contra base medida con stash (comparar por MÉTODO, no por cifra).
F2 — Soma (la emisión)
- Producir:
SidebarMenuButtonProvider.props.onclickySidebarMenuSubButtonProvider.props.onclick→runtimePart.trigger('contact-activate')y, sihref && !disabled,runtimePart.trigger('shift-navigate');SidebarToggleProvider(rail): contact anclado antes detoggle()— sólo la instanciarail, eltriggersigue callado en soma porque el eidos le componeButton;SidebarMenuItemProvider.openSub/closeSub→runtime.partInstance('menu-sub', subRef)?.trigger('emerge-open-sub' | 'emerge-close-sub')sólo enfloatingy sólo si el estado cambia;eventsdel runtime sin handler para los nuevos (no mutan estado del sidebar). - Guard: suite browser del componente (
*.svelte.test.ts— crear si no existe, concreateActiveUixREAL, nunca fakes): clic en fila →contact-activatey despuésshift-navigatecondata-event-iddistintos 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-suben ESAmenu-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 enrecipes/base.ts(+ regenerargenerated/base.csspor el script, NUNCA a mano),sidebar.css(internos--_sidebar-*, capa--state-hover, glifo,width-iconderivado),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 enfont-size-*; elevation; TSC) ·npx tsx scripts/eidos-lint.ts sidebar(0 invalid) ·npm run rtl:check·npm run component:audit -- --only sidebarPASS · navegador: computedmin-block-sizede la fila =--size-md-control-height(36px) yfont-size=--size-md-font-size(16px) por defecto; consize="sm"30/14;MenuAction=IconButtoncon fuente de la escala;--sidebar-width-icon= fila + 2·padding en modo icono; hover pinta--state-hover;data-sizeresponsive cambia al redimensionar; densidadcompactmueve fila y padding a la vez.
F4 — Docs + demo
- Producir: README soma (§Sema events: tabla completa con
Cuándoy «anclado por instancia») · README eidos (eventos +size+ tokens) · demo+page.svelte: controlsize(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:check0 errores ·npm run lint.
F5 — Verificación final en navegador (Chrome REAL, panel visible)
- Con
sound: true(BootUix): clic en fila →touchy luegoslide(osc +2 y +2, separados por el hold); rail →touch+emerge.exit.soft; hover en raíl → 0 osciladores (SILENT) ypresent-risevisible; nada al hacer hover en filas expandidas.press-squeezecorriendo 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;performancedesdejavascript_toolmiente; elonce:trueya enganchó. - Cierre: commit(s) por fase con
-F, sin--no-verify; elcheckfinal 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 |
|---|---|---|---|
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)
queuemedido con timers falsos pasa y está mal — sólo el navegador real dice si elshiftllega tras el hold y no 1,6 s tarde.- Partes repetidas: registrar/anclar por instancia; nunca
runtime.triggerpelado desde una fila — caería en la última registrada. - Un hijo que se registra desde
$effectmata el efecto raíz (A30) — elhasSubdel sub ya lo hace desde$effect; no añadir más registros ahí. display:nonecorta la firma de cierre: siemerge-close-sub(pre) no se ve porque el flyout se oculta en el mismo tick, gatear la ocultación en elPresenceque 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 hacepreventDefaulten suonclickapaga 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-lecturacommit.selectdel 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-subdeclarados sobremenu-sub,SILENTen el pack (como tooltip). - 2026-08-19 — D-SB.4 FIRMADA:
group-labelun 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
mdvía contexto eidos;Trigger=Button,MenuAction=IconButton ghost,MenuBadge=Badgerecortado axs..lg;sizeexplícito gana. - 2026-08-19 — D-SB.6 FIRMADA: default
md(36px / 16px);smpara la densidad actual; se mide antes/después en demo yapp-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 (commit63b2de03c) · 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 conhref→contact-activateconpress-squeezecorriendo EN la fila +shift-navigateen el shell, 4 osciladores (dos sonidos) · fila deshabilitada → 0 eventos, 0 osciladores · raíl → contacto propio (press-squeeze) +emerge-collapsedel panel (collapse-conceal), 4 osc · flyout en modo icono: abrir →emerge-open-subconpresent-risey 0 osciladores, cerrar →emerge-close-subcondismiss-fadey 0 osc — elSILENTdel 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 enmd(algelTriggery 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:
- El
MenuBadgeno llegaba alg— indexaba la tabla de la excepción porpart, queclampToPartSizeya había capado enmd, así que la ramalg→mdera código muerto ysize="lg"daba un chip un paso pequeño. Ahora indexa por la talla del SIDEBAR. Medido: 26 · 26 · 30 · 36. - La fila de Gaps del README de
app-shellafirmaba 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). - La pestaña sema de la demo clavaba las SEIS previsualizaciones en el
panel: con cuatro eventos apuntando a otras partes, el ▶ de
contact-activateestampaba la familia en el panel de 16rem ypress-squeezeencogía el panel ENTERO, y los dosemerge-*-subno podían encontrarse con la regla que los silencia (está atada a[data-sidebar-menu-sub]).SemaPanelrecibe ahora la acción y la página resuelve la parte desde el contrato compilado. Medido: contacto → una fila conpress-squeeze, open-sub → el sub conpresent-rise, shift → el provider. emerge-close-subse sellaba en un nodo que el mismo tick ocultaba:display: nonematabadismiss-fadeantes 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ónrunningy el nodo oculto sólo a los ~516 ms, cuando el canal visual des-sella. ⚠️ Mi primera sonda dijo quedismiss-fadecorría, y era mentira: leíaanimationNameDENTRO del callback del MutationObserver, o sea antes del flush que ocultaba el elemento. El instrumento midió el instante equivocado, no el código.
- El
- 2026-08-19 — D-SB.5bis FIRMADA (salió de F3, medida en navegador): el
MenuBadgees 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 deBadge(«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-menuhabla 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 debook-deviations.mdes PROJECT_CANON y prescribíacommit.subtle+tappara este componente por nombre — enmendada en el mismo commit, con dos cláusulas suyas que ya estaban caducadas (elemerge-open/closedeclarado desde el 2026-08-05, y uncommit.subtleque el tier de verbos había dejado sin implementar); (c) el README de soma describía el estado ANTERIOR al 2026-08-05 (targetitem, emisión enopenNow); (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 enemerge-open/emerge-close(se abre por HOVER; medido con el default de familia, un barrido por dos triggers y salir sonabaopen·open·close, 6 osciladores, sin un clic) — cap. 17 §3, cap. 26 §7, cap. 32 §10-§11, la postura detooltipy 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(familiacontact, sin intent) sobre[data-navigation-menu-link]conpress-squeezecorriendo EN el enlace a +8 y +30 ms y retirado a +120 ms ·shift-navigatesobre[data-navigation-menu]· 4 osciladores (dos sonidos) · cero estampados de familiacommit· el paremerge-open/emerge-closeintacto en el trigger (2 osc cada uno) · 0 errores de consola. - 2026-08-19 — D-SB.1bis FIRMADA (salió de F0):
shift-navigatese estampa en elprovider(el shell que cruza como unidad), post, sinqueue, emitido sin anclar desde elonclickde la fila;contact-activatequeda 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) yfield-langs(provider). «Reach forregimelast» (sema.md). §2 del plan queda enmendado por esta línea.