astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
515 Commits (257a2e0804145d33c83e036befc3f497a467b12f)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
3fe8b974a6 |
uix(color-picker): temable — 51 % → 75 %, y un prefijo ABREVIADO fuera de todo el árbol
20 claves de costura (54 → 71) y cinco nombres renombrados. Global del eje 60 % → **61 %**. `--cp-*` VIOLABA theming §6 r5 («no abrevies el nombre del componente»). Cinco nombres —`checker`, `checker-cell`, `current-color`, `channel-gradient`, `trigger-color`— y **45 ocurrencias en 8 ficheros**, tres de ellos de SOMA: el provider los estampa inline, su test los lee y sus tipos los declaran. Renombrados a `--_color-picker-*` con `git ls-files` delante; dos de ellos —el damero y el gradiente de canal— ya derivaban de un público, así que el censo los reclasifica solo. Precedente: los `--gb-*` / `--_ntp-*` de F2-A. Cosido: el ritmo del componente, la tinta de la etiqueta, la tipografía del disparador y la del VALOR —que es metadato: mono, regular, tinta e interlineado propios—, el borde de la muestra, el raíl del canal y su pulgar, el hueco del anillo de la muestra y el borde del cuentagotas. UNA SOMBRA QUE SON DOS Y HACEN UNA COSA: el pulgar del canal lleva contorno + elevación en un solo `box-shadow`, así que se acuña como UN token y la regla de foco lo compone con su tercer anillo, en vez de repetir las dos sombras. EL INSTRUMENTO NO VEÍA DOS FAMILIAS DE NODO, y las dos se arreglaron midiendo: las muestras del picker **son `<ColorSwatch>`** (llevan `data-color-swatch`, no `data-color-picker-*`) y sus sliders de canal **son SLIDERS** (el pulgar y el raíl llevan `data-slider-*`). Cuatro tokens vivos leían muertos por eso. LO ROJO QUE QUEDA ES ANTERIOR A ESTE COMMIT: el guard reporta 23 sin adjudicar —las muestras, la rueda de tono (`hue-0..360` + `saturation-floor`), el ancho del panel por talla, el cuentagotas y las dos de transición—, todos del bloque que ya existía. Antes eran **24**, así que la cifra baja. Su pase de medición queda anotado en §13 con dos causas ya descartadas: el panel SÍ abre y sobrevive al blur, al parqueo del ratón y al forzado de `data-size`, y el guard ya ve sus nodos. Lo que este commit añadió está medido: dos adjudicadas montando el disparador de muestra dentro del panel abierto, el resto en vivo. Gates: sonda antes/después **0 diffs en 1.350 valores** (8 estados), medida DOS veces —una tras el renombrado, otra tras la costura— · tests de soma del color-picker **7/7** tras el renombrado · censo --only 75 % · `--names` sin desviadas · component-audit PASS · eidos-lint 0 invalid · suite eidos con el único rojo conocido · rtl:check 0 · docs:check 0 · `check` con 72 errores globales y 0 en los ficheros tocados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
97a6f03d76 |
uix(text-gradient): temable — 67 % → 100 %, y la ficha se equivocaba en las TRES
7 claves en una entrada NUEVA de `recipes/base.ts`. Censo 67 % → **100 %** (10 knobs, 10 públicos, 0 literales). Global 2.634 → 2.638 públicos. Tercero de la familia text-* con la forma de text-circular / text-focus: cuatro públicos «de facto» consumidos con fallback y **ninguno declarado**, más tres literales. Sin eje `size` ni privados propios, así que declarar no puede matar una escala. LO QUE LA PROPUESTA §4 NO ACERTABA — y se resolvió midiendo contra el CATÁLOGO, no contra el CSS de partida: - **`weight` era deriva de nombre.** El catálogo tiene 77 claves `font-weight` contra 4 `weight` a secas — y dos de esas cuatro las escribí ayer en esta misma familia. Pasa a `font-weight`, y su valor al paso de escala que ya pintaba: `500` ES `var(--font-weight-medium)`. - **El `calc(100% - 2px)` del hueco no son dos knobs de eje físico** (la ficha los marcaba con ⚠ «pártelo en ejes lógicos») sino UNO: los 2 px son el trazo del marco asomando a cada lado. El knob es `border-width` —con `var(--border-width)`, el paso `thin`, no un literal `1px` igual a un paso de la escala— y el hueco DERIVA de él. - **`blur(4px)` va a la escala de blur.** `blur` es familia MÉTRICA del eje `scaling` (theming §23): un literal es ciego al zoom global. `--blur-sm` es `calc(4px * var(--scaling))` — el mismo píxel al 100 y el correcto al 90/110. Precedente: `dialog`/`drawer.overlay-blur`, también backdrop-filter. Y el shorthand físico `padding: var(--space-1) var(--space-2)` se parte en ejes lógicos (`border-padding-block` / `-inline`), que es la normalización firmada el 2026-07-06; el público de facto `border-padding` era un knob con dos ejes dentro. FUERA DEL CONTRATO POR NATURALEZA: `--text-gradient-duration` y `--_text-gradient-play-direction` los escribe el wrapper INLINE desde `animationSpeed` y `yoyo`. Ningún token gana a un estilo inline, así que declararlos sería acuñar dos nombres que mienten — misma clase que los cuatro ausentes de `knob`, el `item-gap` de `carousel` y el `preview-z` de `drag-drop`. INSTRUMENTO — dos puntos ciegos arreglados ANTES de medir (la lección de F2-B aplicada por adelantado, no después): - La demo arranca con `showBorder=false`, así que sonda y centinela veían **2 nodos** y NINGUNO de los tres tokens del marco. La sonda gana un mapa `DEMO_VARIANTS` (chips de variante re-pulsados tras CADA recarga — recarga una vez por talla) y el centinela una entrada `openWith` que no abre un panel sino que ENCIENDE UNA VARIANTE: uso nuevo del mismo gancho, sin tocar su lógica. - La sonda no leía `backdropFilter`, que `KNOB_PROPS` del censo sí cuenta y el centinela ya leía: un token de fondo podía moverse entre las dos corridas sin que el diff lo viera. Con la propiedad y la variante: **4 nodos**. Hover: 0 nodos medidos, y es correcto — las cuatro partes son `<span>` y el único efecto de hover es `animation-play-state`, que no es knob. LO QUE ABRE (→ next-features §13): **la escala de `font-size` no tiene `2xl`/`3xl`/`4xl`** —es `xxs..xxxl`— y tres sitios los referencian: el token `text-focus.word-size` que subí ayer (`var(--font-size-4xl, 3rem)`, pintando el fallback: segundo fantasma de la clase `--radius-xs`, y en el contrato) y los escenarios de las demos de `text-gradient` y `text-blur`, que sin fallback dejan la declaración INVÁLIDA y enseñan un efecto tipográfico a 14 px. Y `--uix-font-size-*` no existe: 37 referencias en el harness del sitio, cero declaraciones. Ninguno se toca aquí: se miden, se anotan y van a su commit. Demo: pestaña **Tokens** (`TokensPanel`, contrato vivo) y fuera la tabla de tokens escrita a mano de la pestaña API — era una segunda fuente de verdad y este renombrado la dejaba obsoleta al instante. Gates: sonda antes/después **0 diffs** en 720 valores × 7 estados (4 nodos) · centinela **7/7 sin una sola excepción** · censo --only 100 % · --names 7/7 conformes · component-audit PASS · eidos-lint 0 invalid · suite eidos con el único rojo conocido (`skin-media-player`, lint.test.ts) · rtl:check 0 · docs:check 0 · `check` con 72 errores globales y **0 en los ficheros tocados**. Prettier: los seis ficheros editados ya estaban sucios en HEAD (el repo tiene 2.392), no se reformatean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ab64efee77 |
uix(knob): temable — 29 % → 75 %, y un nombre que significaba dos cosas
33 claves en una entrada NUEVA de `recipes/base.ts`. Censo 29 % → 75 %. El knob ya tenía siete públicos «de facto» —consumidos como `var(--knob-x, <default>)`— pero **ninguno declarado**, así que un tema no los veía en `getRecipeTokens()`. Tokenizar ha sido sobre todo DECLARAR lo que ya se consumía, y separar dos cosas que compartían nombre. 1. **Cuatro `--knob-*` quedan FUERA a propósito**: `progress`, `angle`, `start-angle` y `sweep` los escribe el provider INLINE desde el estado del control (`knob-provider.svelte.ts:358`). Canal de valor, no superficie de tema \u2014 ningún contrato gana a un estilo inline. Misma clase que `carousel.item-gap` y que la `z` del preview de drag-drop que retiró la revisión adversarial. 2. **⚠ `--knob-arc-width` significaba DOS cosas con DOS defaults distintos**: el inset de la cara del dial (`--space-3`, líneas 80 y 113) y la separación superior del puntero (`--space-2`, línea 165). Un tema que lo escribiera movía las tres declaraciones a la vez, pero **sin tema cada una valía algo distinto** \u2014 «un nombre que no distingue lo que debería», que es la definición de colisión que usa el propio generador. Separado en `arc-width` (la cara) y `pointer-inset` (el puntero), cada uno con su valor verbatim: cambia la superficie de tema, **no el píxel**. 3. **Las coordenadas por talla SUBEN al TSC, y no era opcional.** El default por talla vivía en un privado y el público era un override que iba DELANTE (`var(--knob-gap, var(--_knob-gap))`): declarar `--knob-gap` en el contrato **habría matado la escala**, porque ganaría siempre. Con el patrón del resto del eje (coordenadas + resuelto por `data-size`) el default sigue vivo y el tema alcanza. Eje sm|md|lg|xl, sin xs; el diámetro es `control-height × 2` en cada talla, proporción del bundle conservada verbatim. `--_knob-value-digits` sigue privado: es el número de dígitos del valor máximo, dato del consumidor. Artefactos y gates: - Sonda antes/después = 0 diffs en 754 valores × 7 estados. Determinismo verificado. - R-5.4: 25/33; los ocho mudos adjudicados con su medición (la demo no monta etiqueta \u2014 0 nodos contados \u2014 ni estado de arrastre; forzados, los ocho mueven). - censo --only 75 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos 439/440 (el rojo conocido) · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (33 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
471ba96d25 |
uix(menu-dial): temable — 0 % → 25 %, y las ranuras de la capa que NO se acuñan
3 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 25 %. **La receta posee sólo el cromo del dial**, como dice su propia cabecera: las acciones son `Button` compuestos y la colocación flotante es de la capa `viewport-placement`. Lo suyo son tres cosas: el hueco del abanico, el paso del escalonado de entrada y el tinte del velo. **`--_viewport-placement-offset` y `-z` NO se acuñan, y ésa es la decisión del commit.** Son las RANURAS DE OVERRIDE que la capa compartida publica, y esta receta escribe en ellas su decisión — 20px en vez de los 16px por defecto de la capa, y el comentario del CSS deja claro que es decisión y no deriva. Acuñar `menu-dial-placement-offset` sería el vocabulario paralelo que la regla 2 de capas compartidas prohíbe: el eje es de la capa, y un tema lo mueve ahí. Es el mismo techo que `listbox` paga por consumir bien `list-surface` — el grueso del 75 % que no alcanza, y es correcto que no lo haga (§13). **El escalonado es tema, no motion**: `stagger-step` no es una duración de animación sino el INTERVALO entre hermanos (multiplica el índice de cada acción), y es lo que da carácter al abanico. Por eso entra al contrato. Artefactos y gates: - Sonda antes/después = 0 diffs en 1.856 valores × 8 estados. - R-5.4: 1/3, los dos mudos adjudicados con su medición — el velo sólo existe con el dial abierto (0 nodos en reposo, contados) y el escalonado sólo se aplica bajo `[data-stagger]` con la lista abierta; forzando ambos atributos con índice 3, alcanza (0.084s → 2.331s = 3 × 777ms). - censo --only 25 % · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (3 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
9f24a31eb9 |
uix(mark): temable — 0 % → 60 %, y 3/3 en el guard
3 claves fusionadas en el bloque `mark` de `recipes/base.ts` (donde ya vivían los dos forwards de paleta). Censo 0 % → 60 %, cero globales y cero literales. **No tiene ni un knob de tipo, y es deliberado**: un resaltado hereda la tipografía ambiente del párrafo en que vive (`font: inherit`, `color: inherit`). Lo que posee es la decoración — el padding en sus dos ejes y el radio. **El padding es em-relativo y deliberadamente diminuto** (0.16em / 0.04em): el tinte tiene que leerse SIN romper el interlineado del párrafo que lo contiene, y eso es exactamente lo que esos dos números protegen. Queda escrito en el README para que quien los mueva sepa que está tocando el ritmo vertical del texto, no sólo el grosor del resaltado. Los dos forwards de paleta siguen privados (THM-2): el fondo tiñe con el rol y la tinta sólo cuando hay `data-color`, porque el defecto es `inherit`. Son el 40 % que no alcanza. Artefactos y gates: - Sonda antes/después = 0 diffs en 174 valores × 7 estados. - R-5.4: **3/3**, sin una sola entrada en el ledger. - censo --only 60 % · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (3 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
1f692629e5 |
uix(scroll-frames): temable — 0 % → 33 %, porque es LAYOUT y no cromo
4 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 33 %. **Es layout, no cromo** — la receta lo dice de entrada y la medición lo confirma: una pista alta cuyo viewport pegajoso fija el medio a sangre mientras la pista rueda. Lo único que pinta es el rótulo de carga, y ése es el contrato entero: su separación del borde inferior, el tipo, el tracking y la tinta. **`--_scroll-frames-viewport-block-size` se queda privado**: es la escotilla que el consumidor sobrescribe para encajar el viewport en un contenedor (`100%`) en vez de en la ventana (`100vh`), y su fallback es geometría de LAYOUT, no un valor de tema — quien lo mueve es el sitio de uso, no el tema. Con los `100%` del medio y de la pista (identidad, D-TH.2), el `line-height: 1` del rótulo y el `opacity` del sistema, forman el 67 % que no alcanza, y es correcto que no lo haga. Artefactos y gates: - Sonda antes/después = 0 diffs en 580 valores × 7 estados. - R-5.4: 0/4, los cuatro adjudicados con su medición — el rótulo sólo existe mientras se decodifican los fotogramas (0 nodos una vez cargado, contados) y forzándolo los cuatro mueven (24→1234px, 12→99px, 1.2→9px, rgb(1,2,3)). - censo --only 33 % · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (4 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
151e5159b8 |
uix(clipboard): temable — 0 % → 63 %, todo en la píldora de «Copied!»
6 claves fusionadas en el bloque `clipboard` de `recipes/base.ts` (donde ya vivían los dos forwards de paleta). Censo 0 % → 63 %. **Todo lo tokenizable es el INDICADOR.** El host va `display: contents` y el trigger reusa la receta de `Button` por el patrón de consumidor, así que la receta sólo posee la píldora que aparece un instante tras copiar. - Los dos forwards de paleta siguen privados (THM-2): el fondo y la tinta del chip los leen a pelo, sin sitio donde meter un público sin duplicar la regla — son el 37 % que no alcanza. - `font-family` queda fuera por la misma razón que en code-block, kbd y announce: lo posee la capa tipográfica. - El padding es un shorthand de dos valores → dos knobs lógicos. - **Los `--motion-*` no son knobs de tema**: son los ganchos con que el indicador afina el preset `scale-fade` del canal de motion (escala 0.85, rápida). Pertenecen al canal, no al contrato, y la política de `prefers-reduced-motion` la emite el generador. Artefactos y gates: - Sonda antes/después = 0 diffs en 1.392 valores × 8 estados. - R-5.4: 0/6, los seis adjudicados con su medición — el indicador sólo existe un instante tras copiar (0 nodos en reposo, contados) y forzándolo los seis mueven su propiedad (gap 4→1234px, padding 2/8→1235/1236px, radio 9999→1237px, tipo 12→99px, peso 500→123). - censo --only 63 % · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (6 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
d568577eb7 |
uix(announce): temable — 0 % → 88 %, y 9/9 en el guard
9 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 88 %, cero
privados y cero literales.
**Es un SERVICIO de región viva ARIA, no un visual**: su host va
`display: contents` y la región está oculta por defecto. Todo el cromo del
contrato pertenece al caso minoritario en que el consumidor pide
`visuallyHidden={false}` y la región pasa a ser también un banner en pantalla —
padding en sus dos ejes, radio, el par anchura/color de borde, fondo, tinta,
tipo e interlineado.
- **`font-family` queda fuera a propósito**: lee `--style-label-font-family`,
la capa tipográfica, y un consumidor no re-declara el vocabulario de su capa
(mismo criterio que en code-block y kbd). Es el 12 % que no alcanza.
- **La regla oculta no se toca**: soma escribe el `style` en línea del sr-only
cuando `visuallyHidden` está activo, y la receta lo dice explícitamente. No
hay nada que tematizar ahí, y meterse sería competir con soma por el mismo
nodo.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 377 valores × 7 estados.
- R-5.4: **9/9**, sin una sola entrada en el ledger.
- censo --only 88 % · component:audit PASS · suite eidos sin rojos nuevos ·
rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con «Talla y tema» y tab `Tokens` en la demo (9 claves).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
7565eff47b |
uix(label): temable — 0 % → 100 % con dos claves
2 claves fusionadas en el bloque `label` de `recipes/base.ts` (donde ya vivía
el forward de paleta). Censo 0 % → **100 %**.
**Seis de sus ocho knobs son SISTEMA y no se acuñan.** `label` es uno de los
seis primitivos tipográficos: sus seis ejes consumen `--style-label-*` a través
de la escotilla por instancia (`var(--_label-{eje}, var(--style-label-{eje}))`),
la forma que D-TH.2-b fijó y la que hace que un `<Label>` y un
`<Text style="label">` se lean idénticos. La capa los posee; re-declararlos
sería el vocabulario paralelo que la regla 2 prohíbe.
Quedan DOS knobs propios, los que el primitivo añade por su cuenta: el `gap` al
icono o indicador que puede acompañar al texto, y `disabled-fg`, la tinta
cuando el consumidor marca la etiqueta como deshabilitada junto a su control.
`--_label-color` sigue privado: conmutador de dos fuentes (eje de tinta en
línea / forward de paleta cuando llega `data-color`), el mismo patrón ya
resuelto igual en `code`, `textarea`, `spinner` y `skeleton`.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 174 valores × 7 estados.
- R-5.4: 1/2. `disabled-fg` adjudicado: la demo no monta ninguna etiqueta
deshabilitada (0 nodos, contados); forzada, alcanza (rgb(1,2,3)).
- censo --only 100 % · component:audit PASS · suite eidos sin rojos nuevos ·
rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con «Talla y tema» y tab `Tokens` en la demo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
a6d4d4f5a3 |
uix(skeleton): temable — 0 % → 45 %, y dos privados que son conmutadores
9 claves fusionadas en el bloque `skeleton` de `recipes/base.ts` (donde ya vivía el forward de paleta). Censo 0 % → 45 %, cero globales. Nueve de sus once knobs pasaban por privado, y la lectura correcta es que **dos de esos privados NO son deuda: son conmutadores**. `--_skeleton-bg` y `--_skeleton-highlight` tienen DOS fuentes cada uno según haya `data-color` — la mezcla neutra cross-mode (un `color-mix` hacia `neutral-solid`, para que el placeholder siga siendo un gris visible en los dos modos: el `neutral-track` desnudo desaparece sobre una superficie clara) o el forward de paleta. Aplanarlos exigiría duplicar cada regla por color, así que se quedan — mismo criterio que el borde de foco de textarea y la tinta de spinner. Son el grueso del 55 % que no alcanza, y es honesto. - **Una coordenada por talla** (`size`) gobierna el alto del bloque, el diámetro del círculo (vía `aspect-ratio`) y el hueco entre líneas de la forma `text`. Ese hueco es `calc(size * 0.5)`: una PROPORCIÓN, construcción, no un knob. - **El radio es por FORMA, no por talla**: `radius-md` en `rect` y `50%` en `circle`, donde el `50%` es identidad (un círculo es redondo) — el knob es sólo el del rectángulo. La línea de la forma `text` lleva el suyo. - `duration` entra al contrato en vez de seguir en `1.4s` a pelo, igual que se hizo con el spinner (§13). Artefactos y gates: - Sonda antes/después = 0 diffs en 203 valores × 7 estados. - R-5.4: 8/9. `line-radius` adjudicado: sólo la forma `text` renderiza `[data-skeleton-line]` y la demo monta `rect` (0 líneas, contadas); forzada la forma, alcanza (4 → 1234px). - censo --only 45 % · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens` en la demo (9 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
e3f39ce282 |
uix(virtual-grid): temable — 0 % → 73 %, gemelo exacto de virtual-list
12 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 73 %, cero globales y cero privados. Es el **gemelo exacto de `virtual-list`**: mismo chasis, mismos valores, misma escala. Se tokeniza igual, y en su propio commit — un componente, un commit. **La talla mueve SÓLO el tipo del cromo**: las dimensiones de cada celda salen de las props (`rowSize` / `columnSize`), no de esta escala, así que hay UNA coordenada por talla y el `radius` está DESACOPLADO a propósito (modelo Radix, constante en las cinco). La escala tipográfica va un paso por debajo desde lg (xs→xs, sm→sm, md→sm, lg→md, xl→lg) y lee al bundle. El resuelto declara `parts: ['root']` porque el `data-size` se estampa en `[data-virtual-grid-root]`, un wrapper de eidos. Surface y outline comparten anchura y color de borde → un par de knobs. El `transparent` de ghost y los `100%` del viewport son identidad de variante y de layout: el 27 % que no alcanza, y es correcto que no lo haga. Artefactos y gates: - Sonda antes/después = **0 diffs en 20.010 valores** × 7 estados (111 nodos: la demo renderiza la ventana entera de celdas). - R-5.4: **12/12**, sin una sola entrada en el ledger. - censo --only 73 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con «Talla y tema» y tab `Tokens`; como en su gemelo, la demo no ataba referencia al stage y se ata una para que el panel lea los computados vivos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
070145786f |
uix(virtual-list): temable — 0 % → 67 %, y 12/12 en el guard
12 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 67 %, cero
globales y cero privados.
**La talla mueve SÓLO el tipo del cromo**, y eso acota el contrato antes de
nombrar nada: las dimensiones de cada item salen de las props
(`itemSize` / `estimateSize`), no de esta escala — lo dice el propio comentario
de la receta. Así que hay UNA coordenada por talla, `font-size`, y el `radius`
está DESACOPLADO a propósito (modelo Radix: constante en las cinco). Acuñar
`radius-{k}` habría sido vocabulario muerto.
La escala tipográfica va un paso por debajo desde lg (xs→xs, sm→sm, md→sm,
lg→md, xl→lg) — el patrón de `table`, `tree-grid` y `tree-view` — y lee al
bundle, no al primitivo crudo. El resuelto declara `parts: ['root']`: el
`data-size` se estampa en `[data-virtual-list-root]`, un wrapper de eidos.
Surface y outline comparten anchura y color de borde → un par de knobs. El
`transparent` de ghost y los `100%` del viewport son identidad de variante y de
layout (D-TH.2): son el 33 % que no alcanza, y es correcto que no lo haga.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 3.451 valores × 7 estados.
- R-5.4: **12/12**, sin una sola entrada en el ledger.
- censo --only 67 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por
fichero limpio.
- README con «Talla y tema» y tab `Tokens` en la demo. La demo no ataba
ninguna referencia al stage, así que se ata una: sin ella el panel no puede
leer los computados en vivo (lo cazó `npm run check`, no una suposición).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
e4bcd505d1 |
uix(code): temable — 0 % → 100 %, y el harness que también le pisa el radio
6 claves fusionadas en el bloque `code` de `recipes/base.ts` (donde ya vivía el
forward de paleta). Censo 0 % → **100 %**: no queda un knob de apariencia fuera
del contrato.
**Seis de sus trece knobs son SISTEMA y no se acuñan.** `code` es el primitivo
tipográfico canónico: sus seis ejes de tipo consumen `--style-code-*` con la
escotilla por instancia (`var(--_code-{eje}, var(--style-code-{eje}))`), la
forma que D-TH.2-b fijó, y un primitivo no re-declara el vocabulario de su
capa. Lo que entra al contrato es el CROMO de las variantes `soft` y `outline`.
- El padding es em-relativo a propósito (la píldora crece con el código que
envuelve) y lo declaran IGUAL las dos variantes con cromo: un knob por eje,
no cuatro.
- `--_code-color` sigue privado: conmutador de dos fuentes (tinta de contenido
en línea / forward de paleta cuando llega `data-color`), mismo patrón que el
borde de foco de textarea.
- El `margin: 0` / `padding: 0` / `background: transparent` / `border: 0` de la
base es RESET: la variante `plain` es texto desnudo por identidad.
EL HALLAZGO — el harness pisa también a `code` (→ next-features §13, ampliado):
Medido con CDP: `[data-uix-docs] code` declara `font-family`, `font-size`,
`padding` y `border-radius` y gana **(0,1,1)** contra la regla BASE de la receta
**(0,1,0)**. En la página de documentación el radio del componente es el del
sitio (3px), no el suyo (4px). Sus reglas de VARIANTE (0,2,0) sí ganan, así que
el efecto es selectivo y por eso sólo `radius` lee muerto. **No es un caso
aislado**: es la misma familia de reglas del harness que tapa el `<pre>` de
code-block con siete propiedades. Cualquier decisión sobre esa regla cubre los
dos componentes.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 174 valores × 7 estados. Determinismo
verificado.
- R-5.4: 0/6, los seis adjudicados con su medición — `radius` por el harness
(CDP), los otros cinco porque la demo monta sólo la variante `plain`, sin
cromo; forzando las variantes, los cinco alcanzan.
- censo --only 100 % · component:audit PASS · rtl:check 0 · docs:check 0 ·
check por fichero limpio · suite eidos sin rojos nuevos.
- README con «Talla y tema» y tab `Tokens` en la demo (6 claves).
Nota de proceso: el fichero de excepciones llegó a quedar con sintaxis TS
inválida por un apóstrofe sin escapar, y la comprobación que hice lo dio por
bueno porque el `$?` capturado era el del `grep` de la tubería, no el del
guard. Corregido, y re-verificados los ONCE componentes con ledger comprobando
el código de salida REAL: cero fallos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
d3089e880c |
uix(link): temable — 0 % → 36 %, y ese techo bajo es la respuesta correcta
5 claves fusionadas en el bloque `link` de `recipes/base.ts` (donde ya vivían los dos forwards de paleta). Censo 0 % → 36 %. **Un enlace en línea hereda casi todo del texto que lo rodea**, así que su superficie de tema es pequeña por diseño. Sus dos escotillas por instancia (`--_link-font-size`, `--_link-line-height`) tienen como fallback `inherit`, y ese `inherit` es SEMÁNTICO: el enlace toma la tipografía del párrafo en el que vive. Convertirlo en token exigiría un público con valor `inherit`, que en `:root` no significa lo mismo — se quedan privadas. Con los dos forwards de paleta, ésos son los cuatro privados que forman el grueso del 64 % restante. Lo tokenizable es la GEOMETRÍA: radio del anillo, hueco del glifo externo, tamaño del glifo (`0.85em` en las dos dimensiones → UN knob) y el grosor y desplazamiento del subrayado, cada uno declarado en las DOS reglas que lo dibujan (`always` y `hover`) con el mismo valor. Las tres opacidades son sistema; el `fill: currentColor` del glifo es identidad, sigue a la tinta del enlace a propósito. **La cascada no se toca.** El CSS lleva dos correcciones de especificidad ya medidas y explicadas en su sitio (el `inherit` condicional, para que un `color` pedido por el consumidor no se descarte en silencio; y el `:active` al final, porque una pulsación es siempre también un hover). El diff de computed confirma que tokenizar no las altera. Artefactos y gates: - Sonda antes/después = 0 diffs en 203 valores × 7 estados. Determinismo verificado. - R-5.4: 2/5, los tres mudos adjudicados con su medición. Ninguno es deuda: el glifo externo no se monta en la demo (0 nodos, contados) y el enlace arranca en `data-underline='hover'` sin puntero encima, así que NO HAY subrayado que mover (`text-decoration-line: none`, `thickness: auto`). Forzando el estado, los tres alcanzan. - En el mismo pase, `text-decoration-thickness` y `text-underline-offset` entran en la lista de propiedades del guard, que no las miraba — verificado que los otros DIECISÉIS componentes con ledger no cambian. - censo --only 36 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio · README con «Talla y tema» y tab `Tokens` (5 claves). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
adb77863cd |
uix(kbd): temable — 0 % → 86 %, y 13/13 en el guard sin una sola excepción
13 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 86 %. 1. **`font-family` NO se acuña**: lee `var(--style-code-font-family)`, la capa tipográfica, y un consumidor no re-declara el vocabulario de su capa — precedente inmediato, `code-block`. El `font-size` SÍ entra, y la diferencia está medida: su fallback era un PRIMITIVO, no la capa. `--_kbd-font-size` es la escotilla por instancia que el wrapper escribe desde la prop `size` (`kbd.svelte:37`), y el fallback pasa a ser el público. 2. **El padding y el min-width son em-relativos a propósito** — una tecla crece con su letra —, así que son knobs con valor `em`, no literales pendientes de corregir. El `line-height: 1` sí es identidad: una tecla no tiene interlineado. 3. **El relieve de tecla física es de la variante `surface`**: borde inferior más grueso + luz interior. Dos knobs propios, no adorno del borde general; outline y ghost son planas por identidad. Artefactos y gates: - Sonda antes/después = 0 diffs en 522 valores × 7 estados. Determinismo verificado. - R-5.4: **13/13**, sin una sola entrada en el ledger — el primero de la tanda que no necesita adjudicar nada. - censo --only 86 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con su «Talla y tema» y tab `Tokens` en la demo (13 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
650d76aa0e |
uix(drag-drop): temable — 0 % → 89 %, casi todo en el fantasma del arrastre
10 claves en el bloque `drag-drop` de `recipes/base.ts` (fusionadas con `preview-z`, que ya estaba, y los dos forwards de paleta). Censo 0 % → 89 %, cero globales. **Siete de sus dieciséis knobs YA eran sistema** y no entran al ratio (las opacidades de fantasma y deshabilitado, los dos anillos de foco y el anillo `inset` de los droppables), así que los puntuables eran nueve — seis de ellos en el PREVIEW, el fantasma que sigue al puntero. 1. Los dos forwards de paleta siguen privados (THM-2): el fondo del `[data-dragover]` los lee a pelo y no admite un público sin duplicar la regla — es el 11 % que falta. En cambio el `border` y el `box-shadow` del preview los COMBINAN con una anchura y una sombra que sí son knobs propios, así que esas dos declaraciones entran al alcance. 2. El `--ring-inset-*` de los droppables no se toca: es vocabulario del anillo del SISTEMA y la receta se limita a alimentarlo con su acento. Acuñar un `droppable-ring-*` sería vocabulario paralelo sobre un transversal. 3. El `padding` del preview es un shorthand de dos valores → dos knobs lógicos. Artefactos y gates: - Sonda antes/después = 0 diffs en 1.827 valores × 7 estados. Determinismo verificado. - R-5.4: 0/10 en vivo, y los DIEZ adjudicados con su medición. No es un instrumento roto ni tokens muertos: el preview se crea sólo mientras hay un arrastre en curso (0 nodos en reposo, contados), y forzando el nodo los diez mueven su propiedad — padding 8→1234px, radio 6→1236px, borde 1→9px, fondo, tinta, familia, tamaño 14→99px, sombra y z 100→4321. - censo --only 89 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con su «Talla y tema» y tab `Tokens` en la demo (10 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
7a0fc2eadb |
uix(link-preview): temable — 0 % → 100 %, y el plano overlay que traía tipografía
33 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → **100 %**: no queda un knob de apariencia fuera del contrato (los tres restantes son sistema — anillo de foco y los dos del plano de profundidad que pintan la flecha). Global 48 %, sin contrato 42 → 41, al 100 % 7 → 8. 1. **Los per-talla llevan `parts: ['content']`**, no el host: el panel está PORTALADO y su `data-size` vive en el content, así que una cascada colgada del host no lo alcanza nunca. La talla mueve sólo dimensiones físicas (padding, min-width, tipo); familia, peso e interlineado son constantes a propósito, como ya decía la receta. 2. **La flecha no se tokeniza**: su fill y su stroke consumen el plano `overlay` para casar siempre con el panel; acuñar `arrow-fg` rompería justo esa garantía. El subrayado del trigger es un `color-mix` sobre `currentColor` — la mezcla ES el aspecto, así que el knob es la declaración entera, no el 35 %. Y el fallback encadenado del color del trigger viaja verbatim: la coma es parte del valor, no un segundo knob. EL HALLAZGO — el plano `overlay` impone TIPOGRAFÍA (→ next-features §12.9): `[data-depth='overlay']` (`generated/base.css:7088`) no declara sólo fondo, borde y sombra: también `font-family` y `line-height`. El panel estampa ese plano para su elevación, así que la declaración del plano empata a (0,1,0) con la de la receta y **gana por orden de carga** (la hoja generada va después del chunk de la receta). Medido con CDP `getMatchedStylesForNode`: el par `font-family` / `line-height` de esta receta **no había pintado nunca**. Retirado con **0 diffs**, que es la prueba. Afecta a los **veinte** componentes que estampan el plano. Lo que hay que decidir —aparte, mueve píxel— es si un plano de profundidad tiene competencia sobre la tipografía o sólo sobre la superficie. Cuarta ocurrencia de la clase «empate resuelto por orden de carga». CÓMO SE MIDIÓ, porque el instrumento estándar tampoco servía aquí: El panel abre en HOVER con retardo y está portalado → la sonda genérica ve **1 nodo**, y una sonda que compara un nodo pasa en falso. Sonda dirigida (abre por hover, captura los cinco nodos reales en las cinco tallas): determinismo verificado y **gate antes/después = 0 diffs sobre 550 valores**, repetido tras la retirada. El guard R-5.4 pasó de **3/35 a 31/33** con dos extensiones: `openBy: 'hover'` para este componente (un hover card no se abre con clic — clicar su `<a>` navega) y `text-decoration-color` en la lista de propiedades, que faltaba pese a estar en `KNOB_PROPS` del censo. Verificado que los otros QUINCE componentes con ledger no se mueven ni una cifra. Los dos tokens que siguen mudos son un conflicto ESTRUCTURAL, no un hueco de demo: para abrir el panel hay que tener el puntero en el trigger, y el hover es justo el estado cuya regla pisa la tinta y el subrayado de reposo — el token de hover lee vivo en la misma corrida. Adjudicados con esa razón. Gates: censo --only 100 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio · README con «Talla y tema» y tab `Tokens` en la demo (33 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
42a3e74136 |
uix(spinner): temable — 0 % → 70 %, y 71 tokens propuestos que eran 18
18 claves en el bloque `spinner` de `recipes/base.ts` (fusionadas con los dos forwards de paleta). Censo 0 % → 70 %, cero globales. Global 48 %, sin contrato 44 → 43. **La §4 proponía 71 tokens para 23 knobs — la peor inflación que ha dado el generador en todo el eje.** El spinner tiene UNA coordenada de talla (`--spinner-size`) que gobierna el anillo, la pista de puntos y las barras, más el trazo (`--spinner-thickness`). La propuesta multiplicaba esa única coordenada por variante × parte × dimensión × talla y producía `ring-track-width-xs`, `dots-track-gap-xs`, `dots-dot-width-xs`, `bars-track-gap-xs`… todos con el mismo `0.75rem`, porque todos SON el tamaño en xs. Un tema habría necesitado veinte claves para cambiar un número. 1. Las proporciones son CONSTRUCCIÓN, no knobs: `calc(size * 0.3)` es el punto, `* 0.18` la barra, `* 0.2` y `* 0.12` los huecos. Definen la FORMA de cada variante y se quedan en la receta — mismo reparto que los gradientes de las guías de tree-grid o el damero de gradient-builder. 2. `fg` / `dots-dot-bg` / `bars-bar-bg` ⚠ no eran tres knobs con dos valores en pugna: son UNA variable (`--_spinner-color`) leída en tres sitios, con el conmutador `data-color='inherit'` que la cambia de forward de paleta a `currentColor`. Se queda privada, igual que `--_spinner-track`, que además envuelve su fuente en un color-mix distinto por rama (60 % / 25 %). Aplanarlas exigiría duplicar cada regla por color. 3. La escala es PROPIA, no la del bundle de iconos: medido, 12/16/20/28/40 px contra los 14/16/18/20/32 de `--icon-size-*`, y sólo coinciden en sm. Verbatim con la desviación anotada — forzar el bundle habría movido el default en cuatro tallas de cinco. 4. `duration` entra al contrato en vez de seguir en `0.9s` a pelo: el registro ya tenía abierta la incoherencia de que el spinner de feed ganó tokens de duración y el de media-player no (§13). Aquí se hace bien de entrada. El 30 % que falta son los dos privados conmutadores, los dos `border-radius: 50%` (identidad de forma) y los fotogramas de los @keyframes. Artefactos y gates: - Sonda antes/después = 0 diffs en 377 valores × 7 estados. Determinismo verificado. - R-5.4: 14/18. Los cuatro mudos, medidos uno a uno y adjudicados: la demo monta SÓLO la variante ring y sin etiqueta (0 barras, 0 puntos, 0 labels, contados), y `track-opacity` sólo se declara dentro de `prefers-reduced-motion` — forzando variante, etiqueta y contexto reduce, los cuatro alcanzan. - censo --only 70 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio. - README con su «Talla y tema» y tab `Tokens` en la demo (18 claves, en vivo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
c56d6544e6 |
uix(card-group): temable — 0 % → 90 %, y dos privados abreviados que mueren
20 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 90 %, cero
globales y cero privados. Global 47 %, sin contrato 45 → 44.
Es un CASCO de composición y su propia cabecera ya lo decía: compone
ToggleGroup, Collapsible, Card y Button, y la receta posee «ONLY the genuinely
CardGroup-level visual». Eso acota el contrato antes de nombrar nada — el cromo
de las tarjetas es de Card, el del título de disclosure es de Button.
1. **`--_cg-pad` y `--_cg-card-radius` eran privados ABREVIADOS** (§6 r5 los
prohíbe: `cg` por `card-group`), el mismo defecto que los `--_mp-*` de
media-player. Pasar a públicos con el nombre completo es a la vez el
renombrado que la regla pedía y la tokenización. La §3 de la ficha decía
«la receta no declara privados propios»: los declaraba en su bloque raíz
(`card-group.css:15-16`), y el escáner no los veía por buscarlos con el
prefijo completo del componente.
2. **`--shape-outer-radius` NO se acuña: se CALCULA.** La receta lo compone
como `card-radius + padding` para que las tarjetas aniden concéntricamente
(§30). Un tema mueve las dos coordenadas y el radio exterior se recompone
solo; el censo ya lo clasifica como sistema.
3. **El eje `size` es sm|md|lg, no las cinco tallas**
(`Extract<Size, 'sm' | 'md' | 'lg'>`): seis coordenadas por talla, no diez.
Escribir xs/xl habría sido vocabulario muerto.
4. Los tres `padding` son shorthand con una parte fija y otra variable; lo que
varía y no es el inset del grupo es el hueco inferior →
`title-padding-block-end` y `description-padding-block-end`.
5. El chevron es geometría propia (dos bordes girados 45°): `chevron-size`
cubre las dos dimensiones y `chevron-width` el trazo. Su `currentColor`
hereda del título a propósito y no es knob.
6. Los `font-size` van al BUNDLE (`--size-{k}-font-size`, alias 1:1) y no al
primitivo crudo — misma trampa que cayó en code-block: la propuesta copia el
CSS verbatim y hereda el incumplimiento cuando el valor de partida ya
violaba `recipe-css-contract`. Aquí se aplicó de entrada.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 2.581 valores × 7 estados. Determinismo
verificado: dos corridas, 0 diffs.
- R-5.4: 15/20. Los cinco mudos son los del encabezado ESTÁTICO, que la demo no
monta (0 nodos `[data-static]` contra 1 `[data-button]`, CONTADOS); forzado
`data-static` alcanzan los cinco, y cada uno lleva su medición en el ledger.
El hueco de demo queda registrado en next-features §13, misma clase que el
`kind='month'|'year'` de date-range-picker.
- censo --only 90 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check
0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
354d166df1 |
uix(code-block): temable — 0 % → 81 %, y la demo que no enseña el componente
20 claves en una entrada NUEVA de `recipes/base.ts` (no tenía). Censo 0 % →
81 %. Global 46 % → 47 %, sin contrato 46 → 45.
El handoff lo llamaba «el más mecánico de los que quedan». No lo es: sus 26
knobs se parten en dos grupos con doctrinas distintas.
1. **El nodo del código NO acuña nada, y la §4 pedía que lo hiciera.**
`[data-code-block-code]` consume la capa tipográfica `--style-code-*` en sus
cinco declaraciones, y las dos que pasan por privado usan la forma canónica
de la escotilla por instancia
(`var(--_code-block-font-size, var(--style-code-font-size))`, escrita por el
wrapper desde la prop `size` y sólo si llega). Es el patrón que D-TH.2-b
fijó para heading/text/code. Acuñar `code-font-family` y compañía sería el
vocabulario paralelo que la regla 2 de capas prohíbe. Todo lo que entra al
contrato es el CASCO alrededor.
2. **Los tres `font-size` iban al primitivo crudo, y lo cazó un guard.** La
propuesta copia el valor del CSS verbatim, y ese valor ya violaba
`recipe-css-contract` («recipes consume the size bundle, not the raw
size-coordinate primitives»): `title/lang/copy-font-size` corregidos a
`var(--size-{k}-font-size)`. Los alias son 1:1 → 0 diffs tras aplicarlo.
Aviso general: cuando el CSS de partida ya incumple el contrato, la
propuesta generada hereda el incumplimiento.
3. Shorthand partidos: los dos `border` y el `border-bottom` de la cabecera
comparten anchura y color → `border-width` + `border`; el `padding` del
`<pre>` → `pre-padding`. Los `transparent` de outline/ghost y el `border: 0`
de ghost son identidad de variante.
El 19 % que falta son esas cinco de la capa, que el censo cuenta como `global`
porque `code-block` no está en `TYPOGRAPHIC_PRIMITIVES` — es un compuesto que
CONTIENE un primitivo tipográfico, no uno de ellos. Extender la clase `system`
del censo a las capas sigue pendiente de tu firma; la métrica penaliza hacer lo
correcto y no se fuerza de oficio.
DOS HALLAZGOS QUE NO SE ARREGLAN AQUÍ (→ next-features §13):
- **La demo de `code-block` no enseña el `code-block`.** `[data-uix-docs] pre`
(`web/routes/uix/uix.css:801`) pesa (0,1,1) contra los (0,1,0) de
`[data-code-block-pre]`, y `[data-uix-docs]` es el shell de TODAS las
páginas. Siete propiedades del `<pre>` las pinta el harness y no la receta:
padding (16px 20px contra 12px), border (1px contra ninguno), radius (8px
contra 0), background (gris contra transparente), font-family, font-size
(13 contra 14) y line-height (18.2 contra 20.3). El fondo, el borde y el
radio que se ven ahí son del sitio. Por eso `pre-padding` lee muerto en el
guard, adjudicado con esa medición. Acotar la regla toca el CSS del SITIO,
que sirve a ~162 páginas: decisión aparte.
- **El guard de claves duplicadas del handoff inspeccionaba el VACÍO.** El
comando documentado usa `grep -oE "^\t…"`, y `grep -E` lee `\t` como una `t`
literal: 0 coincidencias sobre 130 bloques reales, así que salía «vacío = OK»
sin mirar nada — el antipatrón que el proyecto tiene registrado, dentro del
propio handoff. Comprobado con un contador fiable: cero duplicados en el
árbol, así que no hay daño en los commits que confiaron en él. CONTINUE
queda corregido con una versión que CUENTA los bloques y avisa de que «0
bloques» significa guard roto, no fichero limpio.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.305 valores × 7 estados (dos veces: tras
la tokenización y tras la corrección del bundle). Determinismo verificado.
- R-5.4: 19/20; `pre-padding` adjudicado con las siete propiedades medidas.
- censo --only 81 % · component:audit PASS · eidos-lint invalid 0, class-hooks
0 · suite eidos de vuelta al único rojo conocido (skin-media-player) tras
corregir el bundle · rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
dd188f1255 |
uix(tree-view): temable — 0 % → 92 %, y el velo que tiñe la carpeta entera
46 claves en el bloque `tree-view` de `recipes/base.ts` (fusionadas con el
forward de paleta). Censo 0 % → 92 %, cero globales. Global 46 %, sin contrato
47 → 46. Molde del hermano `tree-grid` allí donde el problema es el mismo.
LA PROPUESTA GENERADA FALLABA EN EL NOMBRE DE CASI TODO LO QUE IMPORTA: el
clasificador toma el PRIMER selector de cada regla como si fuera la parte
pintada, y aquí ese primero es el root o el branch-control cuando el nodo real
es otro.
1. `root-width: 1px` / `root-bg` NO son del root: son la GUÍA de indentación
(el `::before` del branch, una línea vertical). → `guide-width` + `guide-fg`,
literalmente los nombres de tree-grid. Un `root-bg` que tiñe una línea es la
clase de nombre que miente.
2. `branch-control-padding-inline-{k}` ⚠ fundía DOS ejes que se SUMAN en el
mismo calc: el ritmo de la fila y el PASO DE PROFUNDIDAD
(`--tree-depth × indent + padding`). Fundidos, tematizar la sangría habría
movido el padding. → `row-padding-inline-{k}` + `indent-{k}`.
3. El prefijo `branch-control-` es erróneo: la regla reza
`[data-tree-view-branch-control], [data-tree-view-item]` — las DOS filas. El
CSS ya llamaba `row` a sus privados. → `row-*`, como tree-grid y como la
lección de table. Y fuera el prefijo `root-` del chasis: el wrapper ES el
componente.
4. `row-padding-block` ⚠ no eran dos valores en pugna sino cinco por talla: xs
colapsa a `0`, las otras cuatro `--space-1`.
5. `branch-indicator-width` + `-height` son UN knob (`1em`) →
`branch-indicator-size`.
Bundle: la altura de fila casa 1:1; padding e indent llevan ritmo propio; la
tipografía un paso por debajo desde lg — verbatim, como table y tree-grid. Los
resueltos con `parts: ['root']` (el `data-size` va en `[data-tree-view-root]`,
wrapper de eidos, no parte del morfo). Los `transparent` de outline/ghost y el
`padding: 0` de ghost son identidad de variante y siguen literales.
`hover-row-bg` SÍ se acuña, al revés que en grid-list, y por medición: la misma
regla pinta dos nodos con arquetipo distinto, y `branch-control` NO lleva
archetype — ahí el plano de la receta es la única pintura (velo medido ausente)
y el token es toda la superficie de tema del hover. Tener un consumidor
legítimo es lo que lo distingue del caso grid-list.
EL DEFECTO QUE LA MEDICIÓN DESTAPÓ (preexistente, NO se toca aquí →
next-features §12.8): al pasar el ratón por la fila de una carpeta abierta se
tiñe LA CARPETA ENTERA, hijos incluidos. `archetype: 'item'` está declarado en
el `<li>` branch (`morfo/components/tree-view.ts:103`), que contiene el control
Y todo el `branch-content`, mientras la fila que el usuario señala es el
`branch-control` de dentro: el nodo velado mide 336 px frente a los 36 px de la
fila — medido y capturado. Un branch anidado acumula además el velo de sus
ancestros, así que sus filas se ven más oscuras que las hermanas. Mover el
arquetipo es morfo y mueve píxel en toda la demo. Es la TERCERA variante de la
misma familia: table lo tiene en fila Y celda, grid-list en fila Y celda sobre
el mismo nodo, y tree-view en un ANCESTRO del nodo señalado.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 8.062 valores × 7 estados.
- R-5.4: 45/46; `disabled-row-fg` adjudicado en el ledger (la demo monta CERO
filas deshabilitadas, contadas; forzada alcanza oklch(0.61 0 0) → rgb(1,2,3)).
- censo --only 92 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (46 claves, en vivo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
893aeaf817 |
uix(textarea): temable — 0 % → 80 %, y el contador que casi pierde su 0.85
37 claves en el bloque `textarea` de `recipes/base.ts` (fusionadas con el
forward de paleta que ya estaba). Censo 0 % → 80 %, con **cero globales y cero
literales de valor**: los 16 privados leen ya su público. Global 46 %, sin
contrato 48 → 47.
LO QUE LA MEDICIÓN CAMBIÓ. La §4 generada proponía 35 tokens; cuatro eran
colisiones y uno habría movido el default:
1. `count-font-size-{k}` × 5 BORRABA el factor 0.85. El CSS calcula
`calc(var(--_textarea-font-size) * 0.85)` y la propuesta ponía el tipo del
input crudo en las cinco tallas: el contador habría crecido un 18 %.
D-TH.5 lo prohíbe. Queda en UN token con el `calc` sobre el público del
input — la escala se escribe una vez y la talla la resuelve el input. Pide
`scope: 'host'`: emitido en `:root` su dependencia no existe, y el contrato
de scope de la receta lo caza (lo cazó).
2. `input-fg` ⚠ eran DOS knobs (texto y placeholder) → `input-fg` +
`input-placeholder-fg`. Igual `count-fg` → `count-fg` + `overflow-count-fg`.
3. `input-border` ⚠ NO eran dos knobs sino UNA variable con su swap de paleta,
y el nombre estaba mal: es el borde de FOCO → `focus-input-border`. El de
reposo es otro token, que la propuesta ni listó por viajar en el atajo.
4. `ghost-input-border` se RETIRA: la regla ghost lee ese mismo conmutador —
sería un valor con dos nombres.
Bundle, medido: la tipografía casa 1:1 en las cinco (lee `--size-{k}-font-size`);
el spacing NO — `padding-inline` casa salvo en xs (8px vs 6px) y `padding-block`
sólo en xs. Verbatim y documentado: un campo multi-línea respira más, y el
spacing queda fuera del guard del bundle. Resueltos sin `parts` (el `data-size`
va en `[data-textarea]` y el input hereda); fuera los cuatro `[data-size]`.
DOS ARREGLOS DEL INSTRUMENTO, los dos por medición y no por sospecha:
- **El guard medía con el PUNTERO encima.** `reopen()` corre antes de CADA
token y hace clic en `[data-{c}-input]` cuando el componente no tiene parte
`content`; el `blur()` de la revisión adversarial quitaba el foco pero no el
puntero, así que `:hover` casaba toda la corrida y toda regla de hover pisaba
a sus vecinas. Costaba TRES falsos negativos aquí. Con `mouse.move(0,0)`
tras cada blur, `input-border` revive solo (31 → 32).
- **`::placeholder` SÍ se lee con `getComputedStyle`.** La nota de §13 que
decía lo contrario nunca se comprobó; medido, el color centinela vuelve tal
cual. Entra en el snapshot (32 → 33) y deja STALE la entrada
`command.input-placeholder-fg`, que el guard señaló y se ha borrado.
Corridos los DIEZ componentes con ledger tras los dos arreglos: ningún otro
cambia, un único STALE, el previsto.
Y un hallazgo de píxel que NO se toca aquí (→ next-features §13): el borde de
foco sólo se ve con Tab. Las tres reglas de `border-color` del input están
ordenadas al revés de lo que significan — hover (0,4,0) > foco (0,3,0) >
invalid (0,2,0) — y hacer clic deja el puntero encima por definición, así que
gana el hover; un campo inválido pierde su borde rojo al enfocarlo o al pasar
el ratón. El anillo de foco del sistema sí se ve siempre, así que no es fallo
de accesibilidad. Arreglarlo mueve píxel.
El 20 % que falta son las dos declaraciones que consumen
`--_textarea-border-focus`, privado a propósito: es el conmutador de una
variable con dos fuentes (el público y el forward THM-2), y aplanarlo exigiría
duplicar la regla por color. Mismo techo que `listbox` paga por consumir bien
su capa. Mi propio veredicto predecía ~100 % y la medición lo corrigió a 80 %.
Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.247 valores × 8 estados (el 0.85 intacto,
que era el riesgo). Determinismo verificado: dos corridas, 0 diffs.
- R-5.4: 33/37; los 4 mudos adjudicados en el ledger con su medición (foco,
invalid y las dos del contador rebasado — estados que la demo no monta).
- censo --only 80 % · component:audit PASS · eidos-lint invalid 0, class-hooks 0
· suite eidos sin rojos nuevos (el conocido skin-media-player) · rtl:check 0
· docs:check 0 · check por fichero limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (37 claves; el panel
muestra el contador computando `calc(calc(1rem * 1) * 0.85)`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
60b5b98969 |
uix(grid-list): temable — 0 % → 85 %, y una selección que nunca pintó
38 claves nuevas en el bloque `grid-list` de `recipes/base.ts` (que ya existía
con los dos forwards de paleta: se FUSIONA, no se añade un segundo). Alcance
del censo 0 % → 85 % (22 de 26 puntuables); global 45 % → 46 %, sin contrato
49 → 48.
El eje `size` se estampa en el PROVIDER —el wrapper de eidos no tiene parte
`root`—, así que los tres nombres resueltos van SIN `parts`: molde `command`,
no `table`/`tree-grid`. Fuera los cuatro bloques `[data-size]` del CSS; los
emite el TSC. Altura de fila 1:1 con el bundle, `row-padding-inline` con ritmo
propio, y la tipografía un paso por debajo de md en adelante — verbatim, como
table y tree-grid.
LO QUE LA MEDICIÓN CAMBIÓ (la §4 generada tenía un error de doctrina y dos
defectos que sólo aparecieron midiendo; el §5 Veredicto se escribió con la
sonda delante, porque estaba vacío):
1. La selección de fila NUNCA HA PINTADO. La receta seleccionaba
`[data-grid-list-row][data-selected]`, un atributo que ni el morfo declara
ni soma estampa (estampa `data-state='selected'` + `aria-selected`): la
regla no casaba jamás y su acento `--_grid-list-palette-element` no llegó
nunca al píxel — una fila seleccionada muestra el velo NEUTRO del
arquetipo, indistinguible del hover (capturado). Retirada la declaración
muerta y su forward huérfano: 0 diffs, que es la prueba. Repararlo mueve
píxel y choca con §12.5 (el arquetipo fija también `color`) → §13, espera
firma. El acento del checkbox (`_palette-solid`) sí vive y se conserva.
2. NO se acuña `hover-row-bg` aunque la propuesta lo pedía. La fila es
`archetype: 'item'`: el velo del sistema la pinta a especificidad plena y
el plano de la receta pierde — medido, el hover pinta DOS VECES sobre el
MISMO nodo (a diferencia de table/tree-grid, donde velo y plano caen en
nodos distintos). Sería un token que no puede ganar; mismo criterio que
listbox.highlighted → §12.7. Y fila Y celda llevan ambas `item`, así que
bajo el puntero se apilan tres capas.
3. «Consume la capa compartida `menu-indicator`» es FALSO. Sale de
`SHARED_LAYERS` (`theming-census.ts:432`), pero esa capa no tiene un solo
selector de grid-list, la receta no la importa y soma no renderiza
indicator: el checkbox ESPEJA el visual pintándolo por su cuenta. Entrada
desviada del censo → §13 (listbox / menubar / navigation-menu están en la
misma entrada y tampoco aparecen en el fichero).
Nombres, sobre la propuesta: `selection-checkbox-{width,height}` son UN knob →
`selection-checkbox-size`; `on-selection-checkbox-bg` → `checked-selection-checkbox-fg`
(es la tinta del glifo, y `on-` es prefijo reservado); `border` partido en
`border-width` + color (molde `command.input-border*`), que el checkbox
comparte; `padding` uniforme se queda `padding` (precedente
`command.viewport-padding`); `max-block` entra al contrato (molde
`command.viewport-max-block`).
Artefactos y gates:
- Sonda antes/después = 0 diffs en 6.090 valores × 7 estados. Determinismo
verificado: dos corridas del mismo código, 0 diffs.
- R-5.4: 37/38 tokens mueven un computed. `invalid-border` adjudicado EN EL
LEDGER con su razón medida (la demo arranca con su control en off; activado
a mano alcanza: oklch(0.9555 0.0207 13.86) → rgb(1,2,3)).
- censo --only 85 % · component:audit PASS (R-5.3 incluida) · eidos-lint
invalid 0, class-hooks 0 · suite eidos sin rojos nuevos (el único es el
conocido skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero
limpio (72 globales, ninguno mío).
- README con su «Talla y tema» y tab `Tokens` en la demo (38 claves, leídas en
vivo). El tab `Recipe` documentaba la regla retirada: corregido.
Las fichas de calendar / carousel / command / month-grid / range-calendar /
table / year-grid entran porque `--report` regenera el informe entero: sólo
actualizan NÚMEROS DE LÍNEA de commits ya hechos que no lo regeneraron. Cero
cambios de alcance. Las otras 161, que sólo cambiaban la fecha, se revirtieron.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
34a95a550c |
feat(feed): temable — 0 % → 94 %, y cierra el bloque F2-A
Octavo y último del bloque.
censo 0 % → 94 % · global 43 %
computed reposo y las CINCO tallas, idénticas al byte
**El título no escala por talla: escala por NIVEL DE ENCABEZADO.** El informe
leyó sus cuatro valores como una colisión de `size`, y son `aria-level` 3..6 —
un encabezado más profundo es más pequeño porque es más profundo. Sus tokens
son `article-title-font-size-{3,4,5,6}`, no coordenadas de talla. Tercera
corrección de este tipo a un veredicto en el bloque.
**El ⚠ del `sentinel` eran DOS medidas**, como sospechaba el veredicto: la
fila-sonda mantiene un alto mínimo para que el observador tenga algo que
intersecar, y el spinner que va dentro tiene su propio diámetro.
El feed tipa por debajo del 1:1 de md en adelante, como `table` y `tree-grid`;
la coordenada lo nombra en vez de esconderlo.
**Sobre la medición, que aquí hubo que trabajarla.** La sonda dio 7 diffs y
ninguno era un cambio:
- el `box-shadow` del nodo `[data-busy]` está ANIMADO, y la sonda congela
`transition` pero nunca `animation` —a propósito, porque congelarla impide
que Presence monte nada—, así que lo muestrea en una fase distinta cada vez:
dos corridas del MISMO código dan cinco diffs;
- y el estado `hover` se mide después de haber forzado las tallas, así que su
altura arrastra la última.
Lo que decide es que las SEIS medidas estables —reposo y las cinco tallas—
salieron idénticas al byte: 544,5 · 564,5 · 628,5 · 792,5 · 966,5 px. Queda
escrito en el README para que el siguiente no persiga el mismo fantasma.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido
Demo con tab Tokens: 50 filas, 0 sin computar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
4cab776677 |
feat(carousel): temable — 0 % → 77 %
Séptimo del bloque F2-A. censo 0 % → 77 % · global 42 % computed 2.842 valores en 7 estados: 0 diffs **Los botones prev/next no se acuñan aquí** — componen `<IconButton>`, así que su cromo es el del Button y un tema los viste por ahí. Esta receta sólo decide DÓNDE se colocan (`trigger-offset`). El veredicto avisaba de comprobarlo antes de copiar filas de trigger, y estaba en lo cierto. **El ⚠ del informe eran DOS gaps distintos**, no un knob con dos valores: el `gap` en columna de la raíz y el `item-gap` ENTRE DIAPOSITIVAS. Y el segundo sólo se aparta del default en `xs`, así que su cascada declara UN override en vez de cinco coordenadas iguales — el TSC no necesita que se le repita lo que ya hereda. **Los puntos del paginador son geometría propia**: 0.375–0.75rem por talla, ni bundle ni desviación que marcar, porque el bundle no tiene coordenada que signifique «un punto». El punto activo se estira a pastilla con un factor que ahora es token (`active-indicator-scale`): es lo que lo hace leer como «estás aquí» y no como un punto más gordo. **El radio interior es concéntrico**, no un segundo radio: viewport y diapositivas redondean por dentro del marco, así que su esquina es la del marco menos el borde — el `2px` suelto pasa a `inner-radius-inset`. Las tres variantes (surface / outline / ghost) entran enteras: padding del marco, borde y superficie. Quedan 5 literales de layout (`block-size: 100%` ×3 para que el viewport vertical no colapse, `max-content` del raíl de puntos) y los 2 privados de paleta. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido Demo con tab Tokens: 30 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
3b325c4394 |
feat(listbox): temable hasta donde le toca — 0 % → 68 %
Sexto del bloque F2-A, y el primero cuyo techo es DELIBERADO. censo 0 % → 68 % · global 42 % computed 2.813 valores en 7 estados: 0 diffs **Este componente no puede acuñar su ritmo de fila, y esa es la regla de oro de las capas compartidas.** Altura, padding inline y block, gap y tamaño de letra los posee `lib/list-surface.css`, por talla, para TODAS las superficies de lista —select, combobox, command y los menús—. Acuñar `--listbox-item-height` sería el vocabulario paralelo que las reglas de esa capa prohíben, y encima ganaría a la capa para todas las demás. Los cinco privados ADOPTAN la capa y se quedan privados; un tema mueve ese ritmo por `--list-*`, que es donde vive. Consecuencia honesta: el censo cuenta esos cinco como privados y el alcance se queda en 68 % en vez de ~86 %. **La métrica penaliza hacer lo correcto** — el mismo defecto de medición que D-TH.2-b arregló para los primitivos tipográficos (`--style-*` pasó a `system`). Registrado en `next-features.md` §13; extenderlo a las capas compartidas es decisión, no corrección al paso. **Dos cosas medidas que NO se tocaron:** - El `highlighted` pinta DOS veces: la receta pone un `--color-surface-overlay` plano y encima el arquetipo `item` añade su velo del 8 %. El plano es el duplicado que §38 deprecó, así que se dejó SIN token — bendecir con un nombre público algo condenado a morir es el error que este eje ya evitó en el codemod. Retirarlo mueve píxel. - La tinta de la fila seleccionada era código MUERTO y se retiró: el arquetipo la fija a (0,4,0) contra los (0,3,0) de la receta. Mismo hallazgo que en `table`, verificado igual, 0 diffs al quitarlo. Lo que sí es suyo y ahora es público: la superficie que sostiene las filas (fondo, borde, radio, padding, gap, tipografía, alto máximo), la esquina y la tinta de la fila, el glifo de selección y el grupo con su etiqueta. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido Demo con tab Tokens: 21 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d177ee331c |
feat(gradient-picker): temable — y su trigger resultó estar muerto entero
Quinto del bloque F2-A. censo 0 % → 88 % · global 42 % computed 609 valores en 8 estados: 0 diffs **El hallazgo llegó por el centinela: 36 de 45 tokens no movían nada.** Con esa cifra no se sigue adelante. El trigger es un POPOVER TRIGGER, y `popover.css` le pinta el cromo entero —altura, padding inline, tipografía, color, fondo, borde, radio, hover y foco— desde `[data-popover-trigger]:not([data-archetype='field-trigger'])`, que gana a `[data-gradient-picker-trigger]` en base (0,2,0 contra 0,1,0) y en los estados (0,3,0 contra 0,2,0). Esta receta re-declaraba TODO eso y nada pintaba: la altura, el padding, el tamaño de letra y el radio venían de popover, y con valores distintos —12px contra el `space-2-5` de la receta, 14px contra su `font-size-md`—. Llevaba así desde que el trigger se volvió trigger de popover. Así que no se tokeniza: **se retira**. Un token sobre una declaración muerta es un token que miente, y este eje ya retiró uno por lo mismo en `table`. Retirarlo dio **0 diffs**, que es la prueba de que estaba muerto. Un tema viste este trigger por el contrato de POPOVER, que es composición funcionando. Lo que sí es del picker se queda y alcanza (verificado a mano, porque el chip usa un hook de CLASE que el centinela no ve): el `gap` de la fila, el chip del degradado (tamaño por talla, radio, borde), las tres anchuras del panel, la lista de paradas y la rejilla de presets. **Correcciones al veredicto**, ambas medidas: `--gp-current-gradient` NO lo estampa el wrapper sino SOMA (`gradient-picker-provider.svelte.ts:197`), así que renombrarlo toca otra capa y no es de este commit; y no es un knob de tema sino un canal de valor —el degradado que eligió el usuario—, sobre el que un tema no tiene nada que decir. Las tres anchuras del panel se quedan como tokens planos leídos por tres reglas, no como cascada del TSC: el content va por PORTAL y dimensiona con `data-picker-size` (el atributo de picker-shell), y el vocabulario de scopes no tiene palabra para el atributo de otro componente. Al registro (`next-features.md` §13) van cuatro incidencias nuevas: que un componente compuesto pueda tener el cromo entero muerto sin que nada avise —merece guard, y el instrumento ya existe—, el nombre abreviado en soma, los dos hooks por CLASE que `eidos-lint` cuenta, y el foco opaco del trigger contra la mezcla suave del sistema. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido Demo con tab Tokens: 25 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
9748733adc |
feat(tree-grid): el árbol se vuelve temable — 0 % → 95 %
Cuarto del bloque F2-A. 42 knobs con 29 privados que no derivaban de nada; ahora 47 claves públicas y tres privados, los tres legítimos. censo 0 % → 95 % · global 41 % → 42 % computed 9.744 valores en 7 estados: 0 diffs **Las guías de indentación eran UN color, no un fondo.** Lo que la propuesta llamaba `root-bg-image` son las líneas verticales por `aria-level`: cinco gradientes apilados que dibujan una línea por ancestro. El knob es `guide-fg` (+ `guide-width`); la construcción se queda en la receta — el mismo reparto que el damero de gradient-builder, donde el color es el token y el patrón es de la receta. **Tres privados sobreviven, y por razones distintas**: `--_tree-grid-columns` no es un knob de tema sino un CANAL de layout que el wrapper escribe inline desde la definición de columnas del consumidor; `--_tree-grid-palette-element` es el forward THM-2; `--_tree-grid-stable-rows` es dato del consumidor. **El hover de fila es no determinista, y viene de antes de este eje.** Dos hallazgos medidos: 1. En filas CON BANDA el hover está muerto: la regla de `striped` pesa (0,7,0) contra los (0,5,0) del hover y usa el atajo, así que en las pares no ocurre nada al pasar el ratón. 2. En las demás, quién gana depende del ORDEN DE CARGA: el velo del arquetipo `item` y el hover de la receta empatan EXACTAMENTE a (0,5,0). Seis corridas de la misma configuración dieron cuatro veces «sin velo» y dos «con velo». Eso segundo obligó a parar y caracterizarlo: al ver 3 diffs tras un cambio trivial (un `border-width` a token) lo primero que hice fue sospechar de mi cambio; comparar dos corridas del MISMO código dio los mismos 3 diffs, así que no era el código sino un empate de especificidad resuelto por orden — la misma clase de fragilidad que el canon documenta para las bandas de z-index. No se toca aquí: decidir quién manda mueve píxel y es la decisión pendiente del velo. El guard de huérfanos cazó tres tokens que acuñé suponiendo (`font-family`, `fg`, `surface-bg`): existían en la receta pero leyendo el valor crudo. Ahora los leen, que es justo lo que el eje persigue. audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido `skin-media-player` Demo con tab Tokens: 47 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b7a4cef08f |
feat(media-player): el proyector se vuelve temable — 0 % → 88 %
Tercero del bloque F2-A. 45 knobs sin contrato salvo el acento; ahora 61 claves públicas, **cero privados y cero globales**. censo 0 % → 88 % · global 40 % → 41 % computed 3.277 valores en 7 estados: **0 diffs** **El veredicto pedía renombrar `--_mp-*` → `--_media-player-*`; hacen falta menos pasos que eso.** El prefijo abreviado viola theming §6 r5, cierto, pero al leerlos uno a uno los dieciséis privados eran ALIAS PUROS de su fuente (`--_mp-fg: var(--color-content-primary)`, `--_mp-accent: var(--media-player-accent)`…). Un alias puro no se renombra: se borra. La capa privada entera desaparece y cada knob pasa a público. **El acento firmado NO se tocó**: `accent`/`accent-strong` siguen siendo `--scale-amber-9/10` — acento de proyector theme-stable, que debe leerse sobre cualquier vídeo en claro Y oscuro, con su doctrina escrita en el propio bloque. Verificado en vivo que sigue re-tintando el rango del Slider compuesto. **El scrim: UN color, dos gradientes.** La tira inferior de controles y el degradado del título salen del mismo `--media-player-scrim`; dos tokens dejarían que un tema rompiera la pareja sin querer. **`audio-player` viajaba en el mismo barco y no era obvio**: consume siete de esos tokens sin declarar ninguno. No es fuga entre componentes — es un SKIN sobre las mismas partes (`[data-media-player][data-variant]`), así que están en ámbito. Su receta se migró en el mismo pass. **Lo que no se pudo medir, y se dice**: `audio-player` no tiene página de demo (la ruta da 404), así que la sonda encontró 0 nodos — y una comparación de 0 valores pasa siempre, que es justo la clase de guard que no vale. Se verificó a mano estampando `data-variant` sobre el player vivo: el padding pasó a `6px 12px`, exactamente los tokens nuevos. Hueco de demo anotado en el README. Centinela 36/61. Los 25 restantes, adjudicados por clase: partes no montadas en la demo (captions, buffering, título de audio, paneles portalados), un pseudo-elemento (`::after` del scrim) y los que pintan DENTRO del Slider compuesto — verificado a mano que `--media-player-track` sí llega hasta `--slider-track-bg`. Los 6 knobs que quedan son literales de relleno (`100%`, `max-content`). audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados · suite: sólo el rojo conocido `skin-media-player` Demo con tab Tokens: 61 filas, 0 sin computar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
564133b485 |
feat(table): la rejilla se vuelve temable — 0 % → 86 %
Segundo del bloque F2-A. 46 knobs sin una sola entrada de contrato; ahora 45
claves públicas.
censo 0 % → 86 % · global 39 % → 40 %
computed 13.050 valores en 7 estados: **0 diffs**
**Una trampa esquivada de milagro**: `table` YA tenía bloque en `base.ts` — los
forwards de paleta THM-2. Insertar uno nuevo habría creado un segundo bloque
`table:` y el catálogo lo habría descartado EN SILENCIO (gana el último). Las
claves se fusionan en el existente; el guard de duplicadas lo confirma.
**Dos nombres corregidos, como decía el veredicto**: lo que el privado llamaba
`root-height-{k}` es la altura de la FILA (`row-height-{k}`), y el prefijo
`root-` desaparece del resto — el envoltorio ES el componente y
recipe-contract §1 no repite la parte en ese caso.
**Qué casa con el bundle y qué no**, medido eje a eje: la altura de fila sí
(1:1); el padding NO (la rejilla empaqueta más denso en xs y más suelto de md
en adelante — forzar el bundle cambiaría el default); y la tipografía casa
DESPLAZADA: de md en adelante la tabla tipa un paso por debajo
(`font-size-md` = `var(--size-sm-font-size)`), que es deliberado y ahora se lee
en el propio nombre de la coordenada en vez de esconderse en un primitivo.
**Dos hallazgos del arquetipo `item`, y ninguno es mío** — la fila lo lleva, y
ese velo de `archetypes.css` está a especificidad PLENA (0,5,0), a propósito:
1. **La prop `hoverable` no suprime nada.** Una fila se ilumina al pasar el
ratón aunque el atributo no esté — medido con hover real, y ocurría ANTES de
tocar esta receta. El arquetipo y la prop no se ponen de acuerdo sobre quién
decide: es decisión de morfo, se presenta, no se toca de oficio.
2. **`selected-row-fg` era un token que mentía.** La misma regla fija el
`color` de la fila seleccionada y gana a la receta (0,5,0 contra 0,3,0), así
que el token no podía mover nada nunca. Lo cazó el centinela; retirado con su
declaración muerta. La tinta de la fila seleccionada es del arquetipo.
Y una corrección de rumbo a mitad de camino: al pasar todo a `background-color`
el velo apareció en 3 sort-triggers (su velo sí va en `:where()`, 0,0,0). Eso
es el barrido sistemático del velo, que NO está firmado — así que en los nodos
velados se mantiene el atajo a propósito, con el porqué escrito en la línea.
Primero lo diagnostiqué como regresión mía; la medición lo corrigió.
Los 6 knobs que quedan: 3 privados de paleta (patrón THM-2, capa compartida) y
3 literales de layout/identidad (`inline-size: 100%` ×2, `opacity: 1`).
El test `horizontal-escape` afirmaba el nombre del privado; ahora afirma el
público, que es justo lo que este eje persigue.
audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
suite: sólo el rojo conocido `skin-media-player`
Demo con tab Tokens: 46 filas, 0 sin computar. README con «Talla y tema» y los
dos hallazgos del arquetipo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
90aa6dc5f0 |
feat(demos): la demo enseña el contrato de tokens — `TokensPanel`
Hallazgo del autor mirando `command`: la demo no enseñaba su superficie de tema. Al ir a comprobarlo salió que **ninguna** demo lo hacía — ni las que este eje ya llevó de 0 % a 98 %. El tab `recipe` lista SELECTORES; los tokens no aparecían en ningún sitio, y lo más cerca era el tab `system` de 23 demos, que en `sidebar` enseña 3 de sus 40 claves. Un componente temable cuya demo esconde sus tokens está a medio entregar: la demo es el producto. `TokensPanel` es un panel COMPARTIDO (junto a SemaPanel / MotionPanel / SystemAxes), no una tabla a mano por demo: pregunta `eidos.getRecipeTokens(component)` — la misma entrada de `recipes/base.ts` que alimenta al generador — así que un token añadido, renombrado o retirado aparece aquí sin que nadie se acuerde de tocar la demo. Una lista escrita a mano sería una segunda fuente de verdad, y en este repo ya está medido lo que cuestan. Tres columnas: el nombre, el valor DECLARADO, y lo que COMPUTA sobre el escenario vivo. Esa tercera es la que convierte «es temable» en algo que se ve en vez de algo que el doc afirma. Un knob por talla o por ámbito se muestra como tal en vez de fingir un valor único. Verificado en el navegador con `command`: **57 filas, 0 sin computar** (`--command-input-padding-inline-sm` → `var(--size-sm-padding-inline)` → `calc(8px * 1 * 1)`). Entra en el protocolo del bloque F2-A: cada componente que se tokenice engancha su tab en el mismo commit. Para los 160 que aún no tienen contrato el panel dice justo eso — que todavía no son alcanzables por un tema. check 0 errores en ficheros tocados Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
bd916ef3fb |
refactor(eidos): el catálogo habla un idioma — 269 claves al vocabulario firmado
D-TH.6, ejecutada. El slot de tinta pasa a `fg` y el modificador interactivo
se pone delante, que es lo que theming §6.7 r7 documentaba sin guard desde
que se escribió. Value-preserving: renombra la clave en `recipes/base.ts` y
sus 793 referencias en 101 ficheros; no toca un solo valor.
claves renombradas 269 en 66 componentes
referencias 793 en 101 ficheros
censo antes/después 162 recetas · 5.203 knobs · 1.841 públicos (37 %) · 56
sin contrato — IDÉNTICO, como debe ser un rename
--names antes/después 269 desviadas → 0
Qué NO entra, y por qué:
47 `{rol}-{slot-de-rol}` canónicas: COLOR_ROLE_SLOTS pone el modificador
detrás POR CONSTRUCCIÓN (`primary-solid-hover`)
13 exentas firmadas `--focus-ring-*` es familia del sistema; el color
de `aura` es un sustantivo; `stop-color` ES una
parte de gradient-builder
47 hovers neutros por VALOR: migran a la capa de estado
(§38 + R-4.3), no se renombran — firma 3
6 ocurrencias en historia changelog, errores-toxico, PLAN-affix/background:
reescribir un registro fechado lo vuelve mentira
El clasificador vive en el censo (`--names`), no en un script suelto, para que
el guard R-5.3 consuma la MISMA gramática que el codemod. Su muta-prueba
(`__names-mutatest.ts`, 27 casos) es lo que hizo el trabajo: cazó que yo
promovía al frente CUALQUIER valor declarado por el morfo, y así
`--sidebar-width-icon` (la anchura del raíl colapsado) se convertía en
`--sidebar-icon-width` (la anchura de un icono), que es otra cosa. La firma
dice «delante lo interactivo, detrás lo dimensional y contextual»: ahora sólo
promociona el vocabulario interactivo cerrado, y lo contextual —`below`,
`loaded`, `vertical`, `icon`— se queda donde estaba. Los cinco casos de
regresión están en la muta-prueba.
También cazó que `dropdown-menu.item-bg-hover` lee `var(--color-primary-element)`:
es un hover CON VALENCIA (el palette swap de recipe-contract §2), no el hover
bespoke que §38 deprecó. Clasificar por el nombre lo habría metido en una
migración que no le toca; se clasifica por el VALOR.
Verificación (§7.4, artefacto por paso):
diff de generated/ 269 renombres 1:1 · 0 cambios de valor · 22 privados
reapuntados a los nombres nuevos (`__names-verify-diff`)
computed tag-group · field · tabs: 6.467 valores en 23 estados,
0 diffs — y comprobado en el navegador que sirve el CSS
nuevo, para que ese 0 no sea el de una copia cacheada
huérfanos 0 en código
suite eidos 434 pasan · 1 rojo, el conocido (`skin-media-player`).
`recipe-css-contract` verde: es el guard que caza el
`var()` sin fallback a un nombre que ya no se emite,
o sea el fallo exacto de un rename a medias
component:audit 163 PASS · 3 NEEDS-WORK, los tres SIN TOCAR por esto
check 0 errores en ficheros tocados (los 73 globales son de
otras sesiones de la rama; se atribuye por fichero)
rtl:check 0 · docs:check 0
formato el renombrado no alarga ninguna línea: ningún fichero
tiene más líneas de 100 chars que antes, así que no se
pasa prettier — hacerlo reformateaba 300 ficheros de
deriva ajena
Entra aquí la corrección del repaso de los 7 ya hechos:
`gradient-builder.checker-color` → `checker-fg` (el damero de transparencia;
ahí `color` era slot). Sus `stop-color-*` no se tocan.
Queda para el paso siguiente: R-5.3 no puede graduar a `error` directo
mientras los 47 hovers neutros sigan hablando el idioma viejo — o migran
antes (firma 3, ya firmada), o el guard necesita una exención greppable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
4a27715170 |
uix(section): la sección de una página es un componente, no un esqueleto que cada block reescribe
Los 19 blocks del tier repetían el mismo chasis: un <section {...rest}> escrito a
mano, dentro Section (el aire), dentro Container (la medida) y a veces
Background (la decoración). Medido antes de tocar: containerSize recableado en
16 blocks, sectionSize en 15, decor en 9, y OCHO copias del mismo Header —
Box maxWidth 48rem + Motion viewport + Stack gap 3—, una de las cuales ya había
derivado en silencio: la de article-grid perdió su Motion.
La regla del propio tier dice que una parte se gana el compound cuando SE
REPITE. Aquí lo que se repite son los blocks, así que la repetición se gana un
componente. Y no uno nuevo: ese componente ya existía a medias y se llama
Section. Crece en vez de nacer un hermano — un segundo componente obligaría a
explicar en cada README en qué se diferencia de Section, y la explicación sería
«Section con tres props más».
Section gana, todo opcional: containerSize (compone Container DENTRO, y sin él
los hijos se renderizan igual que siempre), decor (pinta Background.Pattern
como HIJO, para que la banda sangre a todo el ancho por detrás de la medida sin
ganar caja) y la parte Section.Header.
Y renderiza un <section> real. Renderizaba un <div> a propósito, con la nota
«si un consumidor necesita un <section> de verdad para el landmark, que lo
envuelva» — y nueve blocks hacían exactamente eso, pagando un nodo extra para
poder nombrarse. Un <section> sin nombre NO es landmark (se expone como region
sólo al llevar nombre accesible, que es lo que A-109 dejó medido esta misma
semana), así que el tag no ensucia el árbol de nadie y la raíz duplicada
desaparece.
Para eso Box gana `as`, que cierra F21 («los primitivos de layout no pueden
cambiar de elemento»): lista CERRADA de elementos contenedores, nunca void —
es una escotilla semántica, no un prop de tag libre—, default 'div', y el
modelo de caja, las vars y la receta idénticos rinda lo que rinda. Los ≥2 casos
reales que la decisión original pedía para reconsiderarlo llegaron hace tiempo:
la columna de enlaces del pie que debía ser ul/li, y estos dieciséis blocks.
Migrados DOS como prueba, uno por clase: feature-grid (medida) y stats-band
(medida + decoración). Cada uno pierde dos niveles de anidamiento y su
<section> duplicado — medido en navegador: [data-section] ES el <section>, el
patrón 'grid' sigue siendo su hijo, 1280 de ancho, 64px de aire, 1024 de
medida, y el documento pasa de 3 elementos <section> a 2. Los otros 17 van en
tandas aparte.
⚠️ El paso de tipos que costó los únicos 5 errores de esta rama, y que lo
explica una regla ya escrita en el propio types.ts de stats-band: «sub-parts
extend the props of the canon component they wrap, never HTMLAttributes: the
raw attribute surface clashes with the canon's refined style/class when spread
through (the feature-grid lesson)». Ahora la RAÍZ también envuelve un
componente del canon, así que sigue la misma regla: FeatureGridProps y
StatsBandProps extienden SectionProps.
Gates: svelte-check 72/62 antes y después · vitest eidos+blocks 470/471 (el
rojo es skin-media-player, ajeno y documentado) · blocks:check 0/19 ·
docs:check 0/0. Prosa al día: README de Box y de Section, la demo de Section
(que anunciaba la limitación en cuatro sitios) y F21 cerrado en
PLAN-blocks-quality §6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
18165e74ed |
uix(blocks-demos): la vista previa nace con el estado vivo de ActiveUix, no con los defaults
El iframe de la vista previa es OTRO documento con OTRO runtime (BootUix corre
su propio createActiveUix), así que el estado de la sección no cruza el marco:
la rama de 375/768 se abría SIEMPRE en claro, y en 9 demos siempre en LTR, con
la topbar en oscuro y RTL delante (A-29). Dentro del documento los blocks
siempre atendieron a ActiveUix — el shell conduce el framework por
uix.prefs.setIntent y el modeSource de eidos —; lo que faltaba era la única
serialización que existe entre dos runtimes: la URL.
El arreglo va en el ARNÉS, no demo a demo. BlockDemo lee el estado VIVO por la
superficie sancionada del framework — readActiveUixPrefsSlot(uix.prefs,
'direction'|'language')?.get(), las lecturas por dimensión que son rune-backed
(contador $state por dimensión), y eidos.getThemeContext().mode, que lee el
modeSource del shell — y compone la URL final: los props del BLOCK los pone el
demo, los EJES los pone el arnés. El {#key} y el enlace «abrir ↗» pasan a la
URL resuelta, así que mover un toggle global re-monta el iframe con los params
nuevos y el enlace abre lo que estás viendo.
Retirados los 11 controles «dir (solo la vista previa)» de banner, contact,
cta, feature-grid, feature-split, hero, newsletter, site-footer, site-header,
stats-band y team: eran el eje de sección repetido por demo — la regla del
arnés dice que tema, idioma y dirección los da el shell — y no tocaban prefs,
sólo escribían la URL. El shell queda como único dueño de los ejes; una
precedencia demo-gana habría dejado el toggle global inerte en esas páginas.
Dos correcciones a la ficha: testimonials ya derivaba su URL (el literal
desnudo quedó atrás en F2b), y el alcance real era el sistémico que la propia
ficha apuntaba — mode= no viajaba en NINGUNO de los 19 demos y dir= faltaba en
9. La pata lang sigue siendo inerte salvo en contact (blocks.* registrado),
pero viaja igual: es un eje del shell.
Verificado con la topbar REAL de la sección, no con params a mano: en oscuro +
RTL, el iframe de pricing a 375 arranca base-dark + dir=rtl (tinta
oklch(0.9491 0 0)) y el de hero — que tenía control local — hereda igual; en el
estado por defecto los dos vuelven a base-light + ltr. La URL sale con los
props del block primero y mode/dir/lang detrás.
Gates: svelte-check 72/62 antes y después · blocks:check 0/19 · docs:check
0/0. Huella: BlockDemo +31, cada demo −18 (−192/+39 en total).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
48bad6c726 |
uix(feature-split): una sección se nombra por su encabezado, y sus filas cuelgan de él
feature-split era el único block de sección del tier sin parte .Header. Sus
filas emitían h2 por defecto, así que una sección de tres filas aportaba tres
h2 hermanos al outline y la sección misma no tenía nombre (A-110). La página
compuesta lo rodeaba a mano: encabezado escrito como primer hijo y un level={3}
repetido fila a fila.
El h2 de las filas no era un descuido: era una decisión ESCRITA en el README
del block —«.Title emits h2 by default (each row is a section-level statement
of its own)… drop it to 3 when the page already put an h2 above the rows»—. Se
deroga en sitio, y no por gusto sino porque el resto del tier declara lo
contrario: FeatureGrid.ItemTitle, las preguntas de faq y Pricing.PlanName ponen
en level 3 lo que se repite, y el h2 lo lleva el Header.
Nace FeatureSplit.Header, calcado de Testimonials.Header (Box con medida +
Motion + Stack) y sin contexto, porque este block no tiene eje align que
heredar. Y .Title baja su default de 2 a 3. La forma queda coherente en los dos
casos, que es lo que la hace preferible a las alternativas: con .Header el h2 es
suyo y las filas cuelgan; sin .Header la app ya puso el h2 encima y las filas
cuelgan igual. La tercera vía —un contexto donde el Header marcase su presencia
y el Title derivara su nivel— daba el mismo resultado a cambio de un hijo
escribiendo en el contexto del padre, que es el patrón que ya costó un bucle de
$effect en este repo. Por un solo bit, no compensa.
Verificado en navegador, outline leído del DOM:
/blocks/feature-split/preview h2 «Del evento crudo a la decisión»
h3 ×3 (las filas) ← antes: h2 ×3, sin nombre
/blocks/landing h2 + h3 ×2, dentro de un outline de página
que ya no salta de nivel
La demo compone su encabezado y LandingSite deja de hacerlo a mano: fuera el
Stack escrito a pelo y fuera los dos level={3}. Header medido en su sitio (x
144, 768 de ancho: la misma medida que la copia de las filas).
Gates: svelte-check 72/62 antes y después · vitest src/uix/blocks 36/36 ·
blocks:check verde (19 blocks, 147 ficheros) · docs:check 0/0.
Primera fila del ledger arreglada DENTRO del tier, por acotación del eje: las
tres anteriores de la sesión (A-111, A-112, A-109) eran todas de canon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1e3a67ad39 |
uix(banner): la tira de aviso no es la cabecera del sitio, así que deja de reclamar su landmark
Toda página con cabecera exponía DOS landmarks banner: el <header> del
site-header y la tira de aviso, que estampaba role="banner" a mano. ARIA
reserva ese rol para la cabecera DEL SITIO —identidad, búsqueda del sitio— y
dice que un documento debería marcar como mucho uno (A-109).
Las dos salidas que la ficha proponía estaban mal planteadas, y lo destapó una
revisión adversarial de tres refutadores independientes:
- «Estampar el rol ANTES de los rest props, que es la clase naming donde gana
el consumidor» contradice la doctrina que la propia ficha citaba: role es
clase CONTRATO por nombre de atributo (morfo/types.ts:811-832;
ARIA_NAMING_ATTRS contiene sólo aria-label). El runtime siempre gana ahí, y
con razón: un data-state o un role pisables son un componente que miente.
- Y el orden de estampado NUNCA fue el mecanismo. Medido: el segundo landmark
es el <header> del site-header, que no lleva atributo role ninguno — recibe
banner IMPLÍCITO por ser una <header> fuera de un elemento de sección. Un rol
pisable habría dejado quitarlo caso por caso, empujando el arreglo a cada
app, en vez de arreglarlo por defecto.
- El morfo, además, nunca declaró el rol (aria: []): era un literal del wrapper
de eidos que el README daba por contrato de la parte. Drift morfo↔eidos que
se queda sin objeto.
Lo firmado: la tira deja de reclamar el landmark. defaultElement header→section
en el morfo, fuera el role literal. Un <section> es region SÓLO cuando tiene
nombre, así que el aria-label del consumidor la hace encontrable y su ausencia
la deja como contenido plano — que es exactamente lo que el censo de landmarks
de A-111 ya sancionaba por escrito.
⚠️ SIN nombre por defecto, y esta es la corrección que la revisión me obligó a
hacer sobre mi propia propuesta: yo había planteado un default traducido
(«Aviso»), copiando el patrón de skeleton/spinner. Habría convertido TODA tira
en landmark sin salida — /temas/grafito apila cuatro y habrían salido cuatro
region con el mismo nombre, que es la clase de defecto de A-111 con otro traje.
El precedente de <nav> no transfiere: un <nav> es SIEMPRE landmark y por eso
pide nombre; un <section> lo es PORQUE lo tiene. Quien quiera una tira
encontrable, la nombra.
Derogadas por escrito las dos afirmaciones que decían lo contrario, porque
existían y estaban firmadas: el README del canon («Banner is a landmark for
page-level announcements — site header, persistent notice, system status bar»)
y el «✓ ejemplar» que docs/audit/components/banner.md le puso a ese contrato el
2026-07-07. La otra mitad de aquella decisión —no role="alert", el feedback
transitorio es de Toast— sigue en pie y se dice.
Verificado con el árbol AX de Chrome por CDP, servidor limpio:
/blocks/landing banner ['Aviso del producto','Acme'] → banner ['Acme']
+ region ['Aviso del producto']
/blocks/banner/preview banner ['Principal'] · region ['Aviso del producto']
/temas/grafito banner [] · region [] — las cuatro tiras sin nombre
dejan de ser landmarks, que es lo correcto
Píxel idéntico: la receta selecciona [data-banner], no el elemento — tira a
sangre 1280×54 con el mismo fondo, capturada y mirada. Ningún test, guard, CSS
ni consulta del repo seleccionaba por role=banner ni por el tag.
Gates: svelte-check 72/62 antes y después · vitest eidos+blocks+morfo 692/693
(el rojo es skin-media-player, ajeno y documentado) · blocks:check 0/19 ·
docs:check 0/0 (644) · morfo:check PASS en banner. Prosa re-sincronizada en 11
sitios: los README de canon, block y app-shell, la ficha de auditoría, las tres
demos, landing y su README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
ae9e277fad |
uix(navigation-menu): el botón «aparecía» en vez del panel, y la forma del trigger no la podía tocar ningún tema
Dos defectos que el autor señaló en el megamenú, y los dos anteriores a T-1.
1 · EL TRANSLATE. `emerge-open` / `emerge-close` se estampaban en el TRIGGER,
así que la firma genérica `[data-event^='emerge-open'] { animation: present-rise }`
corría sobre el BOTÓN: opacidad 0→1 y 8px de translate en el control, mientras
el panel —la superficie que entra en el campo— no hacía nada. Medido: a +8 ms el
trigger con `present-rise` y `translateY(5.5px)`, a +120 todavía vivo.
La premisa del target era real («el content se monta al abrir, un sello dirigido
a él muere») y la receta A-36 ya la respondía, por eso dropdown-menu, popover,
context-menu y dialog declaran `content` y ninguno declara `trigger`:
`open` es `post` —el flip monta el panel y el runtime difiere el emit pasado
`tick()`— y `close` es `pre` —el panel sigue ahí para anunciarlo—, con
`targetFallback: [trigger]` para la carrera en que ya no está. Resolución por
estado de montaje, DECLARADA, no un fallback a mano en el provider.
navigation-menu era el último que la arrastraba. Ahora el trigger queda sin
evento, sin animación y sin transform en todos los instantes; el panel se lleva
la aparición. La suite lo fija en las dos ramas (content montado · fallback).
Al mover el sello salió un doble que habría enviado: el panel tiene entrada
propia (slide+scale+fade, 120 ms) y la firma se la pisaba durante el hold; al
retirarse el sello, la de la receta arrancaba DE CERO. El panel entraba dos
veces, 320 + 120 ms. Precedente exacto en tooltip («the preset entrance wins
over the global present-rise signature», medido allí el 13-08): la entrada
propia gana por especificidad. Registro de animaciones tras el arreglo: una sola
entrada de 116 ms, nada en el botón.
2 · LA FORMA. El radio del trigger vivía en un PRIVADO (`--_navigation-menu-
radius`) clavado a `--radius-default` —«decoupled from size (Radix model)», una
línea que leyó a Radix como regla cuando Radix no pinta nada—, la altura salía
de `padding-block: var(--space-2)` (un token de ESPACIO como altura de CONTROL:
medido 33,5 / 36 / 41 px en sm/md/lg, sólo `md` en la escala por aritmética
casual) y los rellenos de hover/abierto estaban escritos a mano. El contrato del
componente publicaba tres knobs (content-z, indicator-w/x): ningún tema podía
decir «píldora» sin mover el radio del sistema entero. Por el TSC y por lo que
hacen Button (`--button-radius-*`), dropdown-menu (`item-*`) y el Sidebar de
ayer (`--sidebar-row-*` por talla), eso es un error, y el autor lo firmó así:
themable, default el actual.
Ahora el trigger (y el Link, que comparte su cromo) toma el bundle canónico
`--size-{k}-*` a través de tokens públicos por talla —`--navigation-menu-
trigger-{height,font-size,line-height,padding-inline,radius}` con sus pasos
`-{sm,md,lg}` y el nombre resuelto por `data-size`— más `-bg-active` /
`-fg-active` para el acento de abierto / aria-current. A `md` el default es
exactamente lo que traía: 36 px, 6 px de radio. Medido antes → después: altura
33,5/36/41 → 30/36/44; radio 6/6/6 → 6/6/10. Y un tema decide la forma en una
línea: `--navigation-menu-trigger-radius: var(--radius-full)` en el root → 9999px
en el trigger, medido.
Y el hover: la receta pintaba `background: var(--color-surface-overlay)` con el
SHORTHAND, que resetea `background-image` —donde vive la capa de estado canónica
que `archetypes.css` ya da a todo `trigger` (R-4.3)—. Medido: `background-image:
none` en hover, la capa muerta y una superficie a mano en su lugar; y el trigger
ABIERTO sin ninguna respuesta al puntero (el mismo bug que la fila activa del
Sidebar, ayer). La regla de hover desaparece (la capa ya está) y el acento usa
`background-color`, así que compone encima: medido, hover cerrado = sólo la
capa; hover abierto = acento + capa.
Fuera del alcance y sin tocar: los enlaces DENTRO del panel
(`[data-navigation-menu-content] :is(a, …)`) conservan su `background`
shorthand — otra superficie, otro selector, otro día.
Gates: check 72 con CERO en los ficheros tocados · soma nav 9/9 · sema+morfo
550/550 · eidos 434/435 (el rojo es `skin-media-player`, ajeno, el mismo que
documenta el handoff del sidebar) · eidos-lint 31 morfo-backed / 0 invalid ·
component:audit PASS · rtl:check 0 · docs:check 0/640 · generated/base.css +41
líneas, sólo las del nav.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
aa3f0e3511 |
uix(navigation-menu): navegar no fija nada, y el megamenú que se abre solo no debería sonar
T-1 de PLAN-sidebar.md §6. El `Link` emitía `commit-select + affirm` al navegar.
El libro lo nombra: TABLA 9.3, «Success de navegación — cambio de contexto con
logro», corregido como «shift + commit.fulfill si procede»; cap. 27 §2 «shift no
es commit… ir a otra pantalla no implica que la operación haya terminado»; y la
apertura de cap. 10, «una navegación no es exitosa por cambiar de pantalla».
Nada queda fijado al pulsar un enlace del nav: `aria-current="page"` es la huella
que deja el router DESPUÉS, no un estado que la parte fije.
Ahora habla el par de cap. 22 §9 —«Link → contact.press seguido de shift.navigate:
la presión no es la navegación»—, el mismo que la fila del Sidebar declaró ayer:
- `contact-activate` ANCLADO en el enlace pulsado (parte repetida → `partInstance`).
- `shift-navigate` en el `provider`, sin anclar: `data-event-*` es UNA ranura por
elemento, y el sujeto del cruce es el `<nav>` que cruza como unidad, no el
control que lo inicia. Sólo si el `<a>` tiene `href` — aquí no es un opt, viaja
en restProps, así que se lee del elemento que la parte ya tenía atado.
Esto REABRE la decisión del 2026-08-05 (ledger A-54), no la pisa: mover el evento
fuera del marco estaba bien; la familia era el defecto.
El pack estuvo a punto de borrarse, y esa habría sido la equivocación de la
sesión. Al quitarle la regla del commit se quedaba con dos selectores vacíos y
propuse retirarlo como ruido. El autor paró y pidió releer: la pregunta no era
qué escribe el fichero, sino qué debe decir la aparición del megamenú. Se abre
por HOVER, y medido con el default de familia un barrido del puntero por dos
triggers y salir de la barra emitía `open` · `open` · `close` — tres sonidos, 6
osciladores, sin un solo clic. Cap. 17 §3 («emerge — normalmente no necesita
sonido»), cap. 26 §7, cap. 32 §10-§11, cap. 5 §11. Así que `emerge-open` y
`emerge-close` son SILENT, la postura que `tooltip` y el flyout del `sidebar`
(D-SB.3) ya tienen para el mismo gesto. El pack DIFIERE del default y vuelve a
ser un pack.
Enmienda D.6 de book-deviations.md (PROJECT_CANON): `navigation-menu` sale del
alcance del `commit.subtle`, y quedan corregidas tres cláusulas suyas caducadas
— el `emerge-open/close` declarado desde el 2026-08-05, el `commit.subtle` que
murió en
|
2 months ago |
|
|
aced677a15 |
uix(toolbar): la acción principal de un cluster ya puede decir su rango — y el gesto ya suena
`Toolbar.Button` tipaba contra el ButtonProps de SOMA ({id, disabled}): sin
variant, sin color, sin size. Un cluster de acciones donde UNA es la principal
no tenía cómo decirlo desde dentro, y la demo del app-shell dejaba «Nueva»
FUERA del toolbar como workaround documentado (A-112).
La ficha ofrecía dos salidas y mi primera recomendación fue la equivocada —
declarar el toolbar deliberadamente uniforme y consagrar el workaround. El
autor la tumbó, con razón: el eidos toolbar-button.svelte era un PASSTHROUGH
que dejaba el <button> nativo de soma, exactamente la clase de bug que
dialog-trigger (2026-06-19) y card-group-title (2026-06-27) ya pagaron, y el
precedente correcto no es menubar-trigger sino Form.Submit. Dos datos que el
primer análisis no tenía:
- El botón era MUDO. El morfo del toolbar sólo declara commit-toggle (el
group-item), así que pulsar una acción no emitía nada. Medido tras el
cambio: click → contact-activate estampado en el nodo + 8 nodos de audio,
donde antes 0.
- La uniformidad la dan los DEFAULTS, no la ausencia del eje: ghost/neutral y
el size del toolbar por contexto de eidos (toolbar/context.ts, calcado del
precedente ButtonGroup y con su misma razón: la receta del Button lee
data-size EN el nodo, un descendant selector no llega). El tipo estrecha a
SelectionVariant (solid | outline | ghost).
La forma es el Button consumer pattern entero: composición vía child de soma
(con el rename outerChild contra la recursión), y la receta CEDE el nodo —
cero cromo de botón en toolbar.css (regla 5); Link y GroupItem siguen siendo
superficie de la barra y los pinta ella. Antes de ceder, las dos recetas sobre
un mismo nodo producían un híbrido medido en el que solid/primary NO pintaba.
La doctrina que faltaba queda escrita en guides/component-guide.md §4: dos
clases de parte con forma de botón en una barra. ACCIÓN en barra (Toolbar.
Button, Form.Submit, Dialog.Trigger) compone el Button; control de SUPERFICIE
de barra (Menubar.Trigger: File, Edit) lo pinta la barra. El test: ¿significa
lo mismo fuera de la barra? Esa distinción vivía en un comentario de
menubar-trigger; el comentario ahora apunta a la doctrina.
Verificado en navegador (Playwright, servidor limpio): el nodo ES el Button
del canon (data-button + data-variant), solid/primary pinta (fondo primary,
tinta blanca), paridad exacta con el GroupItem por construcción — mismo bundle
--size-*: 36px/36px en md, 30px/30px en sm, con la herencia del size del
toolbar medida en vivo al cambiarlo. La demo del app-shell mueve «Nueva» AL
INTERIOR del cluster (workaround retirado, gap del block cerrado en su README)
y la demo del toolbar gana la acción «New» con controles vivos action variant
/ action color.
Gates: svelte-check 72/62 (base 73/62, sin regresión) · vitest eidos+blocks
470/471 (el rojo es skin-media-player, ajeno y documentado en el handoff) ·
blocks:check 0 errores / 19 blocks · docs:check 0/0. Los avisos de prettier de
los dos README ya fallaban en HEAD.
⚠️ El árbol compartido tiene ya encima trabajo NUEVO de otra sesión
(navigation-menu, PLAN-sidebar, book-deviations): este commit lleva sólo los
14 ficheros del eje A-112, verificados por lista.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
bae03e55c5 |
uix(sidebar): cuatro cosas que sólo se ven cuando alguien intenta refutarte
Revisión adversarial del eje (seis lentes, un escéptico por hallazgo): 21
hallazgos, 17 refutados, 4 reales. Los cuatro, arreglados.
1. El `MenuBadge` no llegaba nunca a `lg`. Indexaba la tabla de su excepción
por `part`, que ya venía capado en `md`, así que la rama `lg→md` era código
muerto y un sidebar `lg` sacaba el chip un paso pequeño. Ahora indexa por la
talla del sidebar: 26 · 26 · 30 · 36.
2. `emerge-close-sub` se sellaba en un nodo que el mismo tick ocultaba. El
`display:none` de `[data-state='closed']` mataba `dismiss-fade` antes de
pintar un frame y, como el pack silencia ese evento a propósito, la retirada
del flyout se quedaba sin NINGÚN canal: una ocurrencia declarada que no
ocurre. La regla se guarda ahora con `:not([data-event^='emerge-close'])`,
que es justo la ventana que el canal visual mantiene abierta. Medido:
opacidad 0,85 → 0,35 → 0,03 con la animación viva, y el nodo oculto a los
~516 ms, cuando el sello se retira.
⚠️ 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.
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 —lo contrario de lo que el propio párrafo de al lado explica—, y los
dos `emerge-*-sub` no podían encontrarse con la regla que los silencia,
atada a `[data-sidebar-menu-sub]`. `SemaPanel` recibe ahora la acción
(parámetro opcional: las otras 23 páginas siguen con su thunk sin
argumentos) y la página resuelve la parte desde el contrato compilado.
Medido: contacto → una fila, open-sub → el sub, shift → el provider.
4. La fila de Gaps del README de `app-shell` seguía afirmando que «sus morfos
sólo declaran emerge-expand/collapse», que es exactamente lo que este eje
dejó de ser cierto. Reescrita: queda viva sólo la mitad que lo sigue siendo
—`NavigationMenu` emitiendo `commit-select` al navegar, el antipatrón
«success de navegación» del cap. 10 §12—, y se registra por qué se refutó la
lectura «la fila SELECCIONA la sección».
Gates: eidos-lint 49/0 invalid · component:audit PASS · rtl:check 0 ·
docs:check 0/639 · vitest eidos 434/435 (el rojo es ajeno) · suite del sidebar
8/8 · `check` con delta CERO contra la base medida con stash (72/72).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
63b2de03c7 |
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 |
|
|
edf0639eea |
blocks(app-shell): la barra y el raíl no son la misma navegación en dos tamaños, son dos ÁMBITOS
Corrección del autor, y era una equivocación de fondo, no de forma. Yo había escrito que un menú en la barra sería «una segunda navegación dentro de un shell cuya navegación es el raíl». Es al revés: - la BARRA lleva la APLICACIÓN — su menú general, las áreas que tiene, y lo que es de la persona y no de la página (buscar, avisos, la cuenta). No cambia cuando el lector se mueve dentro de un contexto; - el RAÍL lleva el CONTEXTO en el que está — este proyecto, esta tabla: sus secciones y sus acciones. Cambias de contexto y el raíl entero cambia; la barra no se mueve. Es el `TopNav` + `SideNav` de Atlassian y el `TopBar` + `Navigation` de Polaris, que estaban en mi propio dossier. Y es también por qué una app publica DOS landmarks `navigation` legítimamente: la APG pide nombre único en los repetidos, y cada uno nombra un ámbito distinto. La demo modelaba mal: el raíl era el menú general. Ahora la barra lleva marca + `NavigationMenu` de la aplicación (Bandeja · Proyectos · Informes) y el raíl es el proyecto Apollo (Tablero · Conversaciones · Archivos + sus Acciones). El rastro se va con ella: su sitio es la cabecera de la PÁGINA, no la barra — cambia con cada vista mientras la barra no, y una barra que lo lleva crece una segunda fila en cuanto la ruta tiene tres niveles, que es lo que se midió antes de moverlo. De `site-header` se queda sólo la razón que era cierta: no es que sobre una navegación, es que la pieza es de un SITIO — `Sticky` por defecto (una barra de app bajo `scroll="main"` no debe pegarse) y `Container` MEDIDA (una barra de app va de borde a borde). Su forma marca · nav · acciones es la correcta. **Y un defecto de canon que sólo aparece con dos navegaciones**: `NavigationMenu` deja su landmark ANÓNIMO — el `aria-label` se reenvía al `<ul>` y el `<nav>` se queda sin nombre (el morfo lo declara así: «the root delegates its name to the list it wraps»). Un `<ul>` no es un landmark. Medido: `navigation: ['Navegación principal', null, 'Migas de pan']`. No se arregla desde fuera —nombrar la lista no nombra la región, y envolver en otro `<nav>` anidaría landmarks—, así que queda VISIBLE y registrado en los Gaps. Guards: `blocks:check` verde · `docs:check` 0/0 · `svelte-check` sin errores propios (de paso, `NavigationMenu.Item` exige `value` y no se lo pasaba). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db7bcc3a55 |
blocks(hero): la mitad de escritorio no se oculta en el móvil, no existe — y el clip no se pide
F4 del eje de visibilidad por breakpoint: el primer caso real, y la receta
escrita donde un agente la va a buscar — el README del block que la usa.
Medido sobre build de PRODUCCIÓN (`build` + `vite preview`, Chrome), que es el
único sitio donde la afirmación es comprobable: a 375, cero `<video>` en el DOM
y NINGUNA petición de `video.mp4`; a 1280, uno y una (`206 Partial Content`).
En dev es falsa por construcción — el servidor renderiza a ancho 0 y sirve la
variante `base` antes de que la hidratación la pode.
Dos criterios que la receta fija para el tier. La puerta va DENTRO del snippet,
nunca alrededor del `<Hero>`: envolver el block duplicaría título, copy,
formulario y acciones por breakpoint, que es lo que hacen las secciones
por-breakpoint de Framer y la razón de que deriven; la media es lo único que
difiere, así que es lo único escrito dos veces. Y `display="contents"`: una
puerta decide presencia, no añade geometría.
La mitad móvil no es un vídeo más ligero: es NINGÚN vídeo. El Surface con
aurora cuesta cero bytes y sigue la paleta del tema.
⚠️ Hallazgo del canon, medido de paso y NO arreglado aquí: la media dentro de
un `Background.Layer` desborda la capa — el mínimo automático del ítem de
rejilla gana a `block-size: 100%`. Hero a 1280: capa 567px, vídeo 714px. En la
demo del propio canon con `Background.Video`: capa 288px, vídeo 435px.
`min-block-size: 0` lo colapsa exacto en ambos. `overflow: clip` lo escondía,
así que `cover` lleva recortando descentrado sin que se viera. Es del eje
`background`, cerrado el 2026-08-18: queda escrito en §Found while composing.
`launch.json` gana `preview-prod`: la entrada llamada `preview` arranca
`npm run dev`, así que no servía el build y la medida no se podía hacer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
1d91c11e89 |
blocks(app-shell): la demo no usaba los componentes que SIGNIFICAN lo que enseñaba
Rehecha entera. Cada pieza es ahora el componente del canon que corresponde al
acto, no una caja con el aspecto adecuado:
- las tres cifras son `Metrics` (label · value · delta), con la dirección
desacoplada de la valencia: en «sin asignar», BAJAR es bueno (`goodTrend`),
que es justo lo que las referencias hacen mal;
- la bandeja y la actividad son `Feed` — `role="feed"`, cada conversación un
`role="article"` con `aria-posinset/setsize` y su título con `aria-level`,
teclado de feed incluido. Medido: 2 feeds, 11 artículos, 11 encabezados;
- la barra superior es `Toolbar` (`role="toolbar"`, roving tabindex: cinco
controles cuestan UNA parada de tabulación) más `Breadcrumb` con `current`;
- la tipografía sale de los estilos SEMÁNTICOS del tema — `Text
style="label|caption|body"`, `Heading level` + `style` — en vez de tamaños
elegidos a ojo, que es lo que había.
**Y la avería que esto destapó, que era del arnés y afectaba a los 19
previews**: la foundation declara los `@font-face` y emite los estilos
(`--style-body-*`…) pero no los aplica a ningún elemento — un `Text` ancla su
familia en su recipe y todo lo demás HEREDA del documento. El arnés de blocks no
anclaba nada, así que `html`, `body`, `[data-sidebar]` y cada fila del raíl
computaban **Times New Roman** al lado de los `Text` en Instrument Sans. El
shell de docs sí lo ancla (`uix.css`); éste no. Anclado en `_lib/reset.css` con
los tokens del tema, no con una familia escrita. De paso, el sangrado de 40px
que el navegador pone a `dd`.
Tres huecos del canon que salieron al componer, registrados en los Gaps:
1. **`Sidebar.MenuButton` es mudo y `NavigationMenu.Link` no** — el mismo acto
con dos respuestas. El README del `Sidebar` firma que «navegar una fila es
nativo y no suena», pero entonces el `NavigationMenu` contradice la firma; y
en un shell la fila casi nunca navega: SELECCIONA la sección. Es de sema, no
del tier.
2. **`Toolbar.Button` no acepta `variant`/`color`** — tipa contra el
`ButtonProps` de soma (`{id, disabled}`), así que un cluster no puede decir
cuál es su acción principal. Aquí va al lado, como en las referencias.
3. **No existe `description-list`** y el panel de detalle es exactamente el
«primer detail-view real» que su ficha F5 pone como disparador.
Guards: `blocks:check` verde · `docs:check` 0/0 · `svelte-check` sin errores
propios (dos props inexistentes corregidas de paso: `Icon.Filter` y el
`variant` del `Toolbar.Button`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
026e07beaa |
blocks(app-shell): la demo estaba escrita a mano, no con la capa visual
La escribí sin haber leído la doctrina de eidos y salió lo que sale así: `<div
style>` donde hay primitivos, `inline-size: 16rem` clavado, `font-size` en un
`<strong>`, bordes y fondos a pelo, y contenido de relleno («Bloque de contenido
1»). Rehecha contra la tabla «Build contract» de `guides/component-guide.md`.
Ahora **cero literales y cero `style=`** en el fichero: color por token de rol,
espaciado por `--space-*` a través de los props de layout, tipografía por la
escala de `Text`/`Heading`, paneles por `Card`, separaciones por `Separator`,
anchura del aside por `--sidebar-width` (la medida del propio sistema, que
respeta densidad y `--scaling`).
Dos props que no hacían NADA, encontradas al reescribir:
- `Badge intent="affirm"` — `Badge` no tiene `intent`, tiene `color`.
- `AutoGrid minChildWidth="var(--container-width-sm)"` — le pasé un breakpoint
como suelo de pista, así que las tres tarjetas caían en una columna. Ahora
`Grid columns={{ base: 1, sm: 3 }}`, sin literal ninguno.
Y una tercera cosa medida que NO se arregla desde aquí: el aside llevaba
`Surface variant="soft"` y es **inerte** — `oklch(0.9911 0 0)` contra un fondo
de página de la misma claridad, o sea invisible en claro. Es **F15** de
`PLAN-blocks-quality.md` §6, gap de canon ya registrado («soft no acota un
panel, y Surface no tiene borde»). La frontera la pone un `Separator`, que es lo
que sí acota, y el porqué queda escrito en el fichero para que nadie lo
«arregle» con un fondo a mano.
Verificado mirando las capturas, claro y oscuro, 1280 y 375.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
fd02780002 |
uix(box): ocultar por CSS descarga el vídeo igual, y evitarlo pedía no tener SSR
`visibleFrom` / `hiddenFrom` no ocultan: no renderizan. El subárbol no está en
el DOM, sus efectos no corren y su media no se pide — la mitad que
`display={{ base: 'none' }}` no puede dar. Los once contenedores que componen
Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface,
AspectRatio, Float) la heredan sin tocar sus ficheros. La puerta es un
`$derived` sobre `eidos.dom.isAtLeast(bp)` y un `{#if}`: sin atributos nuevos,
sin generador, sobre los breakpoints que configura el app.
Ninguna referencia lo hace así. Mantine (`hiddenFrom`/`visibleFrom`), Chakra y
Panda (`hideFrom`/`hideBelow`), Framer y Webflow ocultan por CSS y dejan el DOM
montado — Webflow lo documenta como coste de ancho de banda y Framer llega a
duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una
puerta JS pinta la variante equivocada en el HTML (MUI devuelve un `matches` por
defecto en el primer montaje; Vuetify #17252 reporta los saltos de layout), y el
precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.
Aquí ese precio no existe: el proyecto se despliega como SPA (`adapter-static` +
`fallback`, ninguna ruta con `prerender = true`), así que el primer render
ocurre en el navegador con el `innerWidth` real — el build es un `200.html` de
1,8 KB, shell puro. El que SÍ se paga está escrito en el tipo: cruzar un
breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,
`display`. Sólo el dev server renderiza en servidor (ancho 0) y empieza a
descargar la media de la variante `base` antes de que la hidratación la pode:
queda como caveat de desarrollo en el README.
`base` sale del tipo en ambas props: `visibleFrom="base"` sería «siempre» y
`hiddenFrom="base"` «nunca renderiza», y ninguna de las dos es una cosa que una
prop deba poder decir.
Card, Banner, ScrollArea y Sticky rinden su propia raíz y NO heredan la puerta:
queda como gap diferido con la regla del `as` (≥2 casos reales). El puente de
SSR queda escrito con su disparador, no construido — hoy sería un atributo que
nadie lee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
6a53c3dc99 |
blocks(app-shell): la pieza que posee las alturas de la página, y por eso el aviso deja de tapar la cabecera
F3.1 del tier, con su fase 0 firmada delante. El block que faltaba: el que POSEE las regiones de una página de aplicación — sus landmarks, sus enlaces de salto y sus alturas. **A-95 se cierra por construcción, no compensando.** Bajo `scroll="main"` no hay nada fijo: el aviso es una fila, el cromo es una fila y la página es la fila que se desplaza. Medido con la tira puesta: aviso [0,54], cabecera [54,85], **solape 0**, y el hit-test en el centro de la cabecera cae en la cabecera (antes: 49px de solape y el clic en la tira). El eje `scroll` es el que Mantine expone como `mode` y Atlassian como `isFixed` por slot; `body` existe porque `docs-shell` lo necesitará para anclas y TOC. Compone `Sidebar` (sus tres colapsos, su drawer móvil, sus dos landmarks), `SkipLink`, `Sticky`, `Grid`/`Flex`/`Box`. Sin `.css`. Coordina tres booleanos (`navOpen · asideOpen · mobile`) por contexto, sin máquina y sin palabras: nada en un shell puede bloquearse, así que no hay frase que decir. Medido en navegador (Playwright; 1280 y 375, claro y oscuro, LTR y RTL): - landmarks por construcción — 1 banner · 1 main · 2 navigation NOMBRADOS · 2 complementary NOMBRADOS · 1 contentinfo; encabezados 1-2-2-2-2 sin saltos. - `scroll="main"`: documento 720 = viewport, el main scrollea dentro y la cabecera se queda en 0 tras desplazar 900px. `scroll="body"`: el documento scrollea y la cabecera SE PEGA (top 0 a 900px de scroll). - teclado: los tres enlaces de salto son las tres primeras paradas y Enter deja el foco en el `<main>`. - 375: el raíl es drawer, devuelve el foco al trigger con Escape, el aside se pliega y el documento no crece. RTL: sin desbordamiento horizontal, raíl a la derecha, aside a la izquierda. Cuatro averías que sólo aparecieron midiendo, y las cuatro son doctrina ahora: 1. `minHeight` es un SUELO, no un techo: con `100dvh` ahí el documento crecía con el contenido. Es `height`. 2. Un grid que sólo declara filas tiene UNA columna implícita `auto`, que se encoge a su contenido: la página salía en una tira de 32px al lado del raíl. 3. `Box` declara `flex-grow/shrink/basis`, así que pisa el `flex: 1 1 auto` de la receta del inset. Quien está más abajo en el árbol tiene que PEDIR crecer. 4. Un ancestro con `overflow` se lleva el `position: sticky` de dentro. El `overflow: auto` de `Sidebar.Inset` dejaba la barra sin pegar bajo `body` mientras `data-stuck` decía que sí — el atributo dando la razón a una página que no la tenía. Por eso el shell pone su propia caja: gap de canon anotado (el inset debería dejar elegir si es contenedor de scroll). **Y una avería del ARNÉS que este block destapó**: los 19 previews del tier llevaban los 8px de margen por defecto del navegador, así que ningún block se enseñaba a sangre — contra la regla dura del propio tier. Medido: `x: 8, y: 8, w: 1264` en 1280, la página compuesta incluida, cuyo README cita cifras a sangre. Invisible porque todos scrollean; deja de serlo con un shell que llena el viewport. Arreglado en el reset del arnés. De paso, en el canon `skip-link`: el enlace del panel complementario decía «Ir a la barra lateral», que es como todo el mundo llama al raíl de NAVEGACIÓN. En una página que tiene los dos, nombraba al que no era. Ahora «Ir al panel lateral». Guards: `blocks:check` verde con 19 blocks · `docs:check` 0/0 (638) · `svelte-check` 74, ninguno aquí. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
9fd54c80fa |
uix(skip-link): el tier decidió en F2.1 que los shells lo poseen, y no había nada que poseer
Canon nuevo, eidos-native (1 parte, 0 eventos): la pieza de WCAG 2.4.1 que `site-header` difirió a los shells hace tres semanas y que `app-shell` (F3.1) necesita para existir. Nace de su fase 0, decisión Q4 firmada. Lo que decide, y por qué: - **Las palabras son suyas.** «Ir al contenido principal» no es copy: es el nombre de un contrato. La regla del tier (B-7 / D-BLK.5) apunta a esta puerta — si un string parece inevitable, lo posee el componente canónico vía `texts:` + langs. Por eso el prop es `region`, no `label`, y el vocabulario es la lista de landmarks de la APG. Cada clave es una FRASE ENTERA: en castellano el artículo se contrae con la región, así que un «Ir a» + sustantivo saldría mal en media catálogo. - **`region` no se estampa**: elige un texto y ninguna capa lo lee. Declararlo repetiría el error que `Affix` corrigió el 2026-08-15. - **No compone `Link`**: `Link` es tinta en línea y todo su API describe texto dentro de un párrafo. Esto es cromo que aparece de la nada, con su geometría y su suelo; envolverlo sería pisar todos sus props y aun así dejarlos en la API pública. Los componentes del canon renderizan sus nativos; son los blocks los que no pueden. - **El handler mueve el FOCO**, que es lo que el fragmento no hace: un `href` desplaza la vista y deja el foco en el enlace, y el siguiente Tab vuelve al cromo que el lector pidió saltar. El destino se hace enfocable sólo mientras dura el salto. El `href` se queda: es el camino sin JS. - **Peldaño propio de z (950), el más alto de la escalera estática.** No puede compartir el de `affix`: un empate lo rompe el orden del DOM y este enlace es por definición el primer elemento del documento, así que perdería contra cualquier aviso fijado debajo — el fallo medido en A-95. Medido con Playwright (el pane del navegador no tiene el foco del SO, así que `:focus` nunca casa ahí): en reposo 1x1 con clip-path; enfocado `position: fixed`, z 950, píldora de 181x36 sobre `primary-solid` con tinta blanca y anillo de foco, igual en claro y en oscuro; el salto deja el foco en `#demo-main` y el `tabindex` temporal se limpia en el blur; en RTL la píldora espeja al otro borde (x 338 -> 761) sin una segunda regla. Y la demo se corrigió a sí misma: decía «Tab desde el botón de abajo» y la medición enseñó que desde ahí el foco va hacia delante. El botón sube encima del marco, y de paso salió la otra mitad de la lección — por tabulación NUNCA se aterriza en el `main`, porque no tiene nada enfocable. Justo por eso el destino necesita el `tabindex` prestado. Guards: `component:audit --only skip-link` PASS · `morfo:check` PASS · `eidos-lint` 2 selectores morfo-backed, 0 inválidos · `docs:check` 0/0 · `svelte-check` 72, ninguno aquí. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
73f06a7882 |
uix(background): el velo tenía una escala que no ordenaba, y una tinta que le pone techo
Cierra el eje salvo dos firmas. La escala `strength` dejaba de crecer a mitad de camino: tomaba prestada `--opacity-*`, que nombra cuán opaco es un ELEMENTO, y leída como pesos de velo salía `ghost` 0,30 · `scrim` 0,45 · `overlay` 0,65 · `muted` 0,65 · `subtle` 0,80 — es decir, un consumidor que pedía `subtle` recibía el velo MÁS pesado del conjunto, y `overlay` y `muted` eran el mismo número con dos nombres. Ahora el velo tiene su propia escala de cinco pasos (`--background-scrim-strength-xs`…`-xl`) y la unión pasa a `xs|sm|md|lg|xl`, ordenada por construcción: medido en Chrome, α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente creciente. `md` sostiene el 0,45 que shipeó el hero, así que el default pinta exactamente lo que pintaba. Era cambio de API y se hizo mientras el único consumidor era la demo: ningún block nombra `strength`. Y midiendo eso apareció que la tabla de contraste del README era falsa por la mitad, en la dirección incómoda. Estaba calculada sobre una tinta de α 0,66; `--color-overlay` resuelve a `rgba(28, 25, 23, 0.42)`. Los valores reales, ahora compuestos en un canvas y leídos por píxel en vez de calculados: el default da 1,48:1 sobre foto blanca —no 2,10— y el paso más fuerte 2,14 —no 4,42—. Lo que eso destapa no es un número peor sino un TECHO: `xl` gasta la tinta entera y llega a 2,66:1, de modo que ningún paso nuevo puede alcanzar AA, porque el límite es el alpha del token y no la escala. Con tinta opaca los mismos pesos dan 5,45:1 a 0,65 y 9,22:1 a 0,80. Queda como decisión del autor, reescrita con estas cifras, porque es sobre qué ES un scrim y no sobre subir un valor. La rama `@supports not (animation-timeline: view())` queda ejercitada, que era el último hueco de verificación. Chromium ya no puede desactivar scroll-driven —estable, flag de runtime retirado: seis candidatos probados, los seis siguen reportando soporte—, pero el Firefox de Playwright NO lo soporta por sí mismo, así que la rama está viva ahí sin emulación ninguna. Con el fichero de receta real: `--background-progress` 0 → −64px, 0,5 → 0, 1 → +64px, con `animation-name: none` y cero animaciones. Son los mismos extremos que el camino CSS medido en Chrome, o sea que los dos caminos concuerdan; y en Chromium la rama no aplica y escribir la var no mueve nada, que es la exclusión mutua que el README afirmaba sin haberla medido. Sin ejercitar queda un eslabón —que `ScrollProgress` escriba la var extremo a extremo—: desde el shell de este entorno no hay ruta a localhost, y Firefox no alcanza el dev server. El paseo de la checklist A–H, que estaba listado como lectura de F4 y nunca registrado, encontró tres cosas. Faltaba la excepción `A2.3` con su ID (las tres partes llevan `data: []` porque sus attrs son de wrapper y su único estado vive en el Button que compone) y faltaba `## Subset` (color: el conjunto completo, porque en decoración restringir la paleta sería arbitrario; intent: no se acepta). Y una deriva documental: el README decía en TRES sitios que el control de pausa «IS the canonical Toggle» y que dispara `commit-toggle`, cuando D-BG.18 lo cambió a `IconButton` con etiqueta que cambia y sin `aria-pressed` — el evento es `contact-activate` de `button.ts`. Vestigios de la era D-BG.4 que la enmienda no barrió; corregidos, incluido el comentario del propio morfo. La banda de `feature-split` queda firmada POR SECCIÓN, enmendando el plan en sus tres menciones y `PLAN-blocks-quality` §3 columna B, que la pedía por fila: por fila obliga a cada `Row` a poseer el estado de alternancia —su índice y el de sus hermanas— y un block que coordina deja de ser un block que no posee nada. Gates: audit PASS 0/0 · morfo:check (6 de 160 fallan, background no) · eidos-lint invalid 0 y class-hooks 0 · rtl:check 0/180 · smoke del componente PASS · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno en ficheros propios. Tres fantasmas costaron tiempo y son el mismo patrón: leer las vars tras un screenshot devuelve el `write(0,0)` de un `pointerleave`; dar por muerto el `strength` leyendo `backgroundColor` de un scrim GRADUADO, que pinta por `background-image`; y juzgar `fade` y `pause` sin la precondición que necesitan. El instrumento miente antes que el código, y aquí mintió tres veces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d2574e8a7c |
uix(background): la línea de tiempo la capturaba un overflow, y el puntero se iba con el scroll
El travel llevaba dos días escrito y sin medir, porque el panel oculto nunca activa una animación scroll-driven declarada en CSS. Con el Chrome del autor delante se pudo medir, y lo que apareció no fue una confirmación: fueron dos defectos que ninguna sonda había estado en posición de ver. La puerta es `document.visibilityState` — con la pestaña oculta la `ViewTimeline` existe, con `source` y `subject` correctos y `playState: 'running'`, y `currentTime` es `null` para siempre; ahí ni un `await requestAnimationFrame` resuelve. El primero estaba en la demo y el límite es del componente. `[data-uix-stage]` declaraba `overflow: hidden`, y eso es un scroll container aunque no pueda scrollear nunca: `view()` se ancló al stage y el progreso quedó clavado en 52,63% en toda posición de scroll, sin error, sin aviso y con un `translate` de aspecto perfectamente razonable. Con `overflow: clip` —recorta igual, respeta el radio, y no es scroll container— el travel aparece: progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono, y los `4rem` completos de `--background-parallax-travel` en el extremo. Entra en el README como quinto límite porque el caso real no es un harness de demos: es el `overflow-x: hidden` con el que cualquier landing contiene su decoración, que convierte al elemento en scroll container en los DOS ejes. El segundo era del componente. El rect del anfitrión se cacheaba en coordenadas de viewport y sólo lo invalidaban `pointerenter` y un resize; el scroll mueve el anfitrión sin disparar ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión de 288px dejaba `--background-pointer-y` 0,77 fuera de sitio —los 111px de scroll sobre media altura, a la centésima—, o sea fuera del rango −1…1 que la variable promete, con el spotlight despegado del cursor unos 115px y sin recuperarse hasta salir y volver a entrar. Cacheada la caja en coordenadas de DOCUMENTO y normalizando contra `pageX`/`pageY`, que en un evento real ya traen el scroll incorporado: error 0 en los dos movimientos, halo a 1px del píxel pedido, sin lectura de layout en el camino caliente y sin el listener de scroll que este componente existe para no tener. Queda dicho el residuo: un scroller anidado vuelve a desviar, porque `pageY` no lo ve, y se autocura al reentrar. Y lo que era el encargo, medido: 60 fps con mediana de 16,7ms, p95 17,0, máximo 17,1 y 0 de 200 frames por encima de 20ms con el travel vivo —la base con `speed: 0` salió peor, que es ruido de entorno—; y 0ms de reflow forzado en esos mismos 200 frames moviendo los DOS ejes, con el observador de Long Animation Frames que usa `arts/perf`. El cero está validado por mutación: un thrash deliberado de 65ms en la misma página se reporta con 43ms forzados y el script atribuido, porque un instrumento que no ve nada y un cero real se leen igual. Sigue pendiente la rama `@supports not`, que sólo se ejercita en Firefox. Las 174 demos comparten ese stage, así que el cambio se auditó: `affix` idéntico y `sticky` idéntico —cuatro pasadas alternando `clip` y `hidden`, porque la primera miente—, y `anchor-nav` cambia a mejor: su rail `position: sticky` es hermano del scroller interno, con `hidden` no se pegaba nunca y se escapaba por arriba perdiendo media lista, y ahora se mantiene a la vista. Ninguna demo tocada. Dos avisos para quien vuelva. Un `PointerEvent` construido reporta `pageY === clientY`, sin sumar el scroll, así que los eventos sintéticos sirven para DESCUBRIR este defecto pero no para verificarlo: con ellos el arreglo bueno mide −1,389 donde el ratón real da −0,008. Y leer las variables después de un screenshot puede devolver el `write(0, 0)` de un `pointerleave` en vez de la medición; el valor que vale es el del último `pointermove`, trazado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |