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 }
1980 Commits (07a99679d796e0c268ab1e56b2294be4dbe30079)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
07a99679d7 |
docs(theming): el plan del eje theme-reach — medir qué alcanza un tema, y corregir componente a componente
El autor, al ver que el radio del trigger del nav vivía en un privado clavado a
--radius-default: «¿los componentes son themables? si no lo son, es un error
como framework». Lo es. La doctrina ya exige que cada receta declare sus knobs
en recipes/base.ts (recipe-contract §1, theming §6), pero R-1…R-4 sólo
comprueban «sin literales», no «alcanzable por un tema»: una receta con todo en
var(--radius-md) y privados pasa component:audit en PASS y sólo se puede temar
moviendo el sistema entero.
Medido, no opinado — scripts/theming-census.ts (instrumento, NO guard): de 5.205
knobs de apariencia en 162 recetas, 1.621 (33 %) pasan por un token público del
componente; 1.880 atan directo a un primitivo global, 856 a privados, 614 son
literales; 62 componentes no tienen una sola entrada en el contrato; 59 tienen
alcance < 20 %; 6 al 100 %. navigation-menu, tras todo el día de hoy, 44 %.
El plan (PLAN-theming.md): §1 el censo por familia y por componente (tabla
regenerable, no editable) · §2 los cuatro ejes de «personalizable» (tokens ·
talla · color/variante · estados) · §3 la regla R-5 «alcance de tema» para
component-audit, con su válvula de excepción · §4 ocho decisiones D-TH para el
autor (R-5 dura · perímetro de knob · WIP · orden · DEFAULT IDÉNTICO · normalizar
nombres de slot, que hoy incumplen tabs y el propio nav · cuándo gradúa a error ·
panel de temas) · §5 fases F0–F4 con gates · §6 deuda transversal · §7 el
PROTOCOLO DE VERIFICACIÓN por componente, porque «lo ejecutará Opus, y Opus falla
mucho»: cinco preguntas, medir ANTES (computed por parte/estado/talla, sonda
headless desde la raíz, fuera de callbacks de MutationObserver), editar sólo la
forma firmada, medir DESPUÉS (diff de computed = 0, prueba de CENTINELA por cada
token nuevo, desde el píxel hacia arriba, registro de animationstart/end),
guards por fichero, un componente = un commit con sus artefactos, y revisión
adversarial por bloque. Cada paso tiene un artefacto; sin artefacto no está hecho.
El instrumento se probó por mutación antes de creerle, y falló la primera vez:
no contaba una regla de una sola línea (`[x] { padding-inline: 8px; }`), así que
meter un literal no movía la cifra. Regex endurecido; ahora 6→7→8 con una y dos
mutaciones. Las cifras del plan son las del instrumento final, y el handoff
(CONTINUE-theming.md) obliga a reproducirlas antes de seguir.
Nada firmado, nada construido. Gates del commit: docs:check 0/642 · prettier del
script limpio · tsc del script sin errores · el plan cita ficheros que existen
(los once, comprobados).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
a827948459 |
uix(navigation-menu): el hover es el del sistema — la misma capa, sobre la forma de cada elemento
«El estilo tiene que ser el mismo para todos los elementos. ¿De dónde sacas esos estilos?» De ningún sitio que valiera: en |
2 months ago |
|
|
12796ac428 |
uix(navigation-menu): los enlaces del panel hablan el mismo hover que la barra
«¿Por qué los otros ítems no tienen el mismo hover? ¿Qué sentido tiene?»
Ninguno. En
|
2 months ago |
|
|
822b78cf2c |
uix(navigation-menu): el panel sube como lo que es, y la caja gris del hover era el <li>
Dos correcciones del autor sobre |
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 |
|
|
cae88ca15b |
uix(navigation-menu): un <ul> no es una región, y por eso el landmark salía sin nombre
El morfo declaraba el nombre por defecto de la navegación en la parte que pinta
el <ul>, con la razón escrita al lado: «the root <nav> delegates its name to the
list it wraps». La delegación no existe. El gesto que un lector de pantalla
ofrece para saltar entre las áreas de una página llega al <nav>, y llegaba a un
<nav> ANÓNIMO — con el nombre del consumidor puesto una capa más adentro, en un
elemento que ninguna navegación por landmarks visita.
Sobrevivió tres semanas de tier porque con UNA navegación en la página un
landmark anónimo es admisible, y todas las demos del componente y todos los
blocks de sitio montan una. Apareció en cuanto el app-shell montó dos ámbitos,
que es lo que un shell hace por definición (A-111). La APG es explícita: si una
página incluye más de un landmark de navegación, cada uno lleva su nombre.
El arreglo NO es el que la ficha proponía. Su paso 1 quería que la lista
heredase el nombre por aria-labelledby; el barrido de los morfos hermanos con
defaultElement 'nav' dijo otra cosa: breadcrumb (<ol>) y nav-tree (<ul>)
declaran aria: [] en su lista y nombran sólo el <nav>. navigation-menu era el
único fuera de esa forma, así que la pareja de nombre —el aria-label con su
suppression prop-falsy ariaLabelledby, y el aria-labelledby por propRef— sube
entera a la parte Provider y la lista se queda con su aria-orientation.
En soma, la fuente ariaLabelledby sube al nivel del runtime (la forma de
breadcrumb) y el wrapper DEJA de sacar aria-label de restProps: viaja con el
resto y gana el merge por política de naming, que es lo que A-85 dejó escrito y
lo que este componente no aplicaba. Se van con ello el opt ariaLabel y el
reenvío condicional que la List hacía a mano.
Y como la regla no estaba escrita en ninguna doctrina —grep de «landmark» en
morfo.md, canon/ y guides/ daba cero—, se escribe: architecture/morfo.md §Step 4
gana «A landmark is named on ITS OWN element», y nace el censo
morfo/landmark-census.test.ts, que falla si una parte con defaultElement 'nav'
no declara un attr de nombre EN SU PARTE. Probado en rojo por mutación tres
veces: deshaciendo el arreglo (navigation-menu.provider), retirando la excepción
(palabras.breadcrumb) y vaciando el catálogo, que es la mutación que impide que
un censo sobre nada pase en verde. El ámbito es 'nav' a propósito: main, header
y footer son únicos por página, y section y form sólo son landmark cuando ya
tienen nombre.
El guard destapó de paso un segundo caso de la misma clase: la parte Breadcrumb
de palabras declara un <nav> con aria: [] y nadie la nombra, ni en soma ni en
eidos. Queda como A-116 con EXCEPCIÓN FIRMADA por el autor: palabras es el eje
de otra sesión y su rama es compartida. La tercera fila del censo obliga a
retirar la excepción el día que esa parte declare un nombre.
Verificado con el nombre COMPUTADO desde el árbol AX de Chrome por CDP, nunca
leyendo el atributo (un aria-label en el DOM prueba el atributo, no el nombre),
con servidor recién arrancado y esperando la puerta de hidratación honesta:
/blocks/app-shell/preview navigation: ['Navegación principal',
'Menú de la aplicación',
'Migas de pan'] (antes: el 2.º null)
/uix/components/navigation-menu (sin label del consumidor)
el <nav> computa 'Principal' — el default del morfo,
traducido, sobre el landmark; el <ul>, sin nombre
Gates: 420/420 en morfo+sema+soma+eidos tocados · component:audit --only
navigation-menu PASS · morfo:check PASS · svelte-check 72/62 antes y después ·
docs:check 0/0. Los avisos de prettier de los ficheros tocados ya fallaban en
HEAD (comprobado con git show HEAD:… | prettier --check); el fichero nuevo va
formateado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
e2754a2b6f |
docs(sidebar): el handoff del eje, y las seis deudas que abrió en otros
En este eje no queda nada: nueve decisiones firmadas, cinco fases ejecutadas y verificadas, y la revisión adversarial pasada con sus cuatro hallazgos arreglados. Lo que queda es de OTROS ejes, y por eso el handoff los lista con nombre y sitio en vez de dejarlos en la cabeza de quien estuvo aquí: T-1 (`navigation-menu` emite `commit-select` al navegar — el antipatrón «success de navegación»), T-2 (nav-tree · anchor-nav · breadcrumb siguen mudos por el contrato que aquí se retiró), T-3 y T-4 (dos fichas del ledger de blocks que suponen lo contrario de lo que ahora hay), T-5 (el guard del bundle sólo ve tokens, no reglas) y T-6 (la densidad y el zoom no bajan a un subárbol, y eso no está escrito en ninguna doc). El handoff también deja por escrito las dos veces que hoy mintió el INSTRUMENTO y no el código: con la pantalla del navegador oculta rAF se congela y `getBoundingClientRect` daba 256px para un panel de 52; y leer `animationName` dentro del callback de un MutationObserver mide el instante anterior al flush, que es como di por viva una animación que moría 3,5 ms después. 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 |
|
|
f6d5fa1595 |
uix(sidebar): la fila que navega decía «te sentí» a nadie, y el libro dice cómo se dice
«Navegar una fila es nativo y no suena» era una decisión de la fase 1 tomada en silencio (el plan la dejó como «decidir en fase 1») sobre una premisa falsa — «item = Link puro»—: la fila es `<button>` cuando no tiene `href`, la acción es un botón pelado y el `MenuItem` posee un flyout. Medido en navegador: pulsar fila, sub-fila o acción estampaba CERO; el Trigger sí acusaba recibo, porque compone `Button`. El libro no deja margen: cap. 22 §9 compone el enlace como «contact.press seguido de shift.navigate: la presión no es la navegación», cap. 27 §8 lo repite y cap. 22 §12 llama antipatrón al contacto mudo. Así que la fila declara las dos ocurrencias, y la aparición del flyout —cap. 26 §5-§6, un menú anclado— deja de ser un cambio de estado sin evento. Dónde se estampa cada una lo decidió la doctrina, no la comodidad: el gesto en la mano (la fila, anclado por instancia: es parte REPETIDA) y el cruce en la superficie que cruza como unidad (el provider). Eso evita la quinta pareja `queue` que yo iba a declarar: calendar ya movió su `shift-navigate` fuera del botón porque las dos ocurrencias se comían la única ranura y el press-squeeze no llegaba a pintarse. Voz: contact y shift toman la de su familia (`touch`, `slide`) sin escribir nada; los dos `emerge-*-sub` van SILENT como el tooltip — el flyout se abre al pasar el puntero, y un barrido por el raíl serían ocho apariciones sonando (cap. 26 §7, Criterio 12). El `trigger` NO declara contacto: lo trae por composición de `Button`. El `rail` sí, porque es una franja de cromo y nadie más lo estampa. Medido en el navegador tras el cambio: fila con href → `contact-activate` (press-squeeze corriendo) en ESA fila + `shift-navigate` en el shell; fila deshabilitada → nada; sub-fila → el mismo par; rail → contacto propio + `emerge-collapse` del panel; en modo icono, hover → `emerge-open-sub` (present-rise), hover repetido → nada, salir → `emerge-close-sub` (dismiss-fade). Suite nueva de 8 contratos, `check` con delta CERO contra la base medida con stash (72/72), morfo:check PASS, contracts sin fallos nuevos. Plan y decisiones firmadas: docs/process/PLAN-sidebar.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
23667277c1 |
docs(blocks): el shell dejó cinco filas, y una explica por qué nadie la había visto
Handoff reescrito para mañana y las cinco filas que abrió el `app-shell` fichadas en el ledger (A-111…A-115). La puerta de entrada de mañana es la primera. **A-111 — `NavigationMenu` deja su landmark ANÓNIMO.** El `aria-label` del consumidor se reenvía al `<ul>` de la parte `List` y el `<nav>` se queda sin nombre; el morfo lo declara así a propósito («the root delegates its name to the list it wraps»). Pero un `<ul>` no es una región: quien navega por landmarks ve el `<nav>`, y lo ve anónimo. Con UNA navegación en la página es admisible —y por eso lleva tres semanas de tier sin salir—; con dos ámbitos incumple la APG, y un shell de aplicación monta dos por definición. Medido en el preview: `navigation: ['Navegación principal', null, 'Migas de pan']`. La ficha lleva la forma propuesta en cuatro pasos: mover el default de naming de la `List` al `Provider` y que la lista apunte al root con `aria-labelledby`; precedencia de clase «naming» (el consumidor gana si dice algo); un guard que falle si un componente con `defaultElement: 'nav'` no puede recibir nombre en su root; y re-medir el censo. Toca morfo + soma, así que va con el aviso de mirar antes si la sesión del `sidebar` sigue viva ahí. Las otras cuatro: **A-112** `Toolbar.Button` tipa contra el `ButtonProps` de soma y no acepta `variant`/`color` · **A-113** la foundation emite los estilos semánticos y no los aplica a ningún elemento —Times New Roman en los 19 previews—, ARREGLADA en el reset del arnés pero con la pregunta de canon abierta · **A-114** `Sidebar.Inset` impone `overflow: auto` y se lleva el `sticky` de dentro · **A-115** no existe `description-list` y el panel de detalle del shell es el «primer detail-view real» que su propia ficha F5 pone como disparador. El handoff recoge además la corrección de fondo del día (la barra y el raíl son dos ÁMBITOS, no la misma navegación en dos tamaños), las dos averías del arnés que este block destapó, y lo que NO se ejecutó: la app de referencia en modo attach, que sigue siendo el único sitio donde el ecosistema entero se demostraría cableado. docs:check 0/0 (639). Co-Authored-By: Claude Opus 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 |
|
|
e5f30a501d |
blocks(app-shell): «no compone ningún block todavía» no era una razón, era una casilla vacía
El shell declara la excepción B-10 —los shells componen otros blocks por diseño— y no compone ninguno. Eso estaba escrito como si fuera una tarea pendiente. No lo es, y ahora dice por qué. El candidato obvio es `site-header`, y es la pieza equivocada aquí por tres cosas que están en su propio API: es `Sticky` por defecto (una barra de aplicación bajo `scroll="main"` NO debe pegarse — nada se desplaza por debajo de ella), envuelve su contenido en un `Container` MEDIDA (una barra de app va de borde a borde de su inset), y su forma es marca · nav · acciones + drawer móvil, o sea una SEGUNDA navegación dentro de un shell cuya navegación es el raíl. Es el cromo de un SITIO; esto es una aplicación. Lo que sí falta, y queda registrado como gap: lo que una barra de aplicación sostiene de verdad —disparador del raíl, rastro, buscador, avisos, menú de usuario— no tiene pieza en el tier. `user-menu` (F3.6) y `notifications` (F3.7) están planificados y sin construir, y el shell los compondrá POR esta misma excepción el día que aterricen. Hasta entonces la barra es el snippet del app, montada con canon (`Sidebar.Trigger` + `Breadcrumb` + `Toolbar` + `Button`), que es lo que enseña la demo. 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 |
|
|
9333acd661 |
docs(blocks): A-95 deja de ser «bloqueada por una pieza que no existe»
La pieza existe desde ayer. La fila pasa a ARREGLADO con sus cifras: solape 0px
entre el aviso y la cabecera, hit-test dentro de la cabecera, documento = 720 =
el viewport. Lo que la cierra no es un número mejor sino haber quitado la
pregunta que tumbó todas las propuestas anteriores («¿y si tenemos banners a
diferentes alturas?»): el aviso entra en flujo como una fila, así que su altura
es la que sea.
⚠️ Sigue siendo cierto lo que la ficha decía del `Affix`: fuera de un shell, un
aviso con `affix="top"` sigue tapando una cabecera pegada. Es el apilamiento
firmado, no un defecto.
A-109 gana disposición escrita (es del canon: el `role="banner"` va después de
los rest props; dos salidas, y se decide en una sesión del canon).
El handoff cambia de puerta: F3 está abierta y `app-shell` hecho. Queda escrito
lo que la sesión NO llegó a ejecutar — la app de referencia en modo attach, que
sigue siendo el único sitio donde el ecosistema entero se demuestra cableado, y
hoy no lo hace ninguno de los siete shells del repo.
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 |
|
|
f3bf6a0299 |
docs(blocks): la ficha del shell cabía en cinco líneas porque no miraba el ecosistema
Fase 0 de F3.1 `app-shell`, firmada. La ficha original se escribió antes de que existieran el canon `Sidebar` y la capa `affix`, y el autor preguntó lo que había que preguntar: si su alcance tenía delante `active-app` y `active-uix` enteros. No los tenía. Las cuatro decisiones que se firman, y lo que las obligó: - **Q1 — el block NO cablea servicios.** La integración del ecosistema se demuestra en una app de referencia en app-land, no dentro del block. Un shell que leyera `App.session` para dibujar el menú de usuario sería una raíz de composición disfrazada: funcionaría, y metería el arranque del ecosistema dentro de una pieza que ha de poder caer en cualquier app. - **Q2 — dos modelos de scroll por prop.** `main` (rejilla 100dvh, sin fixed, sin números) es lo que cierra A-95 por construcción; `body` es lo que `docs-shell` necesitará para anclas y TOC pegada. - **Q3 — cabecera dentro del inset.** El provider del `Sidebar` ES la fila flex, así que su `Trigger` sólo vive dentro. Una cabecera a todo lo ancho exigiría partir ese provider: queda como gap de canon para v2. - **Q4 — canon `skip-link` + uno por región montada** (técnica G124). De paso, tres cosas que estaban mal escritas y ahora lo dicen: - El dossier afirmaba que **ninguna referencia trae skip-link** y que era superación nuestra. Es falso: Polaris lo trae en `Frame` y Atlassian genera un menú entero. La superación real es generarlos del mismo contrato que estampa el landmark. Y el icon-rail no es de Mantine (su `collapsed` es booleano); es de shadcn, AntD, Toolpad y Atlassian. El dossier tampoco miró nunca el `navigation-system` actual de Atlassian, y el `page-layout` que sí miró está deprecado. - **D-BLK.6 ha derivado**: la celda dice «ni prefs ni langs ni eidos» y la doctrina vigente (`blocks.md` §Services) sancionó dos servicios en julio y agosto. Queda enmendada con lo que sigue siendo cierto: ningún block CREA ni cablea servicios. - **E-5 está desfasada**: `Command.Dialog` ya trae el atajo (`shortcut`, `mod+k` por defecto). El listener a mano de F4.1 no hay que escribirlo. - **F3.2 `auth`**: la ficha dice cuatro vistas y el dominio tiene seis. Falta `update-password`, que no es un lujo — es donde aterriza el enlace de recuperación, y sin ella el camino de `recover` no termina. `blocks.md` gana un párrafo «Shells» bajo §Conventions: qué poseen (geometría, landmarks, alturas) y dónde está la línea (la raíz de composición es de la app). Verificado en código antes de escribirlo: `Sidebar.Inset` acepta `child`, `Box` expone minHeight/position/overflow y `Grid` templateRows —la rejilla del shell no necesita CSS propio—, y `Sticky` expone `root`. docs:check 0 errores, 0 avisos (635 docs). 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 |
|
|
f7be59cdcf |
uix(background): se va `Backdrop`, que llevaba desde F1 absorbido y esperando tu palabra
D-BG.1 firmó la absorción y el borrado el 2026-08-17, pero el borrado exigía orden explícita del autor y no la tenía; llegó hoy. Se van cinco ficheros: `eidos/components/backdrop/` entero —`backdrop.css`, `backdrop.svelte`, `index.ts`, `types.ts`— y su morfo `morfo/components/backdrop.ts`. Con `git rm`, no con `rm`, para que el índice lo registre. El censo se repitió justo antes de tocar nada, no se dio por bueno el de ayer: ningún import del componente ni del morfo fuera de su propia carpeta, no lo exporta el índice de eidos, no aparece en ningún índice de morfos, sin langs, sin demo, y ningún `import.meta.glob` que lo cargara por convención. Los gates lo confirman por aritmética, que es mejor que por ausencia de ruido: `morfo:check` pasa de 168 morfos a 167 y de 8 saltados sin demo a 7 — exactamente uno, el que se fue. `check` conserva sus 72 errores preexistentes y no menciona backdrop en ninguno (el recuento de ficheros baja de 6985 a 6981). vitest de `eidos` + `morfo` 653/654, con el rojo de siempre (`skin-media-player`). `docs:check` 0/635. `smoke` 320/320. Lo que NO se borra son las citas. La §Baseline del README de `Background` sigue nombrando a `Backdrop` —ahora fechada, con la nota de que sus cuatro tramas (`glow · mesh · grid · dots`), su máscara de desvanecido y su caída bajo `prefers-contrast` se migraron valor por valor, y de que la historia guarda el original— porque esa fila explica algo que el código ya no puede: por qué este componente se llama `Background` y no `Backdrop`. El nombre colisionaba con el velo modal (MUI, Vuetify, y el propio archetype `overlay`), y esa colisión es la razón del nombre que hoy llevan `data-background`, `--background-*` y `Background.Video`. Con esto el eje queda cerrado: las cuatro firmas del autor ejecutadas, la verificación en navegador completa —travel, 60 fps, reflow y la rama `@supports not`— y el paseo A–H registrado. Vivo sólo queda lo que pertenece a otros ejes: `Ambient` honrando la pausa (del pack) y el puerto de estado para `<video>`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4a9315d977 |
uix(background): el velo era el dim del modal, y con on="light" combatía a su propia tinta
D-BG.20. El scrim tomaba prestado `--color-overlay`, que es `surface.backdrop`: el dim de los modales (MD3/Radix/Vaul según el changelog), afinado POR MODO porque una página clara necesita menos atenuación que una oscura. Es una herramienta de ATENUACIÓN, y el scrim la usaba como tinta de LEGIBILIDAD — la misma enfermedad que la escala `--opacity-*` prestada que se arregló esta misma mañana, y que trajo tres defectos. El primero, que la misma fotografía se leía distinto según el modo: 1,48:1 en claro y 2,10:1 en oscuro. Una fotografía no cambia con el modo, y la legibilidad no es un eje que el modo pueda mover. El segundo, un TECHO: el alpha propio de la tinta (0,42 / 0,66) acotaba la escala entera, de modo que ni el peso máximo alcanzaba AA y ningún paso nuevo podría haberlo alcanzado. El tercero es el que no había visto y es el peor: con `on="light"` el velo era OSCURO bajo tinta oscura. El scrim combatía a la tinta que existía para sostener. La tinta del velo pasa a ser el SUELO de la tinta que `on` puso en vigor, que es lo que D12 ya define: `--color-content-on-solid-contrast` bajo `on="dark"`, `--color-content-on-solid` bajo `on="light"`, y `--color-surface-default` sin `on`, porque ahí la copia toma la tinta de la página. Tres roles que ya existían —ninguno inventado, §16.C— consumidos como capa 4 → capa 3 (§3) y sin alias. Con la tinta opaca el peso ES el alpha, así que `strength` significa una sola cosa; antes significaba dos, porque con `color="teal"` el ink ya era `--palette-solid`, opaco, y sólo el default era translúcido. La escala se re-afina por el TRABAJO de cada paso, medida con la matemática del propio framework (`$color`: `apcaLc` + `wcagContrastRatio`) sobre la peor obra de cada contexto: `xs` 0,08 · `sm` 0,13 · `md` 0,19 · `lg` 0,40 · `xl` 0,70. `xl` es el único paso que promete legibilidad sobre CUALQUIER fotografía, y lo promete contra los cuatro suelos que el framework ya usa —el criterio del par on-solid (`lib/on-solid.ts`: APCA |Lc| ≥ 60 ∧ WCAG ≥ 3) y el AA 4,5 de §40— en ambos contextos: `on="dark"` sobre foto blanca 6,45:1 con Lc 85, `on="light"` sobre foto negra 8,29:1 con Lc 61. 0,70 es el mínimo que cierra los cuatro; el más exigente, el APCA de `on="light"`, pedía 0,695. Los pasos por debajo son atmósfera y el README lo dice: no son garantías. Verificado en Chrome, no calculado: sin `on` el velo sale `oklch(0.9911 0 0 / 0.19)`, el suelo de la página en claro; con `on="dark"`, `#1c1917` al 19%; con `on="light"`, BLANCO al 19% bajo tinta oscura, que es el arreglo; `xl` en `on="light"` sobre foto negra mide 8,25:1 por píxel donde `$color` predijo 8,29. Y el consumidor real: el hero en layout `background` pinta `#1c1917 / 0.19` con titular blanco y da 1,48 / 5,17 — exactamente lo que daba antes en modo claro, paridad a la cifra. Los heroes en modo oscuro se aclaran de 0,297 a 0,19, que es el modo soltando una decisión que nunca fue suya. El selector de contexto va envuelto en `:where()`, y no por estética: a pelo llega a (0,4,0) y le gana a `[data-color]`, de modo que un `<Background.Scrim color="teal">` dentro de un stack `on="dark"` pintaba el suelo en vez de teal — el sistema de color abierto de §25 derrotado por un selector de conveniencia. Envuelto, la familia baja a (0,2,0): el contexto gana al bloque base por orden y pierde contra el color explícito. Medido antes y después. Queda anotado que el vignette sigue consumiendo `--color-overlay`, y ahí es correcto: es una trama que atenúa bordes, no un velo de legibilidad. Esta decisión salió de leer la doctrina entera después de que el autor preguntara si mi recomendación era conforme al sistema de color. No lo era: yo proponía una tinta opaca única, que servía sólo a `on="dark"` e ignoraba los otros dos contextos, y medía el contraste con un canvas WCAG-only en vez de con los suelos del framework. Los tres puntos los corrigió la documentación. Gates: audit PASS 0/0 · eidos-lint invalid 0, class-hooks 0 · rtl:check 0/180 · vitest eidos 434/435 (el rojo es `skin-media-player`, el de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c691a88c1a |
uix(background): un fondo nunca suena, y no estaba resuelto sino evitado
D-BG.19, planteada por el autor: qué pasa con un `Background.Video` cuyo clip
trae pista de audio, y cómo se relaciona con el interruptor de sonido del
framework o con un «no sound» de la app. La respuesta que había en el código era
que no había respuesta. `muted` era una prop con default `true` y la razón
escrita al lado no era doctrinal sino mecánica —un clip sin mutear tiene el
autoplay rechazado en todos los navegadores—, de modo que el silencio era un
efecto secundario de la política del navegador y no una decisión de nadie.
Lo que eso significaba en cuanto alguien escribiera `muted={false}`: el clip
sonaba FUERA del grafo de audio. `sound.buses.content.setMuted(true)` no lo
silenciaba; no participaba del audio focus, así que hablaba por encima de un
`MediaPlayer` en vez de cederle el paso; no atenuaba el bus `ui` mientras sonaba;
no se proyectaba a MediaSession; y el slot `sound` de `$prefs` —el interruptor
«sin sonido» de la app, el que se proyecta al DOM como `data-sound`— no lo
alcanzaba nunca. Un `AudioContext` por documento es la razón de ser de ese art, y
el componente no tenía ni una referencia a él. De WCAG 1.4.2 se salvaba de
rebote: el control de pausa que el stack ya debía lo para, por carambola.
Así que `muted` deja de ser prop y se escribe `true` incondicionalmente: la API
ya no puede expresar un fondo audible. Tres razones, de más a menos vinculante.
La doctrina propia del componente —toda capa es `aria-hidden` y la decoración no
es contenido, así que un clip de aquí no puede portar significado que el audio
entregue; una capa audible sería contenido vestido de fondo—. WCAG 1.4.2: una
decoración no puede pedir un consentimiento que nadie le dio. Y la fuga de
`arts/sound`, que es la parte que hace de esto un asunto de framework y no de
gusto. Un clip que DEBE oírse es contenido: `MediaPlayer`, que ya es ciudadano de
`sound.media()` y obtiene focus, ducking y MediaSession gratis, o montarlo por
`Background.Layer` y registrarlo uno mismo.
La alternativa —hacerlo ciudadano con `sound.media(el, { focus: 'duck' })` cuando
el consumidor desmutea— queda rechazada por lo mismo que la hacía atractiva: abre
la puerta a que un fondo compita con el contenido real, y ese territorio es de
`Ambient` o de un reproductor.
Verificado en Chrome con la capa en `video`: `el.muted === true`, `paused: false`,
`currentTime` avanzando, `readyState: 4` —sigue reproduciéndose— y el control de
pausa presente. Queda anotado en el código un detalle que invita a un arreglo
equivocado: `hasAttribute('muted')` lee false mientras la propiedad es true,
porque Svelte fija `muted` como PROPIEDAD sin reflejarlo al atributo. No es un
agujero: la propiedad se escribe en el mismo efecto y dos líneas antes del
`play()`, y no hay nada que pueda colarse entre las dos.
Ningún consumidor pasaba `muted` —el `HeroSite` ya lo decía en prosa,
«DECORATIVE — muted»—, así que el cambio de API no rompe a nadie.
Gates: audit PASS 0/0 · vitest eidos 434/435 (el rojo es `skin-media-player`, el
de siempre) · `check` con los mismos 72 errores preexistentes y ninguno propio ·
prettier limpio.
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 |
|
|
db22de616c |
docs(blocks): el handoff de mañana, y las cinco filas que dejó componer la página
F2b queda cerrada entera, así que el handoff cambia de puerta: lo primero de mañana es el repaso block a block que pediste, y va sin ficha escrita a propósito — su forma se acuerda antes de empezar (qué orden, qué se mira, y si lo que salga se escribe en el ledger o en el README de cada block). Las cinco cosas que la página compuesta y cookie-consent encontraron entran al ledger como A-106…A-110, medidas y sin tocar: Button acepta ref y no lo reenvía —la forma exacta de A-94—, Switch no tiene parte de etiqueta y todos los consumidores del repo repiten el mismo apaño, Dialog devuelve el foco a un nodo muerto cuando su disparador se ha desmontado, Banner deja DOS landmarks banner en cualquier página con cabecera, y feature-split es el único block de sección sin .Header. Casi las meto como A-100…A-104, que ya estaban ocupadas. El rango libre empezaba en A-106 y el ledger tiene 110 filas, no 96: las cifras que el handoff repetía —«96 filas, 64-19-13»— eran una foto vieja de julio. Las de ahora están contadas sobre el fichero: 70 ARREGLADO · 24 CONFIRMADO · 13 REFUTADO · 3 DATO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
c786d0f804 |
blocks(landing): las piezas se prueban juntas, y ahí sale lo que ninguna enseñaba sola
Cierra el tramo D y con él F2b entera. Una landing verosímil con 14 de los 18 blocks, a sangre y sin marco de demo, porque el claim del tier —que sus blocks ensamblan sin fricción— es indemostrable block a block: el outline de encabezados sólo existe cruzando secciones, dos landmarks sólo chocan cuando hay dos, y una costura vertical sólo aparece cuando algo se pone debajo. Lo que dice la medida: un h1 y 22 encabezados sin un solo salto de nivel; ningún landmark repetido sin nombre; 0px de hueco entre secciones vecinas —el aire es siempre el padding de cada Section, costuras de 112 a 192px—; cuatro de las cinco cascadas esperando a que su sección entre en viewport; un solo commit-submit en el formulario; setenta tabulaciones sin una trampa de foco y un drawer que a 375px devuelve el foco a su disparador. A-95 vuelve confirmado, ahora con cifras: con affix="top" la tira y la cabecera solapan 49px —la altura entera de la cabecera— y el hit-test en su centro cae en la tira, así que la navegación es inalcanzable. La pieza que falta sigue siendo app-shell. La página compone la tira en flujo, que es lo correcto, y deja el escenario del defecto detrás de ?banner=affix para poder medirlo en vez de citarlo. Un defecto arreglado, y era de los que importan: Newsletter.Reason medía 2,33:1 sobre el panel mientras la nota a su lado medía 8,5:1. Es la frase que explica por qué la acción está bloqueada —la que la doctrina de coordinación obliga a tener— y no se podía leer: la doctrina cumplida en la estructura y derrotada en la percepción. El block ya tenía el mecanismo y ya lo había escrito para su nota («más callado por TAMAÑO, no por tinta»); su razón no lo seguía. Ahora 8,83:1 sobre el panel, y muted fuera de él. Dos gaps nuevos, ninguno tocado aquí: los DOS landmarks banner —el canon estampa role="banner" DESPUÉS de sus rest props, así que nombrar ambos es todo lo que puede hacer la app, y esta página lo hace— y feature-split sin parte .Header, el único block de sección que no la tiene: sin ella emite tres h2 hermanos y la sección se queda sin nombre. El catálogo aprendió a publicar páginas: kind: 'page' en BlockEntry y el guard lo salta en las dos direcciones, con self-test propio probado por mutación. Y una lección de instrumento que costó un susto: una captura fullPage no hace scroll, así que toda cascada bajo el pliegue se queda en opacidad 0 y la imagen sale EN BLANCO. La primera medida dijo «la landing está vacía». Saltar de una vez al final del documento tampoco vale: las secciones pasan de debajo a encima en un frame y el observador nunca las ve entrar. Se recorre en pasos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
06f3c26400 |
docs(background): el handoff del eje, con lo que no pude medir escrito arriba
`CONTINUE-background.md`. F0 → F5 cerradas y commiteadas: el componente existe, tiene demo, está documentado y el tier lo usa. El handoff indexa; la fuente viva sigue siendo `PLAN-background.md` (D-BG.1…18 y la ejecución medida fase por fase). Lo que queda no bloquea el uso: cuatro firmas del autor (la escala `strength` del scrim, que no está ordenada por peso; la foto clara sin ningún peso que llegue a AA; borrar `backdrop/`; y si la banda de `feature-split` va por sección o por fila), una verificación que este entorno no puede dar —el travel real, 60 fps y el detector de reflow necesitan un Chrome visible, y está comprobado que la limitación es del entorno y no del código— y dos tareas que este eje abrió en otros: `Ambient` honrando la pausa, y un puerto de estado para `<video>` que ya tiene dos consumidores. Lleva también las cinco lecciones que costaron medición, para no volver a pagarlas: A30 no es estilo sino supervivencia; `revert-layer` sin `@layer` es `revert`; la demo destapa lo que una sonda no; mide antes de afirmar rendimiento; y la decoración que parece mejor puede ser la ilegible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
331912cf23 |
blocks(background): seis fondos, y el que parecía mejor era el que no se podía leer
F5 de PLAN-background.md: los seis blocks de D-BG.10 (2) reciben `Background.*`. El hero cayó en F2 y la página compuesta ya existía — no había que construirla, había que verificarla con la decoración puesta. La forma es uniforme y tiene un solo desvío: cada block gana un prop `decor` que monta `<Background.Pattern>` como HIJA de su `Section`, la misma composición que el hero ya probó. El desvío es `cta`, donde la decoración va dentro del PANEL, que es donde está el ojo. **Dos hallazgos que sólo la medición podía dar.** Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular bajo AA: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a **2,27:1**. Pintan un lavado claro anclado en `50% 0%` — exactamente donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen, y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1 contra el panel. Por eso el default es `rings`, que además irradia del mismo anclaje superior que usaba el glow. Las tintadas se ofrecen igual: el app decide, pero ya con el número delante, en el tipo y en el README. Y una trama tintada dentro del panel era invisible por construcción: `--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que pinta el panel — un halo del color del panel sobre el color del panel. El block pasa ahora `color="var(--color-content-on-solid)"`, el token que ya usa para su propia copia; medido después, el tinte resuelve a `#ffffff`. **Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band` `grid`, porque una banda de cifras se lee como medida y la rejilla es la trama que lo dice sin decorar por decorar · `testimonials` y `feature-split` `glow`. `site-footer` y `banner` reciben la capacidad APAGADA: la columna B del plan de calidad no pide acabado en ninguno de los dos, y encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió. **Un desvío de alcance, declarado.** La columna B pedía «`Backdrop` alterno» POR FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría a cada `Row` a poseer el estado de alternancia, y eso es coordinación — un block que coordina deja de ser un block que no posee nada. Medido en Chrome sobre la página compuesta real: cinco pilas, los cinco anfitriones adoptados (`position: relative` + `isolation: isolate`) sin que ningún block los posicione; la copia mide 15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y 12,37:1 con `grid`/`dots`. Ledger: seis filas nuevas, A-100…A-105, cada una con su mecanismo y su evidencia. Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · 434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
740274a847 |
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no
F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos
auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los
ejes que F3 añade, y separarlas dejaría un estado que nunca se probó.
Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token
de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más
allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la
deriva contra el puntero), `spotlight` y `attach='fixed'`.
El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de
scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca
`ScrollProgress` y escribe `--background-progress`, que la rama `@supports not`
mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez
porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se
paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll
del propio lector, que es justo la clase que provoca síntomas vestibulares.
**La decisión que no estaba prevista.** El scroll y el puntero quieren mover la
MISMA capa, y una animación sobre `translate` gana a cualquier declaración
estática: el puntero habría dejado de existir sin más. Así que el scroll anima
una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único
`translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el
puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el
shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`.
El precio, dicho porque en la primera redacción escribí lo contrario tres veces:
una custom property NO se puede compositar, así que el navegador recalcula
estilo cada frame. Para una decoración es el intercambio correcto —una property
por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era
falso y ahora el código dice lo que ocurre.
**Dos footguns cerrados por forma, no por disciplina:**
- `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes
había que escribirlo también en la pila, y olvidarlo dejaba la capa
`position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin
error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae
de vuelta). La prop de la pila desaparece: no hay nada que olvidar.
- Un `speed` negativo —una capa que se mueve contra el scroll— invertía el
bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora
usa la magnitud.
**Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una
línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand
la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener
de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando
el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado
e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones
—`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio,
`declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde
el init, y seguir el prop sin escribir en la primera pasada.
**La demo** (`/uix/components/background`, v2, nueve pestañas) monta un
ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí
solo y sin padre no se puede enseñar lo único que importa: que el padre se
adopta y el layout no se mueve. Los chips son uniones completas verificadas por
el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación.
Y fue la demo la que destapó que, con A30, encender `animate` en caliente no
hacía aparecer el control de pausa — el registro era un hecho de montaje.
Invisible en una sonda, obvio con un interruptor.
Registro documental (D-BG.11): `next-features.md` §11 · la frase en
`design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) ·
`PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de
autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y
`Aura` corregida (decía «Not built yet» y está construido) · y en
`motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un
preset nombra una transición discreta CON duración, y esto es modulación
continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor.
Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los
valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y
retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el
`translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada
control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres
de las cuatro clases de capa hasta la segunda auditoría).
⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60
fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y
ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado
que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con
`animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada
por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome
visible.
Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 ·
`rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 ·
`morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de
siempre) · `check` 0 errores propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
dc7c4920d6 |
blocks(cookie-consent): la ley europea como tipo, y las dos salidas al mismo peso
Cierra el tramo C de F2b. El único block cuya fase 0 fue jurídica antes que visual: las referencias shippean el aviso como markup, y el markup no puede impedir las tres cosas que lo vuelven ilegal — un rechazo debilitado, categorías premarcadas, y cerrar tratado como consentir. Aquí son el tipo y el constructor. `essential: true` hace inescribible una UI que ofrezca apagarlo; `initialConsent()` no admite semilla, así que una primera pregunta no puede llegar marcada; `onDecision` es obligatoria, así que un aviso que no informa de vuelta no se puede montar. Las cinco reglas están numeradas en consent.ts y afirmadas por 8 tests, porque son justo lo que un refactor rompe en silencio mientras todo sigue pintando bien. Desviación de la ficha, y a favor: UNA llamada requerida `onDecision(consent, via)` en vez de `onAcceptAll` + `onRejectAll`. Con dos callbacks el consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo renderiza el block y el API no expone knob para degradarlo. Medido: las dos acciones tienen idéntico relleno, tinta, borde, tamaño, peso y alto — 5.96:1 en claro, 8.79:1 en oscuro. Tres cosas se arreglaron por mirar, no por deducir. La barra era un `Card` outline y midió 1.00:1 contra la página: se disolvía en ella, así que va al plano `overlay` del sistema, el mismo que estampa cada superficie flotante del canon. A 375 las tres acciones envolvían con «rechazar» en una fila y «aceptar» en la siguiente, leyéndose como dos botones ajenos justo donde la pareja más importa: ahora son un `Group`, y el markup lo dice. Y el panel devolvía el foco a un nodo muerto — se devuelve aquí, un frame después de que la trampa se desarme, con un pestillo para que el asentamiento del montaje no le robe el foco a nadie al cargar. Tres gaps de canon salieron al componerlo, todos en el README: `Button` acepta `ref` y no lo reenvía (forma de A-94), `Switch` no tiene parte de etiqueta, y el `Dialog` restaura el foco a un nodo muerto cuando su disparador se desmontó. El instrumento mintió antes que el código, otra vez: el pane del navegador no compone frames, así que la animación de salida no termina y un diálogo cerrado sigue pintado — el foco medido allí era ficción. Y `fillStyle` no normaliza oklch, así que todo color leído del estilo computado se volvió negro y dio 1.11:1 sobre texto perfectamente legible. Las cifras de esta ficha se miden por píxel, en Chrome real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
313e4d52fa |
blocks(article-grid): la banda editorial, y el elemento que el canon no podía dar
Lo que las referencias llaman blog. Sus dos preguntas de fase cero quedan resueltas, y ninguna necesitó tocar el canon. El nombre. Blog nombra una sección entera de un sitio, y esto es la fila de artículos recientes dentro de otra página; el tier nombra por función de página y el elemento que renderiza es un article, así que article-grid hace coincidir nombre, función y markup. La palabra blog sobrevive donde un consumidor la busca: la primera línea del readme y la etiqueta del catálogo. El artículo semántico. Un teaser tiene que ser un article o se lee como una caja de texto suelto, pero la tarjeta del canon es un div fijo y eso parecía obligar a elegir entre añadirle un eje de elemento o declarar un hueco. Construirlo enseñó la tercera vía, que no cuesta nada: el bloque renderiza el article y compone la tarjeta dentro. El tier lleva desde su primer bloque renderizando sus propios elementos de seccionado, así que el elemento es del bloque y la superficie sigue siendo del canon. La regla amuralla al primitivo, no a la composición. Y tres decisiones más que sí son de diseño. La tarjeta entera no es un enlace, porque el teaser lleva además un chip y una firma y anidar interactivos dentro de un enlace es marcado inválido: es justo donde acaban las referencias que hacen clicable toda la tarjeta, así que el enlace vive en el título y hay exactamente uno por teaser. El realce al puntero viene encendido, al revés que en precios, porque un teaser sí es algo a lo que se va. Y el bloque no formatea fechas: las compone la app con el componente del canon. Dos errores míos en la tanda, los dos cazados mirando la captura y no la sonda. El ancho mínimo por defecto sólo cabía dos veces en la medida, cuando las referencias lideran con tres columnas; medido y corregido, ahora da tres, dos y una según el ancho. Y el segundo intento de arreglarlo no llegó a aplicarse sin que me diera cuenta, porque lancé el reemplazo sin aserción y el formateador había juntado las props en una línea: culpé a la caché del servidor antes de mirar el fichero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
33031c2ad9 |
uix(background): el media entra en la pila, y el fondo aprende a pararse
F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b7474e499d |
blocks(logo-cloud): la banda de marcas vuelve, y su parte difícil no era la disposición
Se difirió en F2 sobre una premisa que las referencias desmienten —que su versión honesta pedía un marquee o quedaba en un wrap trivial—: Tailwind Plus, Untitled y Flowbite lo shippean estático. Una decisión cuya premisa cae se corrige. Pero el argumento para construirlo no es la paridad. Lo difícil de un muro de logos es la tinta: veinte marcas en veinte paletas gritan sobre la página y entre ellas. Ese tratamiento acaba de entrar en la fundación como contexto, así que el bloque queda fino y es exactamente la forma correcta — cuando lo difícil es un tratamiento, el tratamiento va a la fundación y el bloque sólo lo estampa. Aquí no hay una línea de CSS, que es además lo que el contrato exige. Dos decisiones medidas, no elegidas. Las marcas van en un wrap y no en una rejilla, porque llegan con proporciones dispares y unas pistas iguales dejan huecos irregulares alrededor de las estrechas. Y la parte del ítem se gana su sitio defaulteando el eje de bloque, que es lo único que un logo nunca trae consigo: un vectorial carga su viewBox, un raster sus píxeles, y uno al lado del otro aterrizan a cinco alturas distintas. Verificado con ocho marcas de anchos y colores deliberadamente dispares: las ocho a una sola altura conservando sesenta y cuatro píxeles de diferencia de ancho, que es cada una guardando su proporción. El marquee se queda como hueco con disparador y con su forma decidida: si algún día entra será un preset de movimiento sobre la fila, no un bloque nuevo, y con la preferencia de movimiento reducido parándolo. Y el hueco de la utilidad para nombres sólo audibles sale aquí por tercera vez en la ola. Tres consumidores reales lo piden ya. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ad26922d29 |
uix(foundation): data-ink, el contexto de tinta para marcas ajenas
El hermano de data-on, para un problema distinto: uno re-entinta un subárbol porque cambió su lienzo, este porque el contenido son marcas de otros y se pelean entre sí. Un muro de logos de clientes es el caso que todas las referencias shippean y ninguna resuelve a nivel de sistema: veinte marcas en veinte paletas gritan sobre la página, así que los catálogos retocan cada asset a mano — y por eso venden el modo oscuro como un segundo artefacto, porque un asset retocado no puede seguir a un tema. Va a la fundación y no a un componente, y eso se midió antes de elegir. Image no tiene eje de tinta, ni prop ni regla, y tampoco era su casa: es un componente con máquina de estados de carga para fotos, mientras que un logo suele ser un svg en línea. Y el mecanismo es doble — un vectorial toma la tinta por currentColor y un raster por filtro —, así que un solo eje tiene que cubrir los dos. Un primitivo Logo nuevo se descartó por lo mismo: no habría cargado más que el contexto. Sin tokens nuevos: el reposo y el realce son los que el tier semántico de la escala de opacidad ya tenía. Y el opt-out por marca vale anidado, porque hay marcas registradas que no se pueden alterar y el bloque no puede saber cuáles. Dos medias querys que importan: la transición se anula con la preferencia de movimiento reducido, y bajo colores forzados se retira el desaturado entero, porque ahí la paleta es del sistema y desaturar pelearía contra el contraste que ese modo existe para garantizar. Verificado sobre ocho marcas de anchos y colores dispares: en mono, gris al sesenta y cinco por ciento con la tinta del tema, y otra distinta en oscuro; en brand, el color propio de cada una; al pasar el puntero, color entero. Queda anotada en el generador la trampa que me costó una vuelta: renderBlock une declaraciones con un salto de línea y no añade punto y coma, así que cada entrada trae el suyo salvo la última. Escritas sin ellos, las reglas llegan a la hoja de estilo y no pintan nada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0cee0fa403 |
blocks(tramo B): las quince matrices, y el guard que ya las exige
El entregable central de la ola queda cerrado. Cada block declara ahora, en su propio README, qué composición nuestra alcanza cada variante que shippean las referencias, con la receta exacta, dónde se ha visto y en qué estado queda. Y la regla que gobierna la tabla no admite promesas: una receta que no se ha visto en el navegador no entra. El saldo es lo que la ola buscaba desde que se abrió la pregunta. Unas noventa y cinco filas cubiertas con receta y prueba. Cuatro superaciones con nombre, que ninguna referencia puede expresar porque shippean markup estático: el plan actual que no se puede elegir, las dos secciones que explican por qué un envío está bloqueado, y las cifras que cuentan respetando la preferencia de movimiento. Una veintena de gaps, cada uno con su nombre: la mitad son recetas que existen y no se han enseñado, que es trabajo de demo, y la otra mitad candidatos de canon que ya conocíamos. Y ocho «no se ofrece» con su motivo escrito, que es una postura del sistema y no un agujero — un carrusel en la apertura es movimiento automático sin control de pausa, un mapa es una dependencia externa, filtrar preguntas es estado de datos del app. La primera tabla justificó el tramo entero. El hero declaraba desde julio que el mockup de móvil estaba hecho, y la demo sólo había enseñado el cromo de navegador. El primitivo ofrece tres. Se añadió el control, se midió, y sólo entonces se escribió la fila: una receta declarada no es una receta demostrada, y separarlas es exactamente para lo que este tramo existía. El guard exige ya la sección, encendido al final como mandaba el plan —hacerlo antes habría dejado quince readmes en rojo toda la ola— y con su fixture negativo. Cambiar la regla hizo caer tres fixtures antiguos, que es el self-test haciendo su trabajo, y se actualizaron. Probado en rojo sobre el árbol real: quitándole la sección a team sale su línea exacta, y restaurada vuelve a verde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
00489be263 |
blocks(tramo B): las cinco primeras matrices de equivalencia
El entregable central de la ola, y el que responde a la pregunta que la abrió: no competir en número de dumps, sino demostrar variante a variante qué composición nuestra logra cada una de las suyas — y sacar a la luz, con nombre, lo que no alcanzamos. Cada block gana una sección de equivalencias con cinco columnas: la variante de la referencia, quién la shippea, la receta exacta, dónde se ha visto, y su estado. La regla que gobierna la tabla es que una receta que no se ha visto en el navegador no entra: la columna de demostración es la prueba, no la promesa. Y la primera tabla ya justificó el tramo. El hero declaraba desde julio, en sus gaps, que el mockup de móvil estaba HECHO — y la demo sólo había enseñado nunca el cromo de navegador. El primitivo ofrece tres, así que se añadió el control, se midió el cromo de teléfono a trescientos veinte y sólo entonces se escribió la fila. Una receta declarada no es una receta demostrada, que es exactamente lo que este tramo existe para separar. Van cinco de quince: hero, pricing, cta, testimonials y faq, que son los que esta sesión ha trabajado y cuyas recetas están medidas. Entre las filas quedan nueve gaps con nombre —desde la elevación estática que Card no tiene hasta el carrusel de citas— y dos «no se ofrece» con su motivo: un carrusel en la apertura es movimiento automático sin control de pausa, y filtrar preguntas es estado de datos del app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e7677d6d87 |
blocks(pricing): la tabla de comparación, y las palabras que una marca no dice
La brecha recurrente del dossier en pricing, y la última fila del primer tramo de la ola. Es una PARTE y no un hermano porque tiene que obedecer al periodo de la sección, y sólo una parte puede leer ese contexto: B-10 le prohíbe a un hermano importar el block que lo posee. Vivir dentro de la sección es además lo que permite que la app meta el precio en una celda y cambie con el conmutador, sin que esta página cablee nada. La app construye la tabla y el block la compone. B-5 admite datos justo donde el canon compuesto ya es data-driven, y la tabla del canon lo es: exige la instancia que devuelve createTable. Así que la parte no inventa un formato de matriz propio ni traduce nada de ida y vuelta, que es el acoplamiento que esa regla existe para evitar. Las tarjetas y la tabla conviven, como en las referencias, alimentadas por el mismo array de planes. Dos cosas que el plan había esbozado y que construirlas demostró equivocadas, y quedan escritas en vez de calladas. No hay un Record de ejes visuales por plan: acentuar una columna es componer canon, que es justo lo que ya permite el snippet de cabecera, y ese Record habría sido una segunda fuente de lo que la app declara en su plan. Y el block no fija la columna: el motor es del app, y meter mano ahí sería el block decidiendo cómo se comporta la tabla de otro. Lo que sí posee es lo que la app no puede: el nombre accesible de la tabla y las palabras que una marca de visto o de guion no dice en voz alta. Ese glifo lleva todo el significado de la celda y es silencio para un lector de pantalla, así que va oculto y la frase viaja en su etiqueta, desde un mapa exhaustivo con respaldo en inglés y su test en las dos direcciones. Y absorbe el cast del canon siendo genérico: la tabla tipa su ranura sin parámetro mientras el motor sí lo devuelve, así que todo consumidor castea, empezando por la demo del propio componente. Aquí se castea una vez a la entrada y se deshace a la salida, para que el snippet de celda devuelva la fila con la forma del app. Medido a 1280 y 375: ocho filas por cuatro columnas, tabla nombrada, marcas que se anuncian, y en móvil la tabla desplaza con la columna de características quieta y la última columna alcanzable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
93d18c90df |
uix(table): una tabla ancha vuelve a tener por dónde salir, y expone los tipos que exige
Dos cosas que salieron al componer la tabla de comparación de pricing, y que no son del block. La primera se ve y se mide: a 375 la tabla ocupaba 420 dentro de un contenedor de 327, y esos 93 píxeles eran inalcanzables. Sin barra, sin indicio, columnas desaparecidas. Fijar la primera columna —que funciona— no sirve de nada si no hay nada que desplazar. El envoltorio recortaba en los dos ejes: el recorte existe por el radio de las esquinas y el eje de bloque lo necesita, pero el eje en línea no, y ahí se comía contenido. Ahora el eje en línea desplaza y el de bloque sigue recortando, así que el radio se conserva y nada scrollea en vertical salvo que se pida con maxHeight, que es otra cosa. Medido después: cuatrocientos veinte sobre trescientos veinticinco, desplazable; al mover noventa y cinco la columna fijada se queda donde estaba y la última columna entra en pantalla. La demo del propio componente no cambia. La segunda es de tipos: el componente exige una instancia del motor y no la exportaba, así que cualquiera fuera de libs tenía que cruzar una frontera para tipar su propia envoltura. El guard del tier blocks lo rechazó por nombre y tenía razón: lo que se corrige es que el componente exponga lo que demanda, no que la frontera se ensanche. El guard afirma la forma con su porqué escrito y está probado por mutación: devolviendo el recorte de siempre, falla. Que sea guard y no nota importa porque el fallo era silencioso — nada peta, el contenido desaparece por el borde. Quedan registradas dos filas abiertas que no toco: la columna fijada se ancla con una propiedad física, así que en dirección derecha-izquierda se mueve con el contenido en vez de quedarse —medido, su borde se sale de la pantalla—, y eso es del eje de dirección; y el prop de la tabla no lleva parámetro de tipo, así que todo consumidor tipado castea, empezando por la demo de este mismo componente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b36247944b |
docs(blocks): el plan de ejecución de V1, con la fase 0 corregida
La ficha de Pricing.Compare reservaba dos decisiones y ahora las tiene, resueltas por doctrina y no por gusto: los planes los declara el createTable del app y la parte lo compone —B-5 admite datos sólo donde el canon compuesto ya es data-driven, y Table lo es—, y en móvil la tabla scrollea con la columna de características fijada, porque el canon ya da el pinning. Eso último es una corrección de mi propia fase 0: el README de Table lo lista como gap diferido y es falso a fecha de hoy — el engine, soma y el recipe lo implementan los tres. Me apoyé en el README y di un dato equivocado; el primer paso del plan lo corrige donde vive. Seis pasos con su verificación cada uno, y los riesgos del árbol compartido escritos para el agente que lo ejecute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
70f45033fd |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera
El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA,
no un envoltorio — se renderiza como HIJO de la superficie que viste y se
ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de
foundation, `:where(:has(> [data-background]))`, así que ninguna receta se
parchea y ningún consumidor reestructura su layout. Con la pila entran sus
capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines,
noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el
`on` que reata la tinta del contenido de arriba.
Dos cosas medidas en Chrome real que el plan no preveía:
- La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css`
declara `position: var(--box-position, revert-layer)` en `[data-box]`, que
es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer`
devuelve la propiedad al valor del UA. Una Section o una columna de Grid
computaban `static` y la pila se escapaba al ancestro posicionado más
cercano. La regla escribe ahora también `--box-position: relative`: se
resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que
un `position` por prop —que llega inline— sigue ganando y `Dialog.Content`
conserva su `fixed`. 5/5 anfitriones cubiertos.
- El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README
del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`:
`box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el
shorthand borraba aquel de los tres cuya var estuviera sin poner.
`<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand
con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el
shorthand desaparece de la receta; medido en una fila de 600px:
`flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando.
Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que
un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en
vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el
scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que
la capa lleve su propio `color`.
El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y
gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta
F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es
de F5 por D-BG.13.
Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 ·
recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 ·
504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) ·
`check` 0 errores propios.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
f4ce006423 |
blocks(faq): la lista estática, y un tipo que deja de prometer lo que no da
La brecha del dossier para este block, y la mayoría del formato: seis de siete en Tailwind Plus no son acordeón, sino una rejilla de preguntas y respuestas siempre abiertas. El eje vive en la LISTA, no en la raíz. Primero porque la lista es lo único que cambia —la cabecera se lee igual—, y segundo porque esa colocación es la que permite que el tipo diga la verdad: pasa a ser una unión discriminada. Bajo acordeón atraviesa toda la superficie del Accordion del canon; bajo lista no se ofrece ninguna de esas props, porque una rejilla estática no tiene estado abierto que enlazar, nada que colapsar ni modo simple o múltiple. Ofrecer una superficie y descartarla en silencio es justo lo que A-94 dejó dicho que no se hace, y aquí es donde iba a morder después. El ítem lee la disposición del contexto: no puede discriminar sobre la unión de la lista sin obligar a la app a repetir el arreglo en cada pregunta. Su value y su disabled quedan documentados como del acordeón, la misma convención que el fondo del hero y la media del cta. Y bajo lista la pregunta es un encabezado de verdad, porque ahí ES el encabezado de su respuesta; bajo acordeón el canon la mete dentro del disparador y esa semántica es suya, así que el block no inventa una segunda. Medido a 1280: en acordeón, cinco disparadores, ningún encabezado, medida de setecientos sesenta y ocho y respuestas ocultas hasta pulsar; en lista, ningún disparador, cinco encabezados, tres pistas de trescientos nueve, medida de mil veinticuatro y todas las respuestas visibles sin tocar nada. A 375 cae a una columna conservando ambas cosas. El acordeón queda idéntico. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
50a7f47147 |
uix(background): el contrato del fondo existe antes que su primer píxel
F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
f3bfcf2dd9 |
blocks(testimonials): una cita sola no va en una tarjeta
La brecha que el dossier le apuntaba a este block: la cita única es la variante modal de todas las referencias y el grid la minoría, dos de ocho en Tailwind Plus. Entra como disposición, no como hermano. La anatomía no cambia —cita, autor, avatar—, sólo cómo se colocan, y el precedente es el fondo del hero, que también redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide rotación o carrusel, que es otro componente. Viaja por contexto de configuración, el patrón del align de feature-grid y team, no la coordinación de pricing. Y viaja porque el spotlight no es una piel sobre las mismas cajas: la rejilla deja de serlo y de estampar el ritmo estructural —que es para una FILA, no para una cita—, el ítem pierde la tarjeta, la cita sube de tamaño y se centra, y el autor pasa debajo, donde ya no hay una hilera de caras que alinear. Pedirle a la app que repita esa decisión en cinco partes es el agujero que A-84 cerró en feature-grid. Enmarcar una cita sola la hace parecer un ítem de una lista que no está ahí, así que ahí no hay tarjeta y el tipo lo dice, en vez de aceptar una superficie que luego descarta. La cita se centra con un párrafo de verdad: sobre una caja en línea el centrado no aplica, y sería mentira. Slot nuevo para la marca de quien firma, en las dos disposiciones: es lo que hace que una cita se lea como evidencia y no como una opinión, y es la pieza que ponen todas las referencias. Medido: en grid, cinco tarjetas, veinte píxeles, medida de mil veinticuatro y ritmo escalonado; en spotlight, cero tarjetas, una cita de veintiocho centrada en párrafo, medida de setecientos sesenta y ocho, autor centrado y sin escalonado; a 375 la cita cae a veinticuatro. El grid queda idéntico a antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b92dc3bdb2 |
uix(card): el estado deshabilitado deja de pelearse con la animación por la opacidad
Salió midiendo el plan actual de pricing: la tarjeta no se atenuaba. La regla estaba escrita, el token resolvía a 0.4 sobre la propia tarjeta y el cursor de esa misma regla sí aplicaba — pero la opacidad computada seguía en uno. La culpable es su propia animación de entrada. Corre con relleno en los dos extremos y su último fotograma es opacidad uno; una animación gana a una declaración normal en la cascada, así que fijaba la propiedad para siempre. Aislado en el mismo nodo desactivando la entrada, atenúa correctamente. Llevaba ahí desde que el componente existe, invisible porque el cursor hacía parecer que el estado estaba cableado, y alcanzaba a toda Card deshabilitada del ecosistema. El arreglo es dejar de compartir la propiedad: el estado atenúa por filter, que la animación no posee. Mismo token, misma escala, ninguno nuevo, y el bloqueo del puntero en la tarjeta interactiva no se toca. El guard afirma la FORMA con su porqué escrito —que la regla usa filter y no opacity, más el bloqueo del puntero y la presencia de la animación que obliga a esa forma— y está probado por mutación: devolviendo la regla a opacity, falla; restaurada, verde. Verificado con la animación presente: la deshabilitada computa filter opacity a 0.4 y sus hermanas ninguno; en píxeles, su tinta más oscura sube a 164 frente a 139 de la normal sobre el mismo lienzo, que es justo lo que da un 0.4 sobre el fondo de página. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0646d5ebde |
blocks(pricing): el plan dice dónde estás, y el que no puedes elegir dice por qué
Firma del usuario: faltaba la decoración del elegido, y faltaba poder marcar un plan como no disponible — si estoy cambiando de plan, el que ya tengo no debería poder seleccionarse. Lo segundo cambia la posición doctrinal del block. Su README decía desde julio que nada en una tabla de precios puede bloquearse y que por tanto no hay nada que explicar. Eso vale mientras la sección es sólo una comparación; se cae en cuanto el visitante ya está en un plan. Y ahí manda lo que costó contact: una acción bloqueada debe su frase, o es un agujero — algo que no se puede hacer y nada en pantalla diciendo por qué. El plan gana un estado: disponible, actual o elegido. Un enum y no tres booleanos, porque tres booleanos admiten lo que no existe, como elegido y no disponible a la vez. Y ortogonal a featured, que es la recomendación de quien vende: el plan recomendado puede ser justamente el que ya tienes. Se deriva una sola vez en el plan y viaja por contexto. La acción se lo entrega al botón del app por parámetro de su snippet, así que nadie vuelve a deducir en el punto de uso si se puede pulsar, y las palabras del botón siguen siendo del app. La frase, en cambio, es del block: idlangref con respaldo en inglés dentro de un Record exhaustivo, el mismo mapa que bloquea la acción es el que escribe el motivo, así que un estado que bloquea en silencio no se puede ni expresar. Su test lo afirma en las dos direcciones. Y una parte nueva la dice y posee el id al que apunta el aria-describedby del botón deshabilitado, sin renderizar nada cuando no hay nada que decir. La decoración la pone el canon: el elegido reenvía el anillo de Card, el actual su tratamiento deshabilitado. Nada de un data-pricing-selected propio: sería un atributo que nadie lee, porque un block no pinta CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale con cursor de no permitido, botón deshabilitado y aria-describedby que casa con el id de la frase, traducida por el arnés; al elegir otro, su tarjeta gana el anillo de dos píxeles con el primary de su paleta y las demás no. Queda anotado un gap del canon que salió al medir esto y que no toco: Card disabled nunca atenúa. La regla existe y su cursor sí aplica, pero la animación de montaje fija la opacidad en uno con prioridad de animación y se come la del estado; aislado con no-emerge, atenúa. Es el patrón que la doctrina de motion llama frágil-conocido —dos dueños peleándose por una propiedad en un nodo— y alcanza a toda Card deshabilitada del ecosistema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db026e48f5 |
blocks(pricing): la tarjeta responde al puntero, y el guard deja de leer prosa
El plan reenvía el eje nuevo de Card. No es el `interactive` del canon: eso convertiría la tarjeta en un botón y `.PlanAction` ya tiene uno dentro. Apagado por defecto, porque una fila de planes es una comparación y una tarjeta que responde al puntero sugiere que la tarjeta misma es el control; se enciende donde la tarjeta entera ES la elección. Control vivo en la demo y en la URL de la vista previa. Y al escribir el comentario que explica por qué NO se usa un nativo aquí, el guard del tier se puso rojo sobre mi propia prosa: la regla B-2 leía comentarios como código. Su barrido quitaba sólo los de UNA línea, mientras el que entiende todas las formas y conserva los saltos ya existía tres líneas más abajo, usándose sólo para los imports. Es la lección de la fase cinco aplicada a una regla y no a la otra. Arreglado con dos fixtures nuevos —un nativo nombrado dentro de un comentario multilínea es prosa; un nativo de verdad debajo de ese mismo comentario sigue siendo error— y probado en rojo sobre el árbol real: un button inyectado en el hero sale señalado con su línea exacta, y el árbol queda restaurado después. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |