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 }
219 Commits (07778b597a704a3c2861203f6771aef63dbcbc85)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
07778b597a |
uix(date-range-picker): la vista de década es suya y pasa a tokens; el calendario y el campo esperan su firma
Séptimo del eje theme-reach. Este componente es un híbrido de tres familias y el contrato lo dice ahora explícitamente: la vista de década/mes es invención suya y pasa a contrato público — separación, ancho, cabecera, navegación, rejilla y celdas, con los estados del rango separados por nombre en vez de amontonados en uno solo. Más el acento. Lo que NO se toca tiene dueño y esperando firma: la mitad de calendario lee `--calendar-*` y su corrección es la decisión de familia calendar que la auditoría del sistema dejó registrada; la entrada segmentada lee `--field-*` y le toca el mandato de composición de Field. Duplicar cualquiera de las dos bajo prefijo propio habría sido justo el alias que la purga de 2026-07 mató. Y el mismo defecto de prefijo que sus hermanos, aquí por partida doble: 44 referencias `--_date-field-*` y 9 `--_calendar-font-size` declaradas en ESTE CSS con el nombre de otro componente. Con un DateField o un Calendar anidados se habrían pisado. Renombradas, con el diff de computed en cero. Verificación: diff de computed vacío sobre 493 valores en ocho estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
68eb8af260 |
uix(time-range-picker): el reloj de dos columnas y la franja AM/PM pasan a tokens
Sexto del eje theme-reach, y gemelo de time-picker: mismo reparto, mismas razones. A contrato lo que el componente inventa — el reloj de dos columnas (su separación, ancho, tinta y tipografía), los colores de las tres manecillas y la franja AM/PM entera — y sin tocar el lado con forma de campo, que lee `--field-*` porque compone Field. Y el mismo defecto de prefijo que su gemelo: 56 referencias declaraban `--_time-field-*` dentro de este CSS (más una mención en el wrapper). Renombradas a `--_time-range-picker-*`, con el diff de computed en cero. Dos cosas que la revisión había marcado como acuñables y NO lo son, confirmadas al ejecutar: el realce del reloj al puntero lee `--toggle-palette-solid`, que es la paleta de otro componente — si procede, es un bloque de composición cruzada, y eso es decisión, no corrección; y el `padding-inline` del raíl consume el tamaño del pulgar del Slider, un eje que publica su dueño o no se publica. Verificación: diff de computed vacío sobre 725 valores en ocho estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e4fcdd62ee |
uix(time-picker): el reloj es suyo y pasa a tokens; el campo sigue siendo del Field
Quinto del eje theme-reach, y el primero donde la mitad del componente NO debe tokenizarse: time-picker compone Field, así que su parte con forma de campo lee `--field-*` por diseño y duplicar esos ejes bajo prefijo propio sería el vocabulario paralelo que el contrato prohíbe. Lo que sí es invención suya —la cara del reloj— pasa a contrato: separación, ancho, tinta y tipografía del reloj, los colores de las tres manecillas, el refuerzo del pulgar sobre la superficie del popover y el radio del trigger en línea. Más el acento, que ahora es público en vez de esconderse tras un privado. Y un defecto que el censo no podía ver: la receta declaraba sus privados con el prefijo de OTRO componente (`--_time-field-*`) dentro de su propio CSS. No era sólo cosmético — un TimeField anidado y este picker se habrían pisado el nombre. Son 44 referencias renombradas, con el diff de computed en cero. El 82 % que el censo sigue contando fuera son esos préstamos de Field: no es deriva, es un préstamo con dueño, y su corrección es el mandato de composición de Field que la auditoría del sistema dejó apuntado (§5.3-3), no este eje. El guard de huérfanos me corrigió por el camino: había declarado doce tokens de más, copiados del range-picker (la franja AM/PM y los ticks), que este componente no tiene. Retirados; sólo `trigger-radius` tenía consumidor real y se cableó. Verificación: diff de computed vacío sobre 580 valores en ocho estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ae91573c32 |
uix(proof-of-human): el cromo del reto se puede temar; la paloma y el cielo no, y eso ya estaba firmado
Cuarto del eje theme-reach. El reto ataba su escenario a primitivos globales: alto mínimo, radio, fondo, borde y los bordes de veredicto no los alcanzaba ningún tema. Ahora son 20 claves públicas, con el alto por talla en el canon de dos piezas y `parts: ['root']`, más la guía del trazo (corredor, línea, estela, marcador de meta) y el token arrastrable. Lo que NO entra es la escena — la paloma, la carta, los cielos del reloj — y no por falta de tiempo: el guard de recetas ya lista este componente en la excepción de tono fijo, y sus literales ya llevan anotación línea a línea. Un sello no se vuelve azul porque el tema lo haga. La revisión había anotado esto como decisión pendiente del autor; era una firma que ya existía y no había leído. Dos fallos míos que la verificación cazó y que quedan escritos porque son reutilizables. El primero: añadí un bloque `'proof-of-human'` a las recetas sin ver que ya había uno (el forward de paleta), y en un object literal la segunda clave gana — mi bloque se descartaba EN SILENCIO. Lo delató el diff de computed con 28 diferencias, empezando por un `border-radius` que caía a cero. El segundo: al fusionar los bloques perdí la última clave, y el guard de tokens fantasma lo cazó al instante. Verificación: diff de computed vacío sobre 406 valores en siete estados · component:audit PASS · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0. El centinela no vale aquí y así queda dicho: la demo declara su propio `min-block-size` y un degradado sobre el escenario, de modo que tapa dos de los tokens; los que no pisa responden. El 86 % que el censo sigue contando fuera son los dos ficheros de la escena, que enganchan por CLASE en vez de por data-attr — migrarlos es otro eje, anotado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e774ca7e84 |
uix(natural-time-picker): el cromo de la banda se puede temar, y el cielo sigue siendo el cielo
Tercero del eje theme-reach. La receta ataba todo a primitivos globales y guardaba su geometría en privados con un prefijo abreviado, `--_ntp-*`, que ni el censo veía ni el vocabulario permite: alcance 0 %. Ahora son 60 claves públicas y el 61 % medido. Dos de los tres bloqueos que la revisión había anotado no existían: la doctrina ya estaba escrita y no la había leído. El guard de recetas ya lista este componente en la excepción de TONO FIJO — un cielo no cambia con el tema, y sus literales ya llevaban su válvula línea a línea, así que los cielos, la tinta del sol/luna y la línea de posición se quedan privados y anotados, sin decisión que pedir. Y el panel sigue leyendo `--popover-*` porque su propia cabecera dice que esa superficie ES el flotante canónico: duplicarla con prefijo propio habría sido el vocabulario paralelo que el contrato prohíbe. De ahí que el 39 % restante no sea deuda sino préstamo deliberado. Lo que sí es suyo pasa a contrato: banda y knob por talla con `parts: ['panel']` —conservando su derivación de la altura de control, así que la expresión sigue siendo el knob y mover la talla arrastra ambos—, la línea indicadora, los ticks, el readout (hora, meridiem, periodo), los steppers, los chips y el trigger. Más el `panel-gap`, que sí es del panel y no del popover. Las 37 referencias `--_ntp-*` mueren: el prefijo de un privado es el nombre completo del componente (theming §6 regla 5). El rename alcanza receta, wrapper, README y el propio guard que las citaba. Verificación: diff de computed vacío sobre 6.438 valores en ocho estados. Centinela 40/62 automático, y cada uno de los marcados muertos verificado a mano: banda, knob, gap del panel, línea, radio de la línea e inset de los ticks alcanzan; los `trigger-*` NO, porque `[data-popover-trigger]` gana la cascada y viste el trigger — ya eran inertes antes de tokenizarlos, y ahora está dicho en el README en vez de fingir que el token pinta. Guards: component:audit PASS · eidos-lint 0 invalid · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · el guard del bundle obligó a apuntar `tick-font-size` a la coordenada `--size-xxs-font-size`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
cef2d32f16 |
uix(combobox): el campo y su panel pasan a tokens públicos, con la talla resuelta donde manda
Segundo del eje theme-reach. El combobox tenía UN token propio (`content-z`) y
todo lo demás atado a primitivos globales o enterrado en privados: alcance 0 %.
Ahora son 106 claves públicas y el 76 % medido — lo que queda fuera son tres
hovers que esperan firma, seis literales de layout y doce knobs que pasan por
privados que ya derivan de un público.
La talla sigue el canon de dos piezas (coordenada + nombre resuelto), pero con
una diferencia que el DOM impone: los resueltos del control se emiten sobre
`control` e `input`, no sobre el root, porque el `data-size` que manda vive en el
control — y el input suelto lo necesita fuera de él. Los del panel se emiten
sobre `content`, que viaja por portal y nunca vería un token del root. Es el
precedente de `select` aplicado donde toca.
Altura, tipografía y separación del control consumen la coordenada del bundle
`--size-{k}-*`; el relleno inline SE DESVÍA del bundle en cuatro tallas y
conserva su valor de hoy — la desviación queda visible en el contrato en vez de
redondearse a la coordenada, que habría sido un cambio visual disfrazado de
limpieza. El chip conserva sus expresiones (`control-height − inset`,
`control-font-size − offset`), así que mover el control sigue arrastrándolo.
El centinela cazó un fallo real a mitad del trabajo: la primera pasada dejó
vivos los ocho bloques `[data-…][data-size='…']` de la receta, que pisaban los
tokens nuevos con los valores viejos. El diff de computed daba cero JUSTAMENTE
porque la ruta vieja seguía mandando; sin centinela habría pasado por bueno.
Verificación: diff de computed vacío sobre 1.566 valores en ocho estados —
reposo, las cinco tallas, el panel ABIERTO y hover. Centinela 67/106 automático
y el resto verificado a mano: `content-z` alcanza (80 → 4321) aunque viva en el
wrapper flotante; placeholder, disabled e ink del trigger forzando su estado;
los del panel abriéndolo por teclado. Los siete nombres resueltos no se mueven
por diseño — el tema mueve la coordenada. Los catorce `selected-tag-*`, los tres
`separator-*` y `scrollbar-inset` no son verificables en esta demo (no monta
modo múltiple, ni separadores, ni scroll): queda dicho, con su consumidor
comprobado en la receta, en vez de darlos por buenos.
Un token nace inerte y se anota como tal: `content-font-family` y
`content-line-height` los pisa `[data-depth='overlay']` con la misma
especificidad — es la tipografía de portal del Build contract, y tocarla sería
otro eje.
Guards: component:audit PASS · eidos-lint 0 invalid · suite eidos sin rojos
nuevos · rtl:check 0 · docs:check 0 · el guard del bundle obligó a apuntar
`indicator-size` e `item-indicator-size` a `--size-sm-icon-size`.
El informe `docs/audit/theming/` se regenera entero, así que todas las fichas
actualizan su fecha de medida; las que cambian de contenido son combobox y
gradient-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3cdb5b5f03 |
uix(gradient-builder): el editor entero pasa a tokens públicos — un tema lo mueve sin tocar el sistema
Primer componente del eje theme-reach, y su piloto: la receta declaraba CERO
tokens propios («self-contained», decía la cabecera) y ataba cada knob a un
primitivo global o a un privado. Alcance medido: 0 %. Un tema no podía cambiar
ni el alto del preview ni el radio de la tarjeta sin mover el ecosistema.
Ahora los 72 knobs son contrato público del componente. Los ejes dimensionales
siguen el canon de dos piezas que firmó el Sidebar: coordenadas por talla
(`preview-height-{sm,md,lg}`…) más UN nombre resuelto (`host` = md, `size:sm`,
`size:lg`) que es lo único que consume el CSS — así los bloques `[data-size]`
de la receta desaparecen y la cascada la emite el TSC. La geometría se queda
PROPIA (px × `--scaling`): un raíl de edición no tiene coordenada de
control-height, así que forzarlo al bundle habría sido rediseño. La tipografía
sí lo consume, que es lo que el guard `recipe-css-contract` exige.
De paso mueren dos abreviaturas que escondían tokens al censo y podían cruzarse
por anidamiento: `--gb-checker*` y `--gb-stop-fill` pasan a
`--_gradient-builder-*` (también en el wrapper). El damero de transparencia se
parametriza por `checker-color` + `checker-cell`, y el patrón de cuatro
gradientes se queda privado — un tema cambia el color de la celda, no reescribe
la rampa. El `padding` shorthand se parte en ejes lógicos.
El default no se mueve: sonda antes/después con 6.612 valores computados
comparados en 7 estados (reposo · cinco tallas · hover) sobre 34 nodos, diff
VACÍO. El único diff que apareció era la transición de hover de un Button
compuesto capturada a mitad — demostrado reproduciendo el valor interpolado en
el árbol ya modificado, bajando la espera de asentamiento.
Centinela: 72/72 tokens alcanzan. 49 desde el ámbito del componente en reposo;
las 12 coordenadas de talla y las 2 de preset seleccionado forzando su estado;
`hover-preset-border` con hover real; y los 4 del editor de color sólo desde
`:root`, porque el panel viaja por portal — anotado en el README para que nadie
lo lea como un token muerto.
Dos hallazgos que quedan anotados, no corregidos (mueven píxel, así que son
decisión): el wrapper pinta el preview con el shorthand `background` inline, que
resetea `background-image` y tapa el damero, de modo que la transparencia no
llega a verse; y el foco de la parada usa el patrón de dos anillos con
box-shadow cuando §32 canonizó `outline`.
Guards: component:audit PASS · eidos-lint 0 invalid · suite eidos sin rojos
nuevos (queda el conocido skin-media-player) · rtl:check 0 · docs:check 0 ·
check sin errores en los ficheros tocados · censo 0 % → 80 % (el resto son
privados que YA derivan de públicos, el anillo de foco y tres literales de
layout) · captura 2× revisada.
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
b72a564918 |
fix(eidos): la capa sale de components/ — tenerla ahi acoplaba Fab y MenuDial a Affix
Defecto de diseno mio, cortado por el autor. Mientras la capa vivio en
`components/affix/affix.css`, sus consumidores la importaban con
`'../affix/affix.css'`: **un componente dependiendo del directorio de otro**, que
es canon prohibido — «un componente no es libreria de otro». `Fab` y `MenuDial`
quedaban esclavizados a `Affix` por una ruta, cuando lo unico que comparten es
geometria.
Va a `eidos/lib/viewport-placement.css`, junto a `lib/list-surface.css`, y los
tres consumidores importan de ahi. Ninguno depende de ningun componente.
LO QUE HIZO FALTA PARA PODER MOVERLA, que es lo que me faltaba entender. Ya lo
intente esta manana y lo revirti porque `recipe-css-contract` fallaba — deje que
el guard dictara la arquitectura en vez de preguntarme por que `list-surface` si
puede vivir en `lib/`. La respuesta era el diseno entero: **no tiene clave de
receta**. Declara sus `--list-*` dentro de su propio CSS, componiendo primitivas
que ya son temeables.
Aplicado igual: la capa declara `--viewport-placement-offset` / `-z` sobre el
propio gancho, compuestos de `var(--space-4)` y `var(--z-index-affix)`. Sin clave
en `recipes/base.ts` no hay exigencia de `components/{c}/{c}.css`, y sin esa
exigencia no hay acoplamiento. El retoque ademas queda a la altura correcta: un
tema mueve la escala de espacio y la escalera de z, no un alias por componente.
Renombres que arrastra, todos hacia nombres de CAPA y ninguno hacia un
componente: `--affix-offset`/`-z` -> `--viewport-placement-offset`/`-z`, sus
ranuras `--_affix-*` -> `--_viewport-placement-*`, y las dos customs del remapeo
de safe-area. La clave `affix` sale de `recipes/base.ts` y sus dos tokens del
`:root` generado.
Sin cambio de comportamiento, medido en las tres rutas: `Fab` sigue en `fixed`,
z 100 (la capa ofrece 150 y su puente la baja por la ranura — los dos niveles
haciendo su trabajo) y a 16px de sus dos bordes; `Affix` en sus nueve zonas
exactas con z 150; y su elemento lleva solo `data-affix`, la identidad.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
layer:check 0/3 · suite eidos 123/123 · rtl 0/178 · docs:check 0/627 ·
smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
c945be23b6 |
refactor(eidos)!: un eje de capa = un token publico + una ranura, y Fab deja de acunar los suyos
BREAKING: se retiran `--fab-offset` y `--fab-z`. No hay shim (regla del tier).
Rompe a quien los sobreescriba en CSS de app; el sustituto es `--affix-offset`
y la ranura `--_affix-z`.
Salio de auditar lo de ayer. `Affix` hacia que el prop `offset` escribiera el
MISMO nombre que la capa lee, y eso trajo dos problemas a la vez:
1. Un ciclo. `offset="var(--affix-offset)"` producia
`--affix-offset: var(--affix-offset)` — custom property auto-referencial, que
CSS resuelve a guaranteed-invalid, matando el calc() de los insets y dejandolos
en `auto`. Medido: la caja aterrizaba a 324px del borde que debia tocar. Y la
demo ofrecia ese valor COMO UNO DE LOS TRES CHIPS de `offset`, con la
intencion de decir «el default».
2. Redundancia. Como el prop pisaba el token publico, Fab no podia usarlo y tenia
que acunar `--fab-offset` / `--fab-z`, cuyo UNICO lector era el puente que los
traducia de vuelta a los nombres de la capa. Un vocabulario paralelo para
valores que la capa ya posee — justo la deriva que la capa venia a borrar.
La forma correcta ya la habla el arbol (`code.css` x6, `display.css` x6,
`calendar-select` x4, `button` x2, `dialog`, `kbd`): DOS nombres por eje.
--affix-offset / --affix-z publico, en :root, desde recipes/base.ts.
el default temeable, un retoque para todos.
--_affix-offset / --_affix-z la RANURA de override. Ahi escribe el wrapper
desde el prop, y ahi escribe un consumidor en su
puente cuando su default difiere.
Consumo: `var(--_affix-offset, var(--affix-offset))`. Con la ranura distinta del
token, el ciclo se vuelve imposible POR FORMA, no por aviso: escrito a mano,
`--_affix-offset: var(--affix-offset)` ahora resuelve a 16px en vez de morir.
El puente de Fab baja a dos lineas y pierde toda traduccion de tokens:
[data-fab][data-affix-placement] {
position: fixed; /* gana el empate con [data-button] */
--_affix-z: var(--z-index-sticky); /* el UNICO valor en que difiere */
}
Lo que Fab necesitaba no era un token propio sino un VALOR distinto: se queda en
la banda `sticky` y no en el peldano `affix` (150), para que un menu o un dialogo
sigan abriendose por encima del FAB.
La regla que esto fija para consumidores futuros queda escrita en los dos README:
un eje de capa tiene dos nombres —default publico y ranura— y el consumidor
escribe la ranura. Acunar `--{componente}-{eje}` al lado recrea la deriva.
Verificado midiendo. Affix: `default` sin style inline y 16px, `0px` a 0, `2rem`
a 32. Fab: `--fab-offset` y `--fab-z` resuelven a cadena vacia (retirados), y las
esquinas siguen a 16px, `fixed`, z 100 via la ranura; `static` sigue `relative` /
`auto`. Identico a antes byte a byte en comportamiento.
component:audit affix PASS 0/0 · fab PASS 0/2 · check 69 = base intacta ·
rtl 0/178 · blocks 0 · eidos-lint invalid 0 · suite eidos 151/151 · smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
92c8f6466a |
feat(eidos): Affix — la pieza que Sticky no puede ser, y la capa que Fab y MenuDial ya habian copiado dos veces
Un aviso al principio del documento se va con la pagina: `position: sticky` se despega en cuanto termina su contenedor. Medido el 2026-08-10 sobre el block `banner` — `Sticky edge="bottom"` acaba en `top: -8` sin `data-stuck`. No es un cableado roto: es que un elemento anclado al viewport es `fixed`, y eso es otro componente. Pero no es un componente nuevo: la capacidad YA existia dos veces. `fab.css` fija 4 esquinas, `menu-dial.css` nueve, y las dos ya divergian (`--fab-offset` publico vs `--_menu-dial-offset` interno; `var(--fab-z)` vs `var(--z-index-sticky, 1100)`, un fallback de 1100 sobre un token que vale 100). Y antes de eso existio entera: `air/layout/float`, el primitivo de 9 zonas que el refactor retiro sin sustituto — lo dicen los README de `float` y de `Fab`. Asi que `affix.css` no es un tercer ejemplar: es la CAPA, enganchada en `data-affix-placement` y no en `data-affix`. El split es portante — `morfo-check` selecciona `[data-affix]` en toda la pagina y valida cada match contra `affixMorfo`, asi que si `Fab` estampara la identidad quedaria soldado a este contrato. Estampando solo el gancho de capa, no. Es el patron de `lib/list-surface.css`: una fuente, cero nodos extra. Lo que salio midiendo, no razonando: - `--z-index-affix: 150`, peldano nuevo. Con la tira en `top` empataba con `--sticky-z-index` (100 los dos) y el empate lo rompe el ORDEN DEL DOM: el aviso va antes que la cabecera en el fuente, luego perdia. 30,7px de solape con el cromo encima — el fallo original reproducido por su sustituto. Un peldano toca DOS sitios: `STATIC_Z_INDEX` y la lista cerrada `Z_INDEX_KEYS`. - NO compone `<Box>`, aunque sea el patron de los primitivos de layout: `box.css` declara `position: var(--box-position, revert-layer)` a la misma especificidad que el gancho, y eidos no usa `@layer`. En el orden de carga equivocado, `position: static` — un `fixed` que no hace nada. - `stretch` es solo de los bordes de bloque. El rail del eje inline se envio y se retiro el mismo dia: `Affix` no dimensiona a su hijo, asi que era una caja invisible de 380px con el hijo de 47px arriba, identica a `top-start` en pantalla. El marco medido decia otra cosa; la pintura mandaba. - `env(safe-area-inset-*)` es fisico y los anclajes logicos: remapeado bajo `:dir(rtl)`. Emparejar `inset-inline-start` con `safe-area-inset-left` despeja la muesca equivocada en RTL apaisado — que es lo que hacen hoy `fab` y `menu-dial`, registrado. `banner` recupera lo que su README daba por imposible: `affix="top" | "bottom"`. La disposicion anterior decia «app-land, el canon ya trae Sticky» y nombraba un componente que no puede hacerlo. Verificado con recorrido real (342px en la demo, 1247px en la preview del block): desplazamiento 0 en los dos bordes, nueve zonas exactas, RTL espejando, el ancestro transformado derivando -342px como esta documentado, y `elementFromPoint` sobre la tira devolviendo el aviso y no el cromo. component:audit PASS 0/0 · morfo:check PASS · rtl 0/178 · smoke 311/311. Queda: migrar `Fab` y `MenuDial` a la capa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e5811e422c |
feat(morfo,soma,eidos): el cruce de mes lo sella la vista paginada, no un mes suelto
Con `numberOfMonths=2` el calendario tenia un *shift invisible* a medias: al pulsar «siguiente», junio cambiaba sus fechas quieto y julio cruzaba. Medido — un solo sello, sobre el segundo grid. La causa no es el runtime ni el llamador. `grid` es una parte REPETIDA, y un emit sin ancla resuelve a la instancia viva mas reciente (`resolveEmitTarget`). `shift-navigate` apuntaba a `grid` desde el 2026-08-11, cuando se le saco del boton para que dejara de pisar su `contact-activate` (A-36). Era correcto para un mes y falso para dos, y un destino que solo vale para un valor del parametro no es un destino. La ley que este mismo eje escribio ya traia la respuesta (`morfo.md` §Where the stamp lands): *if it isn't the subject, the morfo is wrong*. El sujeto es la VISTA PAGINADA — lo que cruza como unidad bajo cualquier numero de meses. No existia como parte, asi que se declara: `months`, arquetipo `viewport`, con `targetFallback: [grid]` para que una composicion headless sin ella degrade a lo de antes en vez de callarse. Se descartaron tres alternativas, y por que ------------------------------------------- - Emitir N veces ancladas: acuna N ocurrencias para UN gesto, N ids en el arbitro de dominancia y una repeticion en la memoria de frecuencia (C-2 solo exime a `handle`), y luego hay que silenciar N-1 desde el componente. - Un emit con N sellos: `resolveEmitTarget` es UNA resolucion para TRES lectores «so they can never disagree» — y el movimiento de foco a11y necesita uno. Ademas declararia que cruzaron dos cosas; el cap. 27 dice que cruzo una. - Volver al provider con CSS descendiente a mano: el provider no es el sujeto (la cabecera no se mueve), deshace una correccion medida y duplica el shorthand de animacion en cuatro recetas. De paso, dos partes clandestinas legalizadas -------------------------------------------- `data-calendar-month-panel` lo fabricaban a mano los cuatro demos multi-mes y `calendar.css` lo estilizaba igualmente, sin que ningun morfo lo declarara; el contenedor lo montaban con un `style` en linea que rederivaba el numero de meses en cada pagina, o con clases locales (`.range-months`, `.month-stack`) que apuntaban a un `--calendar-month-gap` que NO EXISTE — el fallback `36px`/`32px` hacia todo el trabajo. Ahora son partes, la disposicion vive en la receta (`grid-auto-flow: column`, sin contar meses) y el token es `--calendar-months-gap`. Correccion de lo que dije al proponerlo: `eidos-lint` NO cazaba esa deriva — clasificaba el selector como `eidos-only`, no como invalido. Lo que gana el cambio es mover 2 selectores de eidos-only a morfo-backed. Quien si lo caza es `morfo:check`, contra el DOM real. Medido ------ Navegador, `numberOfMonths=2`: el sello cae en `[data-calendar-months]` con `data-event-direction=forward`, corre `shift-cross-forward 0.32s`, y los DOS grids se desplazan 10 px a los 40 ms del cruce (antes: uno). En RTL, `--motion-shift-sign` pasa a -1 y los dos van a -10 px. El boton conserva su `contact-activate` en su propia ranura. Consola limpia en los cuatro demos; el date-range-picker, que muestra dos meses por defecto, es donde mas mordia. `morfo:check` contra el DOM: calendar y range-calendar PASS. `eidos-lint`: invalid 0 en ambos, morfo-backed 27→29 y 39→41. `docs:check` 0/623. Verificado sobre el ARBOL INDEXADO en un worktree aparte, no sobre el mio: `check` 57 errores, identico a HEAD, cero nuevos; 2154/2154 en morfo + soma + eidos + sema. Los 6 fallos de `contracts.test.ts` que se ven en mi arbol de trabajo son de la otra sesion (waveform, media-player, audio-player, menubar, aura, radio-group, tabs): en el arbol firmado pasan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
6f42eebfe8 |
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve
El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho
componentes y `emerge-open` en tres. No era estetica — un preset de movimiento
engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna
firma y simplemente no animaba, sin romper una sola prueba.
Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos,
256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran
40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y
`handle-drop` ya existian en 10 y 4 componentes.
`validateMorfo` cierra la puerta: un `events[].name` que no empiece por su
familia ahora lanza. Visto fallar antes con un nombre pelado inyectado.
Lo que el renombrado destapo, y va aqui tambien:
- La receta del splitter enganchaba `commit-resize`, muerto desde
`bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como
slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el
catalogo de morfos — el guard que lo habria cazado en su dia.
- La familia `shift` era muda en el canal visual, contra su propia doctrina
(c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya
describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora
tiene firma direccional: sexto atributo del sello (`data-event-direction`,
`forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por
`:dir()`. Medido: LTR -30px/+30px, RTL los invierte.
- El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro
calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues
—media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze`
moria sin pintar un fotograma. Una superficie, una ranura (A-36).
- 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de
decision, el componente vivo). Las docs desfasadas, corregidas; los nueve
DEFECTOS de codigo obsoleto quedan abiertos y sin tocar.
- `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y
habia tres cosas distintas deletreadas «direction».
check en su linea base con 0 errores nuevos por diferencia de conjuntos ·
docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador
medidas con raton real y rAF vivo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
866089a407 |
fix(blocks,canon): el ledger dice la verdad y el arco perceptivo vuelve a sonar
La auditoria del tier tenia los veredictos desemparejados de sus hallazgos: un join por POSICION, y el journal del workflow era de sesion. Reproducido en vivo al re-verificar — el journal devuelve los resultados en otro orden que la entrada. Ledger nuevo con 90 ids estables (AUDIT-blocks-ledger.md), union SIEMPRE por id: 41 arreglados, 34 confirmados, 12 refutados con motivo escrito, 3 dato. La tasa real de refutacion es del 13%, no del 28%. De las 12 ALTA de percepcion, 8 tenian la causa raiz en el CANON. Los blocks componen bien; lo que estaba roto era el arco perceptivo. CANON - Link: la prop color era inerte en subtle/plain (inherit a 0-2-0 ganaba a la paleta a 0-1-0), el hover clavaba primary y el active quedaba tapado por el hover de variante. Medido: el enlace del footer pasa de la tinta del padre a la suya. - Card: prometia BoxProps y no los aplicaba — height="100%" era un atributo inerte. El tipo dice la verdad y el eje de tamano se aplica con un helper compartido (buildSizeStyle). Tarjetas al fin de igual alto. - Form.Submit / Form.Reset: componen el Button del canon, asi que la accion principal de un formulario recupera el contact-activate en el gesto. Y el aria-label generico deja de pisar el texto propio (WCAG 2.5.3 para todo consumidor), verificado con el AX tree de Chrome. - Field: dejaba de duplicar commit-submit dentro de un Form — un Enter emitia dos commits con intents contradictorios y dos earcons. - NavigationMenu: commit-select se mueve al Link (navegar es el acto evaluable), el despliegue habla como emerge-open/close en vez de fingir una seleccion por hover, y el pack casa por fin con la parte que recibe el estampado — antes sonaba a la ganancia base, 10x lo disenado. - Badge: el boton de quitar compone IconButton; la altura del chip pasa a ser la del control, asi que md significa lo mismo en todo el sistema. - Fundacion: [data-on] arrastra la propiedad color, no solo las variables. BLOCKS - hero: la CTA secundaria pasa de 1.61:1 a 6.61:1. - contact: separa incomplete de invalid — el camino de error por campo era inalcanzable por construccion — y su frase llega a la AT. - newsletter: coordina (fase 4 del plan). Maquina de cinco estados, palabras propias, Submit y Reason como partes que leen el contexto. - site-header ya no congela la pagina al cruzar el breakpoint; site-footer emite su commit; pricing no se vacia; cta llega a sangre de verdad. DOCTRINA - La frontera dura 1 nombra $libs/forms como puerta sancionada. - El contrato B admite un segundo servicio: el anunciador. Un block que posee las palabras de sus estados tiene que poder decirlas. Gates: blocks:check 15/0 · vitest 20/20 en blocks y 402/402 en eidos+blocks · svelte-check 75/54 (linea base) · docs:check 0/0 · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
07515eedf5 |
fix(motion,demos): dos desviaciones propias, encontradas auditando la sesion
1. La regla `:dir()` de `3c3d8e1ac` nacia SIN ACOTAR, o sea matcheaba cada elemento del documento para declarar una custom property que solo lee el nodo animado. Acotada a `[data-animation-style]:dir(...)`: `:dir()` se resuelve contra ese nodo, asi que el caso del subarbol anidado sigue cubierto y el coste deja de ser global. Medido igual que antes — origin 65.05px en RTL / 0px en LTR, con el borde de anclaje clavado en 0. 2. `releasePosition()` en la demo del float-panel (`1857c7854`) soltaba la posicion SIEMPRE, tambien con `useAnchor` apagado: cambiar `side` en modo libre re-centraba un panel colocado a mano. Condicionado al ancla, que es lo unico con lo que la posicion fijada es excluyente. Medido: con ancla off el panel se queda en (24,24); con ancla on sigue re-sembrando (start x=234 · end x=61). Y el hueco de metodo que las destapo: habia tocado `render-css.ts` —el generador de CSS de TODO eidos— corriendo solo los tests del ambito. La pasada COMPLETA da 8 fallos en 4 ficheros, y verifique que ninguno es mio cruzando lo que señalan contra `git diff --name-only 40db0b981..HEAD`: audio-player (sonido), aura + menubar (eje agentico), orca (hook timeout), soma-attr-audit (pasa en solitario: flaky bajo carga). Queda anotado en 9.7 un cabo suelto AJENO: 6.7 retiro la entrada de `data-ready` de `component-visual-attrs.test.ts` al migrar tabs, pero `contracts.test.ts` sigue marcando `data-ready` en radio-group y tabs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
3c3d8e1ac8 |
fix(motion): las barras horizontales crecian desde su punta en RTL
`grow-x` clavaba `transform-origin: 0% 50%` — el borde fisico izquierdo. En RTL la barra se dispone contra el borde DERECHO, asi que la entrada la hacia crecer desde su punta de vuelta a su base, despegandose de su ancla. Medido en los dos consumidores, con la direccion movida por el toggle del topbar: bar-segment borde izq. clavado en 296, el derecho avanza 108 -> 0 bar-list borde izq. clavado en 239, y el de ANCLAJE se aleja 65 -> 3 `transform-origin` no tiene forma logica en CSS, asi que el preset lee ahora `--motion-origin-inline-start`, que `render-css.ts` emite por `:dir()` — el mismo patron que `scale-fade` ya usa con `--floating-transform-origin`. La var HEREDA (al reves que el indice de stagger, que es `inherits: false`): es lo que permite que un `:dir()` de un ancestro alcance al nodo animado. Se declaran las DOS direcciones para que un subarbol que redeclare la suya tambien acierte — la misma razon por la que `[dir='rtl']` esta prohibido. Tras el arreglo el borde de anclaje queda CLAVADO en 0 y la punta avanza, en ambas direcciones. LTR intacto (`origin: 0px`, ancla en 0). Verificado a ojo congelando la animacion al 45%: las cinco barras del bar-list nacen pegadas a su ancla —derecha en RTL, izquierda en LTR— con el stagger escalonado. RTL-1 no podia verlo: es `transform-origin` en un preset de TypeScript, ni siquiera CSS estatico. `metrics-progress` NO usa `grow-x`; los afectados eran solo esos dos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4356020e37 |
feat(audio-player): la caratula de `card` sale a token, como sus dos hermanas
`row` y `bar` tenian tope publico (--audio-player-artwork-size-{row,bar}) y
`card` lo llevaba clavado en la receta como `calc(var(--space-8) * 8)`: un
tema podia retocar dos de las tres caratulas y la tercera no. Ahora son tres.
El valor se preserva EXACTO — 256px a densidad 1x / zoom 1x, verificado
resolviendo la custom prop en el navegador (el viewport del panel es 0, asi
que se mide la cascada y no el valor usado). Cero cambio visual.
Se conserva tambien la asimetria de FORMA, que no es un descuido y queda
escrita: `card` va sobre la escala de espacios (--space-8 ya trae densidad x
zoom compuestos) para que la caratula heroica respire con la fundacion,
mientras `row` y `bar` son rem planos a proposito — una fila de lista y una
barra de app no deben crecer con la densidad o dejan de casar con las filas
y barras de alrededor.
Verificado: recipe-css-contract 31 verdes (el guard de huerfanos exige que un
token declarado se consuma), eidos-lint media-player 0 invalidos,
component:audit --only media-player PASS, docs:check 0, check 75 = baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
9050125076 |
fix(waveform): el buffered vuelve a ser banda recta, y la demo estrena material real
BUFFERED — segunda correccion por MIRADA, y la definitiva.
Las dos formas se construyeron y se miraron. Primero fue el SecondaryRange del
Slider con un velo neutro al 14%: rechazado ("el secundario no puede ser del
mismo color"), y la medicion le dio la razon — velo y onda eran alfas del MISMO
token (--color-content-primary al 14% vs 28%), o sea el mismo gris. Se paso
entonces a una tercera copia recortada del path, con la idea de que "el buffered
ES la onda un escalon mas alla". TAMBIEN rechazado, y por un motivo mas fuerte:
"se hace inapreciable y confunde visualmente".
La leccion, que queda escrita en los dos README: al compartir silueta con la
senal, el ojo lee la banda COMO la senal — es ilegible POR CONSTRUCCION y ningun
color la salva. La forma recta es legible precisamente porque NO es la onda. Asi
que el buffered vuelve al Slider.SecondaryRange (G-2), la parte canonica que ya
existia para esto, en vez de que el waveform pinte lo suyo.
El COLOR si sobrevive de la iteracion anterior: acento lavado
(color-mix del primary-solid con content-primary), que se separa del gris en
TONO y no solo en luminosidad. Tokens de paleta, cero literales. Los tres
colores de la onda son publicos y retunables por el disenador.
Nota de metodo: la distancia RGB avalo un gris que el ojo rechazo al instante
(esa metrica esta dominada por la luminosidad y es ciega al croma), y ademas un
bug propio en la alfa del color-mix dio numeros inflados que se corrigieron. Dos
recordatorios de que la medicion informa pero no decide.
DEMO — material real. El control se llamaba `signal`, que es una de las 8
FAMILIAS del canon, en un componente que declara CERO eventos
(expression: 'delegated'): ensenaba lo contrario de la verdad. Renombrado a
`sample`. Y el material eran los earcons de sema (pitidos de 80-220 ms, `pop`
salia casi plano) porque eran el unico audio del repo; ahora arranca en una
pista real de 2:45 (static/music/t2.wav, aportada por el usuario).
Y un defecto que 30 MB destaparon: el efecto de carga dependia de `source` Y de
`buckets`, asi que mover el slider de buckets volvia a DESCARGAR el fichero
entero. Separado en dos: decode una vez por fuente (caro), rebucketing puro y
barato. Medido: t2 (5 MB, 86 ms) frente al master de 48 kHz/16 bit (30 MB,
186 ms); sus envolventes NO son identicas — delta maximo 0.298 por cubo, medio
0.048, porque los 8 bits suben el suelo de ruido y aplanan los pasajes suaves.
Se elige t2 igualmente: 6x mas ligero por una envolvente marginalmente mas fiel.
Verificado: check 75 = baseline, 0 propios; 35 tests; eidos-lint 0 invalidos;
recipe-css-contract verde; component:audit --only waveform PASS; docs:check 0;
sonda en vivo — 2 paths en la onda, banda recta al 66% en rgb(130,95,161).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
4e4f1b612d |
feat(waveform): F4 eidos + F5 demo — y el buffered pasa a ser la onda
F4 (eidos): wrapper raiz que carga slider.css + waveform.css y resuelve
data-size por ActiveEidos; Wave passthrough; recipe con bars = stroke en
UNIDADES DE VIEWBOX (el svg estira con preserveAspectRatio="none", asi el
ciclo barra/hueco sobrevive) y wave = fill sin stroke; espejo RTL en CSS;
el Slider embebido en scope con track/range transparentes y el thumb
convertido en playhead de 2px a alto completo. Tokens waveform:{height-*,
bar-width, color, color-buffered, color-played, playhead-width} —
playhead-width y color-buffered van ANADIDOS sobre la lista firmada en el
gate, ambos declarados en el README.
F5 (demo + dossier): /uix/components/waveform con picos REALES (fetch de
/sounds/*.wav -> uix.sound.decode -> uix.sound.peaks), cada prop como
control vivo, READMEs de soma y eidos. Fix arrastrado de F3: `dir` no se
reenviaba al Slider embebido.
BUFFERED — cambio de forma (decision del usuario tras ver el render).
El primer intento re-tintaba el SecondaryRange del Slider; el usuario lo
rechazo ("el secundario no puede ser del mismo color") y la auditoria
encontro por que ningun color lo arreglaba del todo: ese part es una BARRA
de 6px con la geometria del track, y sobre una pintura de barras al ~58%
de ciclo el 42% de su area cae sobre fondo desnudo, ademas de tenir el
tramo ya reproducido. Ahora el buffered es una TERCERA COPIA del mismo
path, recortada a su fraccion — es la onda un escalon mas alla, la lectura
de SoundCloud/wavesurfer. El waveform ya no monta Slider.SecondaryRange.
Escalera: color 28% -> color-buffered 55% -> color-played acento.
Guard nuevo (bufferedFraction: ausente sin valor, clampado, dominio
degenerado) VISTO EN ROJO desconectando el cableado antes de darlo por
bueno.
Verificado: check 75 = baseline, 0 propios; 5/5 soma + 4/4 morfo;
component:audit --only waveform PASS; eidos-lint 0 invalidos;
recipe-css-contract y motion.test verdes; docs:check 0; prettier limpio.
Queda F6 (mirada del usuario): el panel del navegador no composita, asi
que la verificacion fue por sonda del DOM, sin captura.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
c4d64ba98a |
feat(media-player): modo audio de referencia — AudioLayout, <AudioPlayer> con 4 variantes y buffer real (F4-F7 parcial)
El modo audio deja de ser "video colapsado" (las 3 reglas CSS de v1):
- F4 sema: el docblock del pack escribe la doctrina D-AP2.7 — el silencio
mientras suena la obra lo posee el SERVICIO (duckUiWhileContent, receta
documentada); la resta-de-gain queda disenada sin cablear a proposito.
- F5 buffer real (G-2 consumido): el TimeSlider soma pasa bufferedTime como
secondaryValue del Slider compuesto y monta Slider.SecondaryRange; se
retiran el div .mp-buffered, la var --media-buffered (y su writeProperty),
--_mp-buffered y --_mp-rail. Look intacto: el 42% pasa a
--slider-secondary-bg en scope.
- F5 wrappers eidos de las 6 partes: Artwork compone AspectRatio+Image del
catalogo via child; RateButton compone Button ghost mostrando {data-rate}x;
Artist/Identity/Transport/LiveIndicator passthrough.
- F5 AudioLayout: espejo de DefaultControls para audio — identidad
(artwork+title+artist+live) + transporte + fila de scrubber + secundarios.
- F5 raiz competidora <AudioPlayer> (eidos/components/audio-player/, el
precedente Toast/Toaster): una etiqueta monta todo, y la metadata del SO se
DERIVA de la identidad visible (title/artist/artworkSrc) salvo override —
lo que ves es lo que muestra el lock screen (D-AP2.5).
- F5 recipe de 4 variantes (card/row/bar/inline) scoped a [data-variant]
(componer a mano sigue sin opinar) + tokens publicos --audio-player-*
(gap, artwork-radius, artwork-size-row/bar), regenerados.
- F7 parcial: el badge live usa el text localizado del morfo via soma (fuera
el "LIVE" hardcodeado) · demo con el modo audio real (<AudioPlayer> +
selector de variante + snippet con paridad) · README eidos con la seccion
del modo audio · component:audit --only media-player PASS.
Verificacion: player 18/18 · recipe-css-contract + component-api-contract
37/37 · eidos-lint media-player 0 invalidos · check 74 = baseline, 0 propios
(demo incluida). Declarado: audio-player.css es recipe de composicion sobre
el morfo del player (el lint per-component no la cubre — gap del tooling);
los internos --_mp-* los comparte la recipe hermana del MISMO morfo.
Quedan F6 (verificacion VISUAL de las 4 variantes — exige ojos) y el resto
de F7 (medicion Chrome del escenario mixto + teclas de medios + stamp sema).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
2f872ef864 |
feat(sound): $sound orquestador (buses+voces+media) y player v2 F1-F3 sobre el servicio
Rediseno clean-room del sonido (gate D-SR.1-12 firmado; la doctrina "la voz
es del art" de PLAN-sound-engine s16.1 queda revocada):
- Mezcla: master -> buses ui/content (gain/mute/duck refcount, gana la mas
fuerte; politica pre-contexto). prefs de UI jamas tocan la obra.
- Voces registradas: la calibracion es DATO del consumidor (sema registra
'sema'; el default conserva la calibracion historica - cero cambio audible,
A/B del usuario: "suena igual").
- Ciudadania de media: sound.media(el) PROVEE el transporte (la forma exacta
del puerto MediaProvider) + foco mixed/exclusive/duck PERSISTENTE (estado
derivado; el ducker mas reciente retiene la palabra) + duckUiWhileContent +
MediaSession de la fuente activa (play/pause/seekto + posicion, liberacion
y promocion) + visibilidad content-aware + attach() opt-in (irreversible,
CORS declarado). Re-registro idempotente por elemento.
- diagnostics.ts con catalogo tipado (contrato de arts) y guards post-dispose
(ningun camino abre un contexto huerfano). Auditoria AU-1..9 resuelta;
bundle medido: 16,2 KB min / 5,6 KB gz.
Player v2 (gate D-AP2.1-13 firmado) F1-F3:
- F1 gaps de framework: aria-valuetext en Slider (valueText por thumb) +
parte SecondaryRange con token --slider-secondary-bg (buffer/clip/capitulos)
+ formatDuration en $libs/days + fix del selector del pack (H-1: la regla
de play/pause construia sobre provider y el stamp aterriza en play-button).
- F2 morfo: 6 partes audio-only (artwork/artist/identity/transport/
rate-button/live-indicator), commit-set-rate, commit-set-time RETIRADO
(D-AP2.13: el scrubber delega en el commit-set del Slider compuesto),
sustain-loading cableado (H-9), teclado j/l, </> y 0-9 (con guard de
modificadores para no pisar atajos del navegador).
- F3 la migracion: el transporte default es uix.sound.media(el, {metadata})
(pineado con el motor real); nativeMediaProvider RETIRADO (media-provider.ts
queda como contrato puro del puerto, sin shim); G-5: los commits de
play/mute/rate cabalgan el evento de RESULTADO (play/pause saltando el
pause de ended, volumechange con transicion de muted, ratechange);
formatTime -> formatDuration; sub-providers + wrappers + exports de las 6
partes; prop metadata publica (captura al registrar, documentado).
Verificacion: 45 suites / 458 tests verdes en el alcance (sound, sema,
active-uix, active-app, morfo, slider, media-player, days) - check 74 =
baseline de la rama, 0 propios - docs:check 0 - invariante de UN AudioContext
intacta (e2e sin tocar). Planes de proceso y handoff al dia
(PLAN-sound-redesign, PLAN-audio-player-v2, CONTINUE-sound-engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2 months ago |
|
|
d4c49aedbf |
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas transcritas de la especificación o derivadas de su regla de construcción, sin una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki. Ningún headless del mercado trae barcode: el terreno lo ocupan encoders imperativos sin anatomía ni a11y. - **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`) es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox (precedente: la familia `chart`) y no el token `size` cuadrado del QR: colapsar los dos ejes en uno haría que la X dependiera de la altura, que es justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de cada estándar. - **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7 módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva su barra portadora por fuera de la zona muda, que no puede comerse. - **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un `BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por `onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría la página. - **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que tiene su propia lista literal. Añadir solo la unión de TypeScript compila y falla en runtime; lo cazó `morfo:check`, no `npm run check`. Verificación: 48 tests de encoder en tres capas (invariantes estructurales de cada tabla · vectores publicados transcritos aparte · decoders independientes que releen la retícula), la lectura real confirmada con escáner por el usuario, y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit` PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da `button`, la demo canaria: sus regex buscan la forma v1 inline del MutationObserver y del `emit`, que la v2 movió al harness compartido. Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode, GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en `next-features.md` §10 — el hueco de numeración se cierra cuando el track de blocks commitee sus §8/§9, hoy sin commitear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
75e1fe32a1 |
uix(sidebar): review adversarial · 31 hallazgos confirmados, arreglados
Review de 6 dimensiones × 3 verificadores escépticos (135 agentes): 43 hallazgos brutos → 31 confirmados, 12 rechazados. Todos los confirmados arreglados y verificados en navegador real. Estado (los dos ejes) - `collapsible='none'` ya no deja el sidebar INALCANZABLE en móvil: la inercia del modo se limita al escritorio, que es donde «nunca colapsa» significa algo. En móvil el panel es un Drawer que arranca cerrado, el raíl está oculto y el drawer no tiene trigger propio: con el toggle inerte no había forma humana de abrir la navegación en un teléfono. - `'none'` PINTA el estado a expandido: antes `data-state`, `aria-expanded` y `useSidebar().open` podían decir «colapsado» sobre un panel plenamente visible, y ninguna regla del recipe ni el toggle podían reconciliarlo. - El estado del drawer móvil se DESCARTA al salir de la presentación móvil: abrirlo en un teléfono y rotar dos veces remontaba el Drawer ya abierto, con overlay, foco atrapado y scroll bloqueado sin tocar nada. Submenú flotante (era el nudo con más hallazgos) - Cierra también por PUNTERO: se abría en `pointerenter` de la fila, pero el único cierre por puntero vivía en el propio flyout — salir hacia la página sin cruzarlo lo dejaba pintado para siempre. - Escape funciona desde donde el foco ESTÁ (la fila), no solo dentro del sub. - Entrar/salir se resuelve en el `menu-item`, que contiene fila Y sub, así que viajar entre ambos no lo cierra. - El flag se limpia al cambiar de presentación: expandir y volver a colapsar reabría un flyout que nadie había tocado. - El motor flotante se engancha solo cuando el sub está ABIERTO, no por modo: antes `autoUpdate` (rAF + observers) corría por cada fila del raíl. - `hasSub` deja de latir a true: se limpia en el teardown del sub. A11y - Las filas del raíl recuperan NOMBRE: un tooltip solo describe (`aria-describedby`), así que con la etiqueta oculta el texto de `tooltip` pasa también a `aria-label` — incluidas las filas con submenú, que no llevan tooltip visible. - El panel off-canvas colapsado sale del tab order y del árbol de accesibilidad (`visibility: hidden` con la transición retrasada para que el deslizamiento siga animando). - El diálogo móvil recibe el mismo nombre que el landmark. - El raíl se llama «Contraer barra lateral» (lo que hace) en vez de «Redimensionar», que prometía un arrastre fuera de alcance; cursor de puntero en vez de `ew-resize`. Recipe - El signo del off-canvas se deriva de side × dirección: en RTL la fila flex se invierte, así que el negativo fijo barría el panel POR ENCIMA de la página en vez de sacarlo por su borde. - El flyout declara banda `z-index` (nuevo token `--sidebar-sub-z`); era la única superficie flotante del ecosistema sin ella. - La transición de `inline-size` (layout por frame) queda SOLO bajo el modo icono; la base anima únicamente `transform`. - El `Drawer` es el único dueño del ancho móvil (dos dueños dejaban una banda de cromo en dos tonos) y se compone con `dragToDismiss`. - Las reglas de modo icono ya no alcanzan el subárbol flotante: sus filas conservan badges y acciones. - Bloque `prefers-reduced-motion` — la traslación del panel es la mayor del sistema. Demo - Copias corregidas donde afirmaban de más: el tooltip ahora sí NOMBRA (y se explica por qué), y los anchos son rem fijos a propósito (no los alcanza el escalado; sí el interior). - Tabla de eventos y de teclado en la pestaña morfo; `onOpenChange` y una tabla de props por parte en API. - Los chips de estado se inhabilitan en `collapsible='none'`, que es lo que el componente hace; botón «show me» que pone icon + colapsado de una vez. - El trigger pasa a ser solo icono: el morfo le da `aria-label`, que pisaba cualquier texto visible. Verificado en navegador: nombres accesibles en el raíl · flyout que cierra por puntero, por foco y por Escape (con retorno de foco) y que no resucita al cambiar de modo · off-canvas fuera del tab order · RTL sacando el panel por su propio borde · z-index 80 en el flyout · drawer móvil nombrado, a ancho completo y con el panel a ras · `collapsible='none'` abrible en un teléfono. Gates: audit PASS 0E/0W · eidos-lint 47 morfo-backed / 0 invalid · svelte-check 0 errores propios · vitest src/uix/eidos 353/353 · contracts.test solo con los 3 fallos ajenos conocidos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
59f20e2b1f |
uix(aura): el Status en dos líneas — cierra D1
La otra mitad de la dirección firmada: el anillo dice el estado como FORMA, pero la respuesta a «¿quién actúa?» es texto, y una palabra gris de 13px junto a un orbe vivo no la daba. Ahora el Status son dos renglones: Esperando tu revisión ← el estado, content-primary, peso medium la custodia es tuya ← qué significa para ti, secondary, un escalón menos Sin parte nueva: el Status ya existía y añadir un `Detail` habría sido abstracción prematura (y una pelea de layout — dos hermanos no apilan sin envoltorio). El soma renderiza las dos líneas por defecto y expone `detailText` en los snippet props, así que un consumidor que traiga sus children sigue mandando. La copia del detalle es FIJA por estado y localizada como la etiqueta (es/en): los contadores reales del run (turnos / actos) NO están en el puerto estructural, y ensancharlo para un subtítulo más bonito es una decisión, no un defecto. Queda dicho en el README por si algún día se firma. Tokens: `--aura-status-color` pasa a `content-primary` (era secondary: el peso informativo estaba en el sitio equivocado), + `-weight`, `-gap` y `-detail-color`. Verificado en Chrome real, claro y oscuro, con un run en `reviewing`: peso 500, primary sobre secondary, y las dos líneas escalando con `data-size`. Tests 478, eidos-lint 0 invalid / 0 class-hooks, morfo:vocabulary 0 errores, check en los 73 ajenos preexistentes. Con esto D1 queda cerrada entera (anillo + jerarquía). Handoff al día. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
e10eed98cc |
uix(aura): la superficie visible del agente — anillo de custodia, orbe-contorno y desenlaces expresados
Rediseño dirigido por el usuario sobre un banco de mockups con el orbe REAL
corriendo dentro. Seis frentes, todos con maquinaria existente:
ANILLO DE CUSTODIA (parte nueva `Aura.Ring`, morfo+soma+eidos)
El estado se lee como FORMA y no solo como tinte: raíl punteado en reposo,
arco que respira al ofrecerse, segmentos = pasos del plan (laten), dos arcos
enfrentados y QUIETOS esperando tu revisión (la quietud lo separa de acting),
arco que gira actuando, anillo cerrado + pulso saliente cuando te necesita.
Envuelve al Indicator (es su marco de referencia, no un hermano posicionado a
mano). El movimiento continuo son loops REGISTRADOS (data-animation-style),
nunca @keyframes de receta (R-4.5) — vienen con su puerta de reduced-motion,
donde el dash sigue distinguiendo el estado. El attr va en los hijos del SVG,
jamás en la parte: motionAttrs() estampa además data-state, del morfo ahí.
EL ORBE ES UN CONTORNO
`thickness` en el efecto orb; el hueco es real (alfa propia) sobre cualquier
fondo. Se va la rama claro/oscuro y el parámetro backgroundColor: el shader
mezclaba con el fondo y derivaba el alfa del canal máximo, o sea pintaba un
disco OPACO del color de la superficie justo en el centro (en claro un círculo
relleno; en oscuro el azul del seed). Y la receta deja de pintar su disco en
cuanto monta el canvas (:has(canvas)) — solo sobrevive donde no hay render
vivo: SSR, tests, effect={false}, sin WebGL.
COLOR = TOKENS DE ROL, PINTADO Y NO ROTADO
colorA/colorB/colorDeep (solid / solid-hover / element) resueltos con
eidos.resolveToken y pasados como hex, que es el seam que el propio arte
documenta. El hue-delta anterior se medía en OKLCH y se aplicaba en YIQ: risk
salía VERDE junto a su propio anillo naranja.
ONION EN EL IDIOMA DEL ORBE
Prop `look="solid" | "luminous"` (defecto solid: nadie más se entera). En
luminous el sector no tiene relleno — filo de luz (pasada borrosa + nítida) y
etiqueta en el color del sector; la cuña sigue siendo área táctil. El trigger
con snippet propio va BARE: sin disco, sin sombra FAB y sin el salto del hover
(el orbe ES el trigger, no su contenido). Medio anillo bottom-center y radios
44/40. El halo respira (loop pulse) y en commit-select da UNA pasada de ping
pivotando en el centro real del menú — la firma genérica de commit anima
box-shadow, que sobre un path de SVG no hace nada.
DESENLACES EXPRESADOS (el hueco real: un fallo era indistinguible de un éxito)
Tres eventos terminales con intent INTRÍNSECO — delegate-return (fulfill) /
-aborted (sin intent: un decline es ausencia, no pérdida) / -failed (loss) —
en vez de un intent dinámico, que no tiene dónde vivir: TriggerOptions no
lleva intent y el fromProp del morfo ata a un prop PÚBLICO, no a estado de
máquina. Puerto `runs?` opcional en AuraAgentSource porque activeRun ya es
null al cerrar (§0: sin él, anuncio genérico). Primeras firmas de la familia
delegate en el registro; la de loss es MOVIMIENTO (return-recoil: retirada +
descenso), no tinte — no estrena ningún hue.
MAPA SEMÁNTICO DE COLOR (demo)
Un hue = un significado en toda la superficie: gris=sin agencia ·
naranja=tu revisión · morado=él actúa · rojo=escalated (exclusivo) ·
teal=tu sí. El anillo de autoridad reusa los colores de la custodia con el
MISMO sentido (Review=risk porque vivirás en reviewing; Auto=primary porque
actúa) y los declines pasan a neutral — canon: el intent de un cancel es
`absent`. Además el anillo en reposo DELEGA (las tres autoridades abren el
run) y se cierra por transición, no siempre que no hay run.
Dos guardas del framework corrigieron el diseño por el camino:
- BK-FRAME-NO-INTENT rechazó dar intent a una familia de marco sin
justificación escrita (excepción A-1): queda el intentRationale — el motor
no emite nada y no hay evento evaluable que enmarcar, así que el retorno
consolidado es el ÚNICO sitio expresivo del desenlace.
- fallbackTarget gana sobre el target del morfo (contrato de TriggerOptions):
el sello caía en el provider. Los terminales pasan el Ring cuando existe.
Verificado en Chrome real (ventana delante: en segundo plano el navegador
congela rAF y ResizeObserver y las medidas mienten): seis estados en claro y
oscuro, y un error de transporte REAL apuntando a un puerto muerto →
delegate-return-failed · loss · return-recoil sobre el Ring · región
«Agente: falló — control devuelto».
Tests 478 (morfo 114 · soma/aura 11, +5 casos de desenlace · eidos 353),
eidos-lint 0 invalid / 0 class-hooks nuevos, morfo:vocabulary limpio,
check en los 73 errores ajenos preexistentes.
Pendiente (handoff actualizado): la jerarquía tipográfica del Status (D1
llevaba dos líneas y solo se hizo el anillo) · el pulso de commit-select se
corta si el consumidor cierra el anillo al elegir (necesita retención tipo
Presence) · D5 la superficie de composición del encargo, decisión abierta.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
3 months ago |
|
|
21bbf9f506 |
uix(sidebar): F1.7 eidos + demo · raíl, modo icono y submenú flotante
Segunda tanda del sidebar: la capa visual, la demo canónica de 9 pestañas y la verificación en navegador real. Con esto el componente está vivo; queda el review adversarial. Eidos - Recipe con los dos ejes: off-canvas por `transform` (trabajo de compositor, el inset no reflowea) y `width` animado SOLO en modo icono, donde el inset sí tiene que seguir al raíl. El raíl NO lleva `will-change` (una franja fina tiembla a DPR fraccionario — memoria NavMenu). - Anchos como TOKENS: `--sidebar-width` 16rem · `--sidebar-width-icon` 3rem · `--sidebar-width-mobile` 18rem, más raíl, gaps, altura de fila y colores. - Tres composiciones que la capa headless no puede hacer: `Drawer` para la presentación móvil, `Tooltip` para nombrar filas en modo icono y `Badge` para los contadores. El trigger compone `Button` por el patrón `child`. - Convención de icono documentada: sobrevive el PRIMER elemento de la fila. Arreglos que destapó el navegador - **`mobile` en SSR**: sin viewport, `isAtLeast` responde «por debajo de todo» → el HTML salía con `data-mobile` y la hidratación NO lo corrige (no diffea atributos y el valor ya no cambia). Viewport desconocido = ESCRITORIO. - **Estado móvil propio** (arranca cerrado): en escritorio `open` es preferencia de layout persistida; en móvil sería un overlay tapando la página, que jamás puede ser el estado inicial. `onOpenChange` solo se dispara con el estado de escritorio. - **`open` controlado/no controlado**: se adopta el patrón de la casa (`$bindable` con default, el provider escribe siempre) — mi versión previa con estado interno no propagaba `bind:open`. - **El submenú flotante no se posicionaba**: `FloatingAnchor` resuelve su provider por CONTEXTO y el eidos envuelve la fila en un `Tooltip`, que publica el suyo → el ancla caía en el tooltip y el submenú se quedaba en `translate(0,-200%)`. Ahora el ancla se cablea directamente al provider flotante del item. Además el `<ul>` ya no se posiciona a sí mismo (colapsaba el wrapper a tamaño cero) y toma elevación del plano `overlay` por `data-depth`, como cualquier panel flotante del ecosistema. - **Tooltip vs submenú**: una fila con submenú no lleva tooltip en modo icono (dos superficies sobre el mismo ancla se pisan) — nuevo `useSidebarItemOr()`. - El item envuelve (`flex-wrap`) para que el submenú caiga en su propia línea en vez de ponerse al lado de la fila. Demo: shell de app real (grupos, badge, acción, submenú, fila deshabilitada), controles vivos de los tres props y las 9 pestañas del canon. Verificado en navegador real (Playwright): dos landmarks nombrados en el idioma activo · `aria-current="page"` en la fila activa · cada menú etiquetado por SU grupo · raíl enfocable que alterna con Enter · colapso 256→48px · off-canvas por transform · submenú flotante anclado a su fila y Escape que devuelve el foco · sema `emerge-expand/collapse` estampando en `panel` · móvil 600px = Drawer cerrado con el landmark dentro · RTL espeja el shell entero · oscuro · 0 errores de consola. Gates: `component:audit --only sidebar` PASS 0E/0W · eidos-lint 45 morfo-backed / 0 invalid / 0 class-hooks · `svelte-check` 0 errores propios · `vitest src/uix/eidos` 353/353 · `contracts.test` solo con fallos ajenos (menubar, radio-group y las claves camelCase de aura). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
e835163103 |
uix(nav-tree): F1.8 WIP · data-driven docs nav tree (soma+sema+eidos)
Complete component, green on all static gates (audit PASS, eidos-lint 20/0, svelte-check 0, contracts clean for its parts). NOT tree-view: APG Disclosure Navigation (<nav> landmark + native links + disclosure groups), data-driven (E-1) — the app passes `nodes` + `activeHref`, the provider resolves the active node and auto-expands its ancestor trail. - morfo: nav/list/item/trigger/link/group; emerge-expand/collapse events - soma: NavTreeProvider (shared state, pure isExpanded query) + per-node NavTreeItemProvider (menubar pattern, recursive render) - sema pack: soft emerge on group disclosure (mirrors collapsible) - eidos: rail + per-depth indent recipe; own bespoke rows (not composed Link), Badge deferred to a Gap; aria-current="page" from data-active Remaining before F1.8 closes: demo + browser verify + adversarial review. Handoff: docs/process/CONTINUE-nav-tree.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
fe2d43b7ac |
uix(anchor-nav): F1.6 · scrollspy TOC (soma+eidos) + hardening
Nav landmark + native anchors; IntersectionObserver band detection (never a scroll listener + getBoundingClientRect — anti-reflow doctrine). aria-current="location", per-depth rail indent, data-active drives the eidos accent. Real-IO chromium test: band tracking + bottom dead-zone. Adversarial-review hardening: - ref-count register/unregister so duplicate hrefs / mount churn can't drop a still-referenced target (+ test) - tokenize the rail transition (--duration-fast/--ease-default) and the focus outline-offset (calc(var(--focus-ring-width) * -1)) - document two v1 limits (instant-jump dead-zone, link-order == section order) — both need a v2 observation-contract change Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
5b84749ee5 |
uix(prose): F1.5 · tipografía editorial con auto-exclusión de componentes embebidos
Quinto componente F1 del plan blocks (eidos-only; alcance E-2 = suelo del
dossier §P4). Estiliza HTML/markdown RENDERIZADO que el app pasa como children.
EL DIFERENCIADOR (que ninguna referencia puede): cada regla de elemento se
emite a especificidad CERO vía `:where([data-prose] EL)`. Como todo componente
del framework se estiliza por `[data-{component}]` (≥0,1,0), un <Button> /
<Callout> / <Card> embebido conserva su PROPIO estilo AUTOMÁTICAMENTE — sin
anotación `not-prose`. Verificado en navegador: un <Callout> dentro de prose
mantuvo su tinte affirm, borde-acento 3px y layout grid en claro Y oscuro; las
reglas :where de prose no lo tocaron. El donut `:not(:where([data-prose-ignore],
…))` es el opt-out explícito para subárboles HTML crudos.
- morfo: 2 partes (provider bare data-prose + ignore documenta el attr opt-out;
eidos-lint lo ve morfo-backed). Selectores de elemento = estructurales bajo la
parte (S1). 0 eventos, apg:none, passive
- recipe: cobertura de paridad (h1–h6 incl. h5/h6 que tailwind NO; p, a, strong/
em, blockquote borde-acento lógico, ul/ol + ::marker, dl, code/pre + reset kbd,
img/figure/figcaption, hr, table); ESCALA EM-relativa (un font-size raíz
re-deriva todo — no la escala 5× de tailwind); overflow de tabla estilo GitHub
sin wrapper (display:block+overflow); trim de bordes; SIN backticks en code
(doblarían sobre <Code>); dark GRATIS por roles (no el patrón -invert);
propiedades lógicas → RTL correcto
- decisión :where (no @scope): :where establecido en el codebase + soporte 2021+;
@scope diferido a Gaps (mitad de tamaño pero «Baseline newly available» sin
precedente aquí)
- demo v2 9 tabs con markdown real + <Callout>/<Button> embebidos (prueba de
auto-exclusión) + ejes size/measure; README (Comparativa 5 refs · Decisiones ·
Passive · Gaps con disposición)
Verificación: component:audit PASS 0E/0W (21 literales em con /* literal */
justificado + eidosSnippet) · eidos-lint 45 morfo-backed/0 invalid ·
recipe-css-contract 30/30 · svelte-check sin errores propios · navegador:
h1 36px/bold, h2 24px, medida 65ch (~692px), blockquote acento, tabla
display:block+overflow, code chip, AUTO-EXCLUSIÓN del Callout en claro y oscuro.
Nota: nav sidebar fuera del commit (flip CRLF ajeno); índice langs reconstruido
prose-only (entangle con aura sin commitear de sesión paralela).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
3 months ago |
|
|
998b11d9bc |
uix(sticky): F1.1 · position:sticky con detección de estado (soma+eidos) — el primer behavioral
Primer componente F1 con comportamiento real (soma), y el que desbloquea el block site-header (F2.1). Construido tras un fan-out de investigación (6 lectores) que fijó el mapa: template = FeedSentinelProvider, motor = IntersectionObserver por el port bendecido de $adom, escritura de attrs por syncAttrs (nunca dom.apply crudo). Alcance E-2 = suelo del dossier §P3. - morfo: 2 partes (provider=box → data-sticky/data-edge/data-stuck · sentinel aria-hidden → data-edge); 0 eventos con justificación de COMPLEX BEHAVIOR (apg: none); data-stuck presente/ausente + data-edge enum espejan @container scroll-state(stuck: top/bottom) — este componente es el polyfill cross-browser de ese contrato CSS futuro - soma: un StickyProvider registra AMBAS partes en un runtime; el observer vive en un $effect que RETORNA observeIntersection(...) como teardown (mirror de feed); el callback async voltea stuck=$state; rootMargin DERIVADO del offset (la línea de disparo del centinela ES la línea de pin — acoplamiento matemático, offset obligatorio); root=scroll-host (default viewport), Document coercionado a null; offset viaja como --_sticky-offset (dato A8, no CSS visual — precedente meter) - eidos: recipe fino que SOLO posiciona (position:sticky + inset lógico desde --_sticky-offset + token --sticky-z-index + centinela layout-neutral 2px con margen negativo); el tratamiento del estado pegado es del consumidor ([data-sticky][data-stuck]) — el recipe posiciona, no decora - centinela = HERMANO de flujo del box (dos nodos raíz), no hijo (un hijo se fija con el box y nunca detecta); orden según edge - demo v2 9 tabs con scroll container real + readout de stuck vivo; READMEs soma + eidos (Comparativa AntD/Mantine/Chrome-technique/headless/CSS · Decisiones · Passive justification · Gaps con disposición) Verificación: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed/0 invalid ([data-stuck] sin regla = intencional, consumidor decora) · morfo:check limpio · svelte-check sin regresión propia · 5 tests: 4 unit (rootMargin por borde, root null-coerce, flip de stuck, disabled, cleanup) + 1 REAL-IntersectionObserver en chromium (foreground: no-stuck→scroll→ stuck→scroll-back→no-stuck — prueba end-to-end de la geometría offset↔ rootMargin, lo que el pane suspendido no puede). Navegador: box pinea a los 12px del offset, recipe correcto. Decisión declarada (test harness): sticky-provider.svelte.test.ts usa el installSomaHarness compartido (translator identidad) como los otros 86 tests de provider — TÉCNICAMENTE en tensión con la regla CLAUDE.md «never fake translators; use createActiveUix». Son unit tests del provider; la integración real la cubren contracts.test.ts + la verificación en navegador. Seguido el precedente universal; flaggeado para tu ratificación. Nota: nav sidebar de sticky (web/routes/uix/+layout@.svelte) fuera del commit — el archivo arrastra un flip CRLF de sesión paralela (2125 líneas de ruido); la entrada viaja en el árbol de trabajo. Registros langs/soma de sticky reconstruidos sticky-only (los índices tenían entangle con aura sin commitear). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
3 months ago |
|
|
9124570e69 |
uix(callout): F1.4 · admonicion inline (morfo+eidos) al suelo del dossier
Tercera pieza F1 del plan blocks (alcance E-2, dossier §P3). Banner = tira de anuncio de pagina; Callout = el aside del documento (split deliberado). - morfo: 4 partes (provider role=note / icon / title / content) + texts con TITULOS DEFAULT LOCALIZADOS por intent (note/tip/warning/caution — patron GitHub de label visible: la semantica nunca viaja solo en color/icono); 0 eventos justificados (dismissible DIFERIDO a pasada soma+sema por la regla de admision — dismiss ES un evento real) - el hueco IMPORTANT resuelto con el modelo Radix, sin 5o enum: `intent` (neutral|affirm|risk|threat — mapea NOTE/TIP/WARNING/CAUTION) + `color` override SOLO bajo neutral (doctrina §4: intent evaluativo gana); IMPORTANT = intent neutral + color + titulo propio (preset en la demo) - pintura sobre la maquinaria C6/THM-2 (patron badge): forwarders por color + slots `_palette-track/text/solid` → el generador emite la cascada de 8 roles Y el forward presence-guarded al shared layer — 33 escalas y colores custom con CERO CSS extra - a11y: role=note NOMBRADO via aria-labelledby → Title SOLO mientras esta montado (registro reactivo por context local); escalacion tipada role=status|alert|none (el role=alert estatico de shadcn = bug de referencia que NO copiamos); Title NO es heading (protege el outline que escaneara anchor-nav); icono decorativo = mapa doctrinal IntentIcon - fix cazado en navegador: el `+=` del registro leia el estado dentro del tracking del $effect del hijo → effect_update_depth silencioso (contador a -997, attr nunca estampado) — untrack() en register/cleanup; leccion registrada en memoria (incidente 2 de la clase) - recipe: grid con acento logico border-inline-start (RTL-correcto), nesting tolerado (Docusaurus); demo v2 9 tabs con preset IMPORTANT + PalettePicker showIntent=false; README completo Verificacion: component:audit PASS 0E/0W · eidos-lint 8 morfo-backed/0 invalid · guards 37/37 · svelte-check 76E/51W = baseline exacto · navegador: «Nota»/«Atencion» localizados · risk=ambar hue 45-60 · IMPORTANT=plum hue 326 resuelto por shared layer · labelledby=titleId · cero errores de consola. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
3 months ago |
|
|
bf940c9a4a |
uix(result): F1.3 · estado terminal de flujo/pagina (morfo+eidos) al suelo del dossier
Segunda pieza F1 del plan blocks (alcance E-2 = suelo de paridad, dossier
§P3). Comparte el esqueleto de empty-state a proposito: EmptyState describe
AUSENCIA de datos, Result reporta un RESULTADO — un lenguaje de layout, dos
contratos (duplicacion consciente registrada en el README; revisable al 3er
consumidor).
- morfo: 6 partes display (provider/media/title/description/actions/extra;
extra SIN archetype — parte genuinamente propia), scope ['eidos'],
0 eventos justificados (el resultado ya OCURRIO antes de renderizar)
- status: enum de 7 = paridad AntD CON `warning` (decision firme del
dossier) y HTTP renombrados semanticos (forbidden/not-found/server-error,
nunca '404' stringly); default 'info'; data-status = attr eidos-only
- media default: compone el mapa doctrinal IntentIcon (success→fulfill ·
error→threat · warning→risk) + Info de catalogo; HTTP = codigo mono
grande NEUTRO aria-hidden (situaciones, no fallos — AntD jamas pinta 404
de rojo); children reemplazan el default entero (context local eidos-only
con getter reactivo)
- Title default h2 (vs h3 de empty-state — Result suele SER la pagina);
Actions con label→role=group; Extra alineado a inicio (detalle que lee)
- recipe: glifo display 3× xl bundle; tokens publicos
gap/actions-gap/measures + {status}-color como forwarders de rol
retintables; sin eje size (paridad AntD, gap diferido)
- demo v2 9 tabs con copy por status + entrada nav (Status) + README
(Comparativa · Decisiones · Passive justification · Gaps con disposicion)
Verificacion: component:audit PASS 0E/0W · eidos-lint 6 morfo-backed + 5
eidos-only sancionados · recipe/api/visual-attrs 37/37 · morfo:check verde
(los 7 fallos listados son deuda foranea preexistente) · svelte-check
76E/51W = baseline exacto · navegador: success=fulfill verde 84px ·
error=threat rojo · not-found=«404» mono neutro, copy conmutando, cero
errores de consola.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
3 months ago |
|
|
f19e91b985 |
uix(empty-state): F1.2 · componente base display (morfo+eidos) al suelo del dossier
Primera pieza F1 del plan blocks (PLAN-blocks.md; alcance E-2 = suelo de paridad, dossier §P3). Ruta de 9 fases completa: - morfo: 5 partes display (provider/media/title/description/actions), scope ['eidos'], 0 eventos con justificacion pasiva (patron renderEmptyState de las refs headless), Title role:'heading', texts.label - langs: components.empty-state.label (es/en) — el indice tambien recoge la retirada foranea del import de words (inseparable por staging de archivo; coherente con la migracion palabras ya enviada; words.ts sigue en su arbol) - eidos: compound EmptyState + Media(kind icon|media, placa 2x glifo) + Title(level 2-6, default h3 — modelo Atlaskit, tamano visual desacoplado) + Description(measure 45ch) + Actions(label -> role=group +aria-label, buttonGroupLabel); recipe sobre el bundle --size-* (titulo un paso discreto arriba; sm=in-collection, lg=hero); tokens publicos minimos (gap/actions-gap/media-bg/media-fg/media-radius/description-measure) - demo v2 9 tabs (harness: SystemAxes/MotionPanel/SemaPanel; snippet con paridad; escena in-collection via Card) + entrada nav (grupo Status) - README: Baseline · Comparativa (shadcn/Chakra/Atlaskit/AntD/Polaris) · Decisiones · Passive justification · Gaps con disposicion Verificacion: component:audit PASS · eidos-lint 5 morfo-backed + 4 eidos-only sancionados · morfo:check + morfo:vocabulary verdes · recipe-css-contract/api-contract/visual-attrs 37/37 · svelte-check 76E/51W = baseline exacto (cero regresion) · navegador claro Y oscuro por estilos computados (titulo 18->24px, placa 40->64px, chips vivos, cero errores de consola). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
3 months ago |
|
|
9a7a438a96 |
fix(emoji-picker): panel encaja en el Popover + ancho tokenizado, sin overflow
El contenido (331px) desbordaba el Popover (~284px, --popover-min-width-md) y el viewport lo recortaba con scroll horizontal — la ultima columna de emojis salia cortada, evidente en oscuro. Fixes: - El Popover recibe su ancho via prop `width` = TOKEN `--emoji-picker-panel-width` (23rem, token de componente al estilo color-picker content-width — NO hardcode). El contenido pasa a inline-size:100%; nada desborda el popover. - PopoverContent size="sm" (padding mas ajustado) + categorias en ToggleGroup size="xs" → las 9 caben sin scroll (antes 6/9). Tonos size="sm". - scrollbar-width:thin en el grid (el unico scroll, vertical, esperado). Tamaños: iconos/glifos canonicos (Icon size, --size-lg/xl-font-size, ToggleGroup size); grid-max-block-size min(50vh,17rem) replica el patron de command.css. Verificado en claro Y oscuro (capturas): content Hscroll 0, strip Hscroll 0, 9 categorias visibles, celda 36px, un solo panel; component:audit PASS · eidos-lint invalid 0 · recipe-contract 30/30 · check baseline 76. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
d59b8af9aa |
uix(chat): v2 · emoji-picker (picker completo) + escaparate rico del bloque
Nuevo componente emoji-picker y rediseño del escaparate de mensajeria. emoji-picker (ruta 9 fases): - Compone Popover (open/close/foco/portal) + Command (busqueda + grid + nav de teclado + a11y combobox/listbox, su "grid mode" documentado) + dos ToggleGroup (categorias + tono de piel). Delegated: sin eventos propios (D.1); el grid y las celdas doble-registran Command.List/Item, conservando la a11y pero con contrato visual propio (gana especificidad con selectores de 2 niveles, sin tocar attrs [data-command-*] ajenos — eidos-lint limpio). - Datos: $libs/emoji, dataset vendorizado de emojibase (MIT) horneado por scripts/generate-emoji-data.ts (precedente lucide: JSON crudo en static/ + script generador). ~1906 emoji con nombre CLDR + keywords, 316 con tono de piel single-person. Grid acotado (categoria activa o resultados de busqueda), sin virtualizacion (refinamiento v2). Helpers puros + test (12/12). - morfo delegated (7 partes + header/footer de layout); soma (root + trigger + content + grid + option + preview, provider-test 4/4); eidos (panel empaquetado, receta con tokens --emoji-picker-*); langs es/en; demo 5 tabs (excepcion delegated documentada); READMEs dobles. Escaparate del bloque (web/routes/uix/demos/chat) — rehecho rico y correcto: - Avatares por-autor con color solido (Avatar del ecosistema): antes gris neutro indefinido, ahora teal/ambar/verde/indigo/primary bien contrastados. - Dos columnas: chat-list (sidebar con badges + typing-en-lista) + sala. - Conversacion rica: separador de dia, run agrupado, reaccion, reply citado, recibos de lectura, typing con avatar, y el emoji-picker integrado en el composer (inserta en el draft). - Solape de burbujas (transitorio, estimateSize=48 con mensajes ~90-126px) mitigado subiendo el estimate a 76. Auditoria de tamanos/espacios: recipes del bloque limpios (todo con tokens --space-*/--size-*/--color-*); unico literal (punto tono-capable) tokenizado. Verificacion: check baseline 76 (0 nuevos) · component:audit emoji-picker PASS · eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · emoji lib 12/12 · provider-test 4/4 · Playwright PASS (busqueda 'rocket'->rocket+ astronautas, click->onSelect+cierra, teclado, tono oscuro 👋->👋🏿, picker abre en el composer). Capturas claro miradas. next-features §7 marca emoji-picker SHIPPED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
f9889b0b18 |
uix(chat): v2 · chat-list — panel de conversaciones (compone Listbox)
Componente nuevo del bloque chat v2: el panel de lista de salas/DMs. Compone el Listbox del ecosistema (doble registro id/ref compartido): selección única + roving-tabindex + typeahead + role=option vienen de Listbox; chat-list aporta la disposición de fila (avatar · título/preview · meta) y el estado visual data-unread (negrita + Badge compuesto). - morfo: expression 'delegated', sin eventos propios (D.1 — el actor es el Listbox compuesto); parts provider/item(data-unread)/avatar/title/preview/meta. - soma: ChatListProvider crea ListboxProvider dentro (value string <-> string[]); ChatListItemProvider per-instance (unread = boolean crudo); 4 clases de contenido explicitas. - eidos: rejilla avatar | (titulo/preview) | meta, avatar y meta abarcan las dos filas; truncado; unread -> titulo en negrita. No neutraliza el arquetipo item (la conversacion ES una opcion seleccionable). Sin @keyframes; ejes logicos. - demo: 5 salas, seleccion limpia unread, typing-en-lista (compone ChatTyping), Badge de no-leidos; tabs live/system/api/morfo/a11y. - READMEs dobles (eidos con Passive justification, delegated); next-features 7 marca chat-list SHIPPED. Verificacion: check baseline 76 (0 nuevos) · component:audit PASS · eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · Playwright PASS (role=listbox, 5 salas, aria-selected, unread=2, badges=2, typing-en-lista, click + teclado seleccionan). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
8fe9d1faaa |
uix(chat): v2 · jump-to-message highlight en chat-message
Tercer y último remate de v2 (batch elegido por el usuario). chat-message gana el prop `emphasized` → attr `data-emphasized`: al saltar a una cita (reply.onJump → scrollToIndex) la app enciende `emphasized` ~1.4s y la fila objetivo hace un flash suave que se desvanece. VISUAL PURO — sin evento de sema (desvía a propósito del signal-emphasize -target del plan): las apps de referencia hacen un flash SILENCIOSO; un signal que pitara por defecto sería molesto. Cumple 2-de-3 (soma escribe el attr, eidos lo lee). El pulso = toggle de dos estados + transition (cero @keyframes, R-4.5); reduced-motion = instantáneo. Morfo attr + soma prop/provider (devuelve el booleano al resolver, no '') + eidos recipe (wash color-mix 14% de toda la fila) + demo componente (botón flash) + escaparate (reply → jump → emphasize). next-features §7 marca los 3 remates shipped. Verificado (Playwright): flash on → bg tintado → auto-clear a 1.4s; escaparate intacto. audit PASS, eidos-lint invalid=0, recipe-contract 30/30, check en baseline (76). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
3 months ago |
|
|
e838e7107b |
uix(chat): v2 · read-by list — parte ReadBy en chat-message
Segundo remate de v2. chat-message gana la parte opcional `ReadBy`: una fila de avatares "visto por N" que la app compone (AvatarGroup), típica de mensajes propios; el emisor la alimenta con los acuses recolectados vía el onMessageSeen (v1). DECORATIVA (aria-hidden): el indicador Status ya transmite "read" a AT; las caras son realce visual. Morfo part + soma provider/wrapper + eidos wrapper/recipe (fila de avatares muted, ejes lógicos in/out) + demo con toggle. next-features §7 marca read-by y typing-avatars como shipped. Verificado (Playwright): la fila aparece con 3 avatares + "Visto por 3", aria-hidden, en la columna del cuerpo. component:audit PASS, eidos-lint invalid=0, recipe-contract 30/30, check en baseline (76). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
3 months ago |
|
|
4c4878f139 |
uix(chat): bloque de mensajería chat-* v1 (log · message · composer · typing)
Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica: el modelo de datos es de la app, los componentes reciben props + snippets. - chat-log: Feed + VirtualList(anchor:'end') en triple registro; day separators, unread divider, filas de sistema (Feed.Article), pill "N nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware. - chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega, reply embebido, reacciones (chip pressed + tapback panel), acciones hover. - chat-composer: Textarea (doble registro) + FileUpload; teclado Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers); barra reply/edit. - chat-typing: sustain-streaming (visual-only, loop del sistema); aggregateTypers 1/2/3+ con plantillas plurales. - $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers. - VirtualList: extensión anchor:'end' (followOutput, prepend por clave estable, sticky monótono, onAtEndChange) — sin scroller paralelo. Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves (out = primary-element + texto normal; in = surface card + sombra) sobre lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal, iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños: size="md" = 16px canónico. Fixes de framework (afectaban más allá de chat): - EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos (pagination/file-upload/chronos/chat) caían al fallback EN en locale es. Fix parseLangRef ?? parsePathFallback + test de regresión (132/132). - virtual-list commit-set-resize con channels:[]: es bookkeeping de ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción (interventions de navigator.vibrate / AudioContext). Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers; variante $connection señalada). Gaps v2 registrados en next-features §7. Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract 29/29 · Playwright escaparate PASS end-to-end · consola limpia · npm run check en baseline (76, cero errores nuevos). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
3 months ago |
|
|
b93cc6c5c0 |
uix(color): arregla el custom-color roto de Card y Avatar (fill)
BUG (descubierto en la verificación adversarial de la iniciativa): un color
CSS crudo (p.ej. `color="#3b82f6"`) NO pintaba el FILL de Card ni Avatar —
caía a neutral. Card/Avatar (Fase 2a) usaban un split bespoke pre-helper
(`isCanonicalColor` + seed `--{c}-color-custom` + bloque derivador
`[data-{c}][data-color-custom]{--_{c}-palette-*: …}` en su .css). Ese bloque
quedó ENSOMBRECIDO por el forward compartido de THM-2 en `generated/base.css`
(misma especificidad (0,2,0), carga después → gana); para un valor crudo
`--palette-*` está sin definir (el shared layer lo deriva de `--color-custom`,
NO de `--{c}-color-custom`) → host neutral.
Fix (los alinea con TODO el resto de la iniciativa):
- card.svelte / avatar.svelte: el split bespoke → `resolveComponentColor`
(genérico: canónico→`data-color`; crudo→`data-color-custom` + seed
`--color-custom`, que el forward YA consume). Ambos ahora COMPONEN el seed
con el `style` del consumidor (`composeInlineStyle`) para que `...rest` no lo
pise — Card lo pisaba (style antes de rest), por eso el custom se perdía
incluso con el seed correcto.
- card.css / avatar.css: borrados los bloques bespoke `[data-{c}][data-color-
custom]{…}` (muertos + ensombrecidos). Las variant slices leen
`--_{c}-palette-*`, que el forward llena para rol/escala/custom uniformemente.
- Avatar RING y BADGE son canales de color SEPARADOS (`--avatar-ring-color-
custom` / `--avatar-badge-color-custom`): NO se tocan — usan su propio
namespace de tokens, el forward no los ensombrece y funcionan (verificado).
- base.ts: quitados los 4 tokens de recipe muertos (`card`/`avatar` ·
`color-custom` + `color-custom-contrast`); ring/badge se conservan.
Verificado en Chrome: Card real (picker del demo) + Avatar sintético pintan el
custom en soft/solid/outline/ghost, componiendo el style del consumidor;
roles/escalas siguen exactos (behavior-preserving); ring + badge intactos.
Orphan test resuelto (los `-contrast` ya no huérfanos). Los 2 fallos residuales
del contract son WIP de chat-composer (otro track, sin commitear), ajenos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
3 months ago |