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 }
447 Commits (bf12b8a57192561c521e997f4da72b6004fee28f)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
bf12b8a571 |
feat(skin-media-player): cuatro aparatos sobre el contrato del player — versión A, para el histórico
Se commitea PARA CONSERVARLA: esta versión se borra y se rehace. El autor la valoró como duplicación, y la medición le da la razón. Queda aquí para que la segunda no repita sus errores. Qué hay: - `eidos/components/skin-media-player/` — cuatro objetos dibujados (disco, giradiscos, casete, pletina) sobre el morfo `media-player`. El aparato anida su soporte como `<svg>` interior, así que la pintura del soporte se reutiliza. - La afordancia es un `<button>` HTML superpuesto, colocado en % del viewBox, y NO un `role="button"` sobre un `<g>` como en los bocetos. Precedente: `onion-menu` («a real HTML button overlaid at the SVG centre… which an SVG element can't do») y el knob de `natural-time-picker`. - Transporte compuesto sobre las partes reales: MARCHA/REPROD sólo arrancan y PAUSA sólo pausa, anulando el toggle de soma con `preventDefault()` en la costura (patrón de `RateFloat`). PARO/STOP no existe en el contrato y se compone `togglePlay()` + `seek(0)`. - `engaged()` — `started` se enciende con la primera reproducción y no se apaga nunca, así que por sí solo dejaba el brazo apoyado sobre un disco parado. Con `started && !ended && (currentTime > 0 || !paused)` salen los tres estados de una máquina: parada, sonando, en pausa. - Geometría portada del boceto: brazo por cinemática inversa de forma cerrada (r² = K + A·cosθ + B·senθ) y bobinas por conservación de área (R_izq² + R_der² constante), de donde sale sola la velocidad correcta de cada bobina. El contador de cinta lee el medio en vez de un `setInterval` propio. - Los 83 colores del autor se conservan intactos: `skin-media-player` entra en `FIXED_TONE_COMPONENTS` — la excepción que el guard ya reserva para los componentes que DIBUJAN un objeto físico (`natural-time-picker`, `color-picker`, `proof-of-human`). Acotada a esta carpeta a propósito: dentro de `media-player/` habría eximido al recipe del player real. - `web/routes/otros/players/` — los cuatro bocetos originales del autor (estado simulado, sin audio) y el banco de verificación. Por qué se rehace, medido: 1. DUPLICACIÓN. `fit()` idéntico en record y cassette; `hold()` y `stop()` idénticos en turntable y deck; `MediaPlayerProvider.require()` + `engaged()` en los cuatro. Son cuatro componentes que comparten carpeta, no un skin: cada cuerpo se agarra al provider por su cuenta y recablea las mismas costuras. Lo correcto es al revés — un componente que recibe el player y resuelve las acciones, y a cada aparato sólo su pintura y su mapa de mandos. 2. MOTION A MANO. Tres `@keyframes` locales y siete transiciones con números crudos, existiendo el motor: `smp-turn` ES el preset `spin` (`loop-spin`, periodo por `--motion-loop-spin`) y `smp-tape-pulse` ES `pulse`; las transiciones debían salir de `--duration-*` / `--ease-*`. Y un defecto funcional: el generador apaga los loops por `[data-motion='reduce']` Y por la media query; mi bloque a mano sólo tenía la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba el plato. 3. La raíz `skin-media-player.svelte` empaqueta `Media + Skin + barra` por dentro, así que la forma del reproductor (inline/bar/row/card, que es el eje de `AudioPlayer`) no se podía elegir y el MediaPlayer no se veía por ningún lado en la demo. Verificado antes de commitear: `npm run check` sin errores nuevos (la base del repo, 72, no se mueve) · guards de eidos 49/49 · `rtl:check` 0/179 · en navegador con audio real, transporte, brazo siguiendo el surco, bobinas por área y contador de cinta. Co-Authored-By: Claude Opus 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 |
|
|
de0615caf2 |
fix(morfo): affix deja de declarar lo que solo lee una capa, y el gancho pasa a nombre de CAPA
Correccion doctrinal, senalada por el autor: **morfo es la capa declarativa ENTRE
capas**. Un atributo que consume una sola no es contrato.
`affixMorfo` declaraba `data-affix-placement` y `data-affix-stretch`. Los lee
UNA: el CSS. No debian estar ahi — y la doctrina nombra la familia exacta
(`morfo.md` §`undeclaredState`: «the escape hatch for attrs OUTSIDE the contract
— a visual wrapper's `data-size`, a presentation flag like `data-sheet`»). Los
declare porque `morfo-check` los exigia, que es la herramienta dictando la
doctrina: el guard clasifica por PREFIJO DE NOMBRE, la doctrina clasifica por
NATURALEZA, y las dos solo chocaban porque el gancho llevaba el nombre del
componente.
Y no era su nombre. Lo estampan tres —`Affix`, `Fab`, `MenuDial`—, asi que no es
de ninguno: pasa a **`data-viewport-placement`** / `data-viewport-stretch`. La
colision con el guard desaparece por construccion, sin excepcion que escribir.
Es el mismo error que `--fab-offset` leido por la capa: nombrar por un
participante algo que es de todos.
`affixMorfo` se queda con lo que si es contrato: la identidad `data-affix`, que
es como el DOM dice «esta caja es un Affix» a quien pregunte.
TAMBIEN INTENTADO Y REVERTIDO, con su motivo, para que nadie lo reintente:
mover el fichero a `eidos/lib/viewport-placement.css` junto a `list-surface.css`.
`recipe-css-contract` lo tumbo y tenia razon — **el sistema de tokens esta
indexado por componente**: toda clave de `recipes/base.ts` exige su
`components/{c}/{c}.css`. `list-surface` puede vivir en `lib/` porque NO tiene
clave de receta (sus `--list-*` viven dentro de su propio CSS, por `data-size`,
no como defaults temeables en `:root`); los nuestros si lo son. La parte portante
—«esto no es de nadie»— la lleva el nombre del gancho, no la carpeta. Queda
escrito en la excepcion E-2.2 del README y en el PLAN.
Sin cambio de comportamiento: 68 sustituciones de nombre, mismas reglas, mismos
valores. Verificado en las tres rutas.
check 69 = base intacta · component:audit affix 0/0 · fab 0/2 · menu-dial 0/0 ·
morfo:check affix PASS · layer:check 0/3 · contrato de capa 13/13 ·
recipe-css-contract + api-contract 50/50 · rtl 0/178 · smoke 311/311.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
4b39b718c6 |
fix(eidos): el aviso al pie se lee donde se ve, y la fuente unica deja de mentir sobre si misma
Cierra los dos hallazgos que quedaban de la auditoria.
ORDEN DE LECTURA (a11y). `position: fixed` saca la tira del flujo VISUAL pero no
del DOM: se lee donde esta en el fuente. Con `affix="bottom"` el aviso se ve al
pie y la mini-pagina lo renderizaba primero, asi que un lector de pantalla lo
anunciaba ANTES que la cabecera — medido en la preview del propio block, era el
primer nodo del documento.
Quien decide eso es el APP, no el block: el block coloca, el documento ordena.
Asi que la correccion no es un prop, es que la demo deje de modelar el desajuste
— el aviso se renderiza donde se ve — mas la advertencia en el typedoc de `affix`
y una fila en los Gaps. Verificado en los dos bordes: con `bottom` el aviso va
DESPUES de la cabecera en el DOM y se ve al pie; con `top` va antes y se ve
arriba. En ambos, z 150 y pegado a su borde.
AFIRMACIONES OBSOLETAS. Cinco sitios seguian diciendo que `Fab` "ships a private
copy today" y que su migracion estaba pendiente — falso desde
|
2 months ago |
|
|
acb7f0b416 |
test(eidos): la capa compartida deja de no tener guard — uno de texto y uno de valor computado
La auditoria encontro que `affix` no tenia NINGUNA cobertura automatica: nueve
zonas, el remapeo RTL, la regla de stretch y el empate de `position` con
`[data-button]` estaban verificados solo a mano. Con dos consumidores y un
tercero planificado, eso era el hallazgo estructural.
Van dos guards, y la division del trabajo entre ellos es medida, no supuesta.
`shared-layer-contract.test.ts` (texto, proyecto node) — nueve zonas cubiertas,
`stretch` solo en bordes de bloque, ningun eje leido sin la ranura de override,
ninguna regla de geometria enganchada en `data-affix` (la identidad), y el
contrato de consumidor: importa la capa, estampa el gancho, no acuna tokens
propios, y reafirma `position` si el primitivo que compone ya declara uno.
Existe por UNA razon que el navegador no puede cubrir: `getComputedStyle` de
`--_affix-safe-inline-start` devuelve `"0px"` en escritorio — medido. La
sustitucion ya ocurrio, asi que que `env(safe-area-inset-*)` alimento la ranura
se perdio. Un remapeo con una sola mitad volteada se lee igual que uno correcto
en toda maquina donde corre CI, y solo aparece en un movil con muesca, apaisado
y en RTL. El texto es el unico sitio donde ese emparejamiento se ve.
`scripts/layer-check.ts` (navegador, `npm run layer:check`) — para todo elemento
con el gancho: `position` es el que la capa promete, la z resuelve, los tokens
publicos resuelven, y ninguna ranura declarada inline computa a vacio (la firma
exacta de un ciclo de custom property). La lista de consumidores es DERIVADA de
quien importa la capa, asi que MenuDial entrara el dia que migre sin tocar nada.
Lo que NO comprueba, y esta escrito en su cabecera en vez de tapado: la
geometria. La asercion obvia —"el inset anclado no es `auto`"— es INUTIL:
`getComputedStyle` da el valor usado, asi que un `auto` se lee como pixeles
(medido: `-1976.7px` en una sonda, `324px` en el fallo real del ciclo). No hay
forma a nivel de propiedad de distinguir "324px porque el calc murio" de "324px
porque lo pidio el autor". Eso exige comparar contra el bloque contenedor, que
exige caminar ancestros. Diferido al tercer consumidor.
Ambos guards se verificaron POR MUTACION, porque un test en verde no prueba
nada hasta que falla sobre el defecto que dice atrapar:
texto M1 media mitad del remapeo RTL -> falla
M2 zona borrada -> falla
M3 vuelta a un solo nivel de token -> falla
M4 puente sin `position` -> falla
M5 fab reacuna --fab-z -> falla
navegador M6 la capa pierde el position de veras -> falla
M7 token de z renombrado -> falla
M8 la demo deja de ejercitar la capa -> falla
M8 no es teorico: la PRIMERA corrida de `layer:check` dio verde habiendo mirado
CERO elementos, porque la demo de fab arrancaba en `placement="static"`, que no
estampa gancho. Un guard que aprueba porque no habia nada que mirar es peor que
no tenerlo, asi que ese caso es ahora un fallo y el default de la demo pasa a
`bottom-end` — "floating" es el nombre del componente y su demo deberia
ejercitarlo (la caja del stage tiene `contain: layout`, no se escapa).
Y una comprobacion mas que dejo dicha: quitar `position: fixed` del puente de fab
NO rompe nada en el orden de carga actual — la capa gana el empate igual. Esa
linea es un seguro contra un orden que puede cambiar entre builds, no el arreglo
de un estado roto; por eso la cubre el guard de texto y no el de navegador.
check 69 = base intacta · smoke fab PASS · layer:check 0 violaciones sobre 2
consumidores · suite 120/120.
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 |
|
|
1fc0a7a96b |
refactor(eidos): Fab deja de tener su propia copia de la colocacion — la lee de la capa
Primera de las dos migraciones que dejo abiertas `Affix`. `fab.css` tenia cuatro reglas de esquina que eran una de TRES copias privadas del mismo CSS (menu-dial tiene nueve; `air/layout/float` las tuvo antes que las dos). Se borran: ahora `fab.svelte` importa `../affix/affix.css` y estampa `data-affix-placement`, y en `fab.css` queda un puente de tres lineas. Es la forma con la que las recetas de menu/listbox consumen `lib/list-surface.css`. El puente reafirma `position: fixed`, y no es redundancia: `button.css` declara `position: relative` sobre `[data-button]` a la MISMA especificidad (0,1,0) que la regla base de la capa, y los dos ficheros van code-split — en el orden de carga equivocado la capa pierde el empate y un FAB flotante computaria `relative` en silencio. Los ocho insets siguen viniendo de la capa; solo esa propiedad se pelea en local. Queda escrito en los dos README como lo que le debe al puente cualquier consumidor cuyo primitivo compuesto ya declare `position`. Nada cambia de comportamiento ni de API: - `--fab-offset` y `--fab-z` siguen siendo publicos de Fab; el puente apunta la capa a ellos en vez de sustituirlos. `--fab-z` se queda a proposito en la banda `sticky` y NO en el peldano `affix` (150) — un menu o un dialogo tienen que seguir abriendose por encima del FAB. - `placement="static"` ya no estampa gancho ninguno, asi que no casa nada y el consumidor lo coloca. La salida pasa a ser una ausencia en vez de una quinta regla. - `data-placement` desaparece del DOM del FAB; solo lo leia `fab.css`. Medido en las cinco colocaciones sobre la demo: las cuatro esquinas a 16px de sus dos bordes (`--fab-offset` puenteado), `fixed`, z 100 — identico a antes — y `static` sin gancho, `relative`, z `auto`. `MenuDial` intacto: conserva su `data-placement` propio y su trigger es un `Fab placement="static"`. Y queda probado en vivo el motivo del split de attrs: `morfo-check` NO juzga el `data-affix-placement` del `<button>` contra `affixMorfo`, porque se salta todo attr que no empiece por el kebab del propio morfo. component:audit fab PASS 0/2 (sus excepciones documentadas) · affix PASS 0/0 · rtl 0/178 · eidos-lint fab/affix invalid 0 · smoke 311/311. Los dos fallos previos de morfo:check (fab `data-fab-size`, menu-dial `data-state`) siguen exactamente igual: son ajenos a esto. Queda: migrar `MenuDial` (9 zonas), con su fallback `1100` y su offset interno. 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 |
|
|
778877dc94 |
docs(soma): los pickers dicen ya lo que Escape hace — adopcion del texto del eje de dismissal
El JSDoc de color/time/time-range-picker y dos demos llevaban en el arbol sin commitear desde el 2026-08-11 mientras HEAD decia lo contrario de la conducta shipped (82d269cd8: Escape descarta en AMBOS modos, un cierre sin causa es un descarte), y el ledger §D5 citaba como evidencia una linea (time-picker/types.ts:42) que en HEAD no existia. Trabajo del eje de dismissal adoptado tal cual — cero cambios de codigo, solo el texto que hace verdadero el contrato documentado. 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 |
|
|
b201f190db |
refactor(morfo,soma): el default del nombre sube al contrato y las cadenas que lo duplicaban se van
Consecuencia de la precedencia en dos clases. Tres barridos que resultaron ser
el mismo defecto visto desde tres alturas.
ESPEJOS (10). Lineas de provider que reescribian el `aria-label` que el bag ya
emitia: popover y float-panel close, editable x3 —su propio comentario ya
confesaba «resolves to the same id»—, palabras x4, y los seis hand-merges
consumer-first de wrapper (file-upload x5, command-list) que reimplementaban a
mano la politica de `mergeProps`. Con ellos cayeron cuatro errores de tipado
preexistentes.
BUTTON. Retirada su danza de dos entradas: la entrada `propRef` + `prop-truthy`
del morfo, el destructure del wrapper y la fuente del provider. El attr del
consumidor fluye por restProps y gana por politica; el tipo con su JSDoc se
queda, que documenta la API sin enrutar nada.
LAS ~28 CADENAS `resolvedAriaLabel`. Eran la danza de Button a escala: el prop
publico ES `aria-label` (los types hacen `Without<>` y lo re-declaran), asi que
el wrapper lo sacaba de restProps para que el provider lo resolviese contra un
default y lo devolviese al bag. 21 componentes normalizados; el default vive
ahora en el morfo y la supresion por labelledby es una `condition` declarada en
vez de un `if` escondido en un resolver.
Y CROSS-ELEMENT, la clase que el barrido destapo y que no estaba prevista:
textarea, mask-field, search-field, password-field y navigation-menu declaraban
el prop en una raiz que pinta un `<div>` sin rol mientras el nombre pertenece a
un control HIJO. El morfo declaraba la etiqueta INCONDICIONAL mientras el
provider la suprimia ante un `Field.Label` y luego la pisaba siempre — el plan
se evaluaba y se tiraba. Ahora el default vive en la parte que posee el nombre,
la supresion se declara (`prop-falsy fieldLabelled`, prop virtual del provider)
y el prop de conveniencia de la raiz se reenvia con spread CONDICIONAL: escribir
la clave sin condicion la pone a `undefined` y BORRA el default que el bag acaba
de aportar. `aria-label` gana a `<label for>` en el computo del nombre, asi que
esa supresion es correccion, no cosmetica.
waveform era otra cosa y por poco se rompe: su default se QUEDA en el provider
—el nombre lo lleva un `<Slider>` compuesto, que no es parte suya—. Su defecto
estaba en el otro extremo: prop `ariaLabel` camelCase, unico en el catalogo, y
`Without<..., {}>` que no filtraba nada, asi que un `aria-label` del consumidor
aterrizaba en el div sin rol y se perdia. Alineado con el catalogo; el tipo
estrecho caza al hacerlo un consumidor real (media-player-time-slider).
De paso, la incoherencia de gemelos: range-calendar declara ya sus
month/year-select con los MISMOS refs `#?common.calendar.*` que calendar.
Fuera de alcance por medida, no por pereza: las etiquetas que cambian con el
ESTADO (`mapRef` devuelve `map[String(raw)]` sin traducir) y las INTERPOLADAS
(`translationRef` nombra una clave, no admite params).
Refs compilados 145 -> 157. check 70 = base. soma 1269 verdes (1 timeout ajeno
preexistente), morfo+sema 496. SSR de las paginas migradas sin una sola fuga de
`#?`: el consumidor gana donde lo pasa y el default resuelve en castellano
donde no.
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 |
|
|
a542f9f10d |
feat(eidos): chronos abre su estructura, y el cuerpo por defecto la estrena
C5 rebanada 1 de 4. Que partes son publicas no es cuestion de gusto: es `kind` en el morfo (21 publicas, 12 privadas). Namespace `Chronos.*` con la forma de Table/Calendar, y la raiz gana ranura de composicion — pasar `children` REEMPLAZA el cuerpo por defecto. Antes se renderizaban ADEMAS de la vista completa, un segundo arbol colgando de un calendario entero; ningun consumidor los pasaba. Ocho partes de toolbar (`Toolbar`, `Heading`, `Prev`/`Next`/`Today`/`Undo`/ `Redo`/`SearchButton`), ergonomicas al estilo `Calendar.PrevButton`: componen el IconButton/Button del sistema, su glifo y la llamada al provider, asi que el consumidor recibe un control que FUNCIONA y no un gancho vacio. El cuerpo por defecto se recompone desde ellas — no es un camino privilegiado, es el primer consumidor de la API compuesta. `searchOpen` sube al provider: en un toolbar compuesto el boton y la paleta viven en subarboles distintos y el provider es el unico sitio que alcanzan los dos. Y la prueba compuesta destapo un hueco real — el SearchButton compuesto no abria nada porque la paleta vivia en el cuerpo por defecto. Los overlays pertenecen a la RAIZ: extraida a `chronos-search-palette.svelte`. El Dialog del editor tiene el mismo hueco y sigue dentro de la vista; es lo primero de la rebanada 2. Dos declaraciones falsas mas, del patron de siempre: `toolbar` declaraba `header` y se pintaba un `div`; `heading` declaraba `div` y se pintaba un `span`. Los wrappers renderizan lo DECLARADO, medido sin desplazamiento. Medido con dos instancias en la misma pagina: next en la compuesta mueve solo la compuesta (junio -> julio), prev en la de por defecto mueve solo esa (junio -> mayo), ids de heading distintos, y la paleta de cada raiz lista sus 18 eventos por separado. check 74 = base · 458 vitest · morfo:check PASS chronos · docs:check 0/621 · rtl:check 0/177 · eidos-lint invalid 0 · consola limpia en las tres vistas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
49362d89e9 |
feat(sema,sound): un pack se entrega como quieras — y un `data:` no se pide, se descodifica
La entrega deja de ser parte del contrato. Un pack de ficheros puede llegar
como quince URLs o como UN JSON de base64 (`packFromBase64`): mismo tipo, mismo
intercambio en caliente, misma reserva sintética por entrada, y un nombre que
no existe sigue sin compilar. Medido sobre `static/sounds/ui-inline.json`: una
petición en vez de quince, y 69.082 B con brotli frente a 79.332 B de los
ficheros sueltos — el +33 % del base64 lo deshace la compresión, y el JSON gana
además porque nadie comprime `audio/mpeg` y todos comprimen `application/json`.
## `step` no se oía
Era a la vez el MÁS CORTO (18 ms) y el MÁS FLOJO (gain 0.025), y esas dos cosas
se multiplican: el oído integra energía durante ~100-200 ms, así que un tono
corto necesita MÁS amplitud, no menos. Medido, rendía a −29,8 dBFS con la
novena parte de la energía de `touch`. Ahora 0.06 a los mismos 18 ms.
Lo que no había era el guard: la distinguibilidad es una RELACIÓN, y dos
sonidos pueden ser perfectamente distintos entre sí y ser los dos inaudibles.
`sound-names.test.ts` gana un suelo absoluto sobre `gain² × duración`.
## El precalentamiento no precalentaba
- `preload` pedía el contexto con `getOrCreateContext()`, que hace
`await resume()` — y antes del primer gesto esa promesa se queda PENDIENTE en
Chrome, no se rechaza. No descargaba nada hasta que el usuario ya había
pulsado, que es justo la latencia que el preload existe para quitar.
`decodeAudioData` funciona en un contexto suspendido.
- Aplicar un pack en caliente no calentaba la caché: cada nombre pagaba su
viaje la primera vez que sonaba. Ahora `rebuildMap()` llama a `warmSamples()`.
- Con `preferences.sound === 'off'` no se descarga NADA — un pack que no se
puede oír son bytes gastados en silencio. Pero la petición se RECUERDA en vez
de tirarse: el nivel se lee en el dispatch, así que la primera reproducción
audible vacía la cola y encender el sonido no devuelve una caché fría.
## Un `data:` se descodifica en el sitio
`connect-src 'self'` BLOQUEA `fetch('data:…')` — la directiva casa por ESQUEMA
y `'self'` no cubre `data:` — mientras deja pasar una ruta del mismo origen. Un
pack inline enrutado por `fetch` caía a síntesis EN SILENCIO justo en el
entorno que llevó a alguien a hacerlo inline. Y es ~6x más lento. `bytesFor()`
lo resuelve con `atob`.
## De la revisión adversarial
- Las claves del JSON se validan: una que no sea `SoundName` LANZA, igual que
una ruta mala en `assertMapPaths` y por la misma razón — tragarse la errata
da el síntoma «parte de mi pack no se aplicó» sin nada a lo que apuntar.
- `isSoundName` preguntaba `name in SEMA_MAP.sounds`, y `in` recorre el
prototipo: `isSoundName('toString')` devolvía `true`. Inofensivo mientras
todos los llamantes tecleaban el nombre; nada inofensivo al validar red.
- `warmSamples` sólo tenía una de las dos ramas que sí tiene el constructor, así
que una app con canal de sonido propio se calentaba al arrancar y nunca más;
y esa rama leía la preferencia UNA vez, en construcción. Las dos se caían por
lo mismo: ahora comparten `warmUrls()` en vez de duplicarse.
- El demo del catálogo cancela su fetch: elegir `inline` y saltar a `synth`
antes de que llegara reinstalaba el pack sobre un `clearMap()` ya hecho.
- `fallbackFor(name)` borra los dos casts que copiaba todo el que autorase un
pack de ficheros.
## Documentación
`architecture/sema.md` gana la sección de las dos formas de entrega y el
párrafo de `preloadSamples` reescrito — describía un precalentamiento que ya no
es el que ocurre. El README del arte conoce la ruta `data:`. La página de packs
gana su sección equivalente y deja de señalar unos `.wav` sueltos como «el
ejemplo».
Y un fantasma: `soft` no existe — es un nombre del catálogo viejo que quedaba
como ejemplo en ocho sitios, incluido el docstring de `applySounds`, cuyo
ejemplo copiado literalmente LANZABA.
`static/sounds/ui-inline.json` se genera desde `packs/ui-mp3.ts`; el comando
está documentado ahí y reproduce el fichero byte a byte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
11b54025d4 |
feat(sema): pack de ejemplo con ficheros mp3, recortados y normalizados
El autor dejo una biblioteca de 35 mp3 en `static/sounds/ui` y pidio
recortarlos y normalizarlos para usarlos como pack alternativo. Medidos
ANTES de tocar nada, los originales tenian tres problemas que habrian
hecho parecer roto al sistema:
· LATENCIA DE ARRANQUE. `button_hard` no empezaba a sonar hasta los
347 ms. Un sonido de pulsacion que llega un tercio de segundo despues
no confirma nada: se lee como eco. `button_squishy` 148 ms,
`blocked` 78 ms, y varios mas con 25 ms de silencio de cabeza.
· DURACIONES FUERA DE ESCALA. `item_select` 862 ms para seleccionar en
una lista donde puedes recorrer cinco elementos por segundo — se
solapan tres a la vez. `success_chime` 1591 ms.
· NIVELES SIN NORMALIZAR, factor 14x entre picos (0.066 … 0.961): unos
inaudibles al lado de otros.
PROCESADO con ffmpeg: recorte de silencio en ambos extremos + loudnorm a
−20 LUFS / pico real −1.5 dB, mono 44.1 kHz. Resultado medido en el
navegador: TODOS arrancan en 0 ms, y las duraciones se desploman —
`button_soft` de 862 ms a 28 ms, que resulta ser la longitud ideal para
un toque; `copy` a 78 ms, casi exacto al `tick` sintetico. 292 KB en total.
⚠️ Los originales NO estaban en git y el procesado los reemplaza. Copia de
seguridad de esta sesion en /tmp/ui-originales (efimera): si los quieres
conservar, guardalos tu.
EL PACK (`src/uix/sema/packs/ui-mp3.ts`) mapea 16 nombres a 16 ficheros.
Es una TRADUCCION, no una busqueda: la biblioteca esta nombrada por
componente (`button_`, `tab_`, `panel_`, `window_`) que es justo el eje
que el vocabulario rechaza, asi que cuatro ficheros compiten por el nombre
`open` y sobran quince. Cada entrada conserva como reserva el sonido
sintetico del framework, de modo que un 404 degrada a la voz por defecto
en vez de enmudecer.
Lo imperfecto queda ESCRITO en el fichero, no escondido: `tick.fulfill`
dura 425 ms y `tick.loss` 723 ms, muy por encima de la ventana perceptual
que persigue el pack sintetico. Es el precio de usar material encontrado
— suena mas rico y se comporta peor. Un pack grabado PARA este sistema se
grabaria a sus duraciones.
`engine.applySoundPack(pack)` instala entradas enteras (frente a
`applySounds`, que retunea valores de las que ya hay); `clearMap()`
revierte tambien el pack. Y el catalogo de la documentacion estrena
conmutador: mismo nombre, otra voz, para comparar A OIDO.
VERIFICADO en el navegador espiando el grafo de audio: con el pack por
defecto `tick` crea dos osciladores a 850/1275 Hz; con `ui-mp3` crea un
buffer de 78 ms y CERO osciladores. El cambio funciona de punta a punta.
sema+morfo+soma 1075/1075 · uix sin errores de tipos nuevos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
4f1b9d4236 |
docs(web): la pagina del sonido explica el sistema entero, no solo su uso
Segunda correccion del autor, y mas dura que la primera: «para ser una
pagina de documentacion me parece ridicula, no habla de los morfos, ni
nada de nada, ni la generacion de sonido, las utilidades, nada, la teoria,
el que se persigue... mezcla lo complejo con lo simple, como si el que la
lee supiera de donde viene todo».
Tenia razon: era una guia rapida disfrazada de documentacion. Enseñaba a
usar el sistema presuponiendo el ecosistema entero — mostraba un bloque
`semantic: { family, verb, intent }` sin haber dicho nunca que es un
morfo, ni donde vive, ni quien dispara el evento, ni como se convierte en
sonido audible.
REESCRITA COMO DOCUMENTO, ocho secciones que van de la teoria al oscilador:
1. QUE SE PERSIGUE — lo que faltaba entero. Un suceso es UNA cosa
expresada por varios canales; de ahi los tres objetivos que explican
todo lo demas: que la lectura sobreviva a perder un canal, que no
fatigue, y que sea coherente — y por que eso ultimo obliga a quitarle
al componente la libertad de elegir.
2. EL RECORRIDO — morfo declara → soma dispara → sema elige → $sound
fabrica, con la ignorancia mutua entre capas explicada como decision.
3. EL MORFO — que es, donde vive, para que sirve, con la declaracion REAL
del Toggle entera y las tres palabras (familia/verbo/intent) definidas
con sus conjuntos cerrados completos.
4. SEMA — los tres canales (visual estampa atributos, sound, haptic) y las
dos busquedas.
5. COMO SE FABRICA EL SONIDO — lo que no estaba y era la mitad del tema:
dos osciladores a una quinta mezclados al 30%, el filtro paso-bajo que
ES el brillo, la envolvente con sus topes y por que existen, el barrido
de contorno de ±400 cents, y la aspereza como trémolo en serie. Mas
como encaja un fichero de audio y su reserva.
6. INTERACTIVO — ahora muestra tambien la firma que recibe el motor.
7. LAS UTILIDADES — preferencias (full/reduce 0.4/off), memoria de
frecuencia (umbral 3, −15% hasta suelo 25%, ventana 2s, exenciones),
SILENT como retirada del canal, buses ui vs content, y la escala de
ventanas perceptuales.
8. CUANDO UN COMPONENTE QUIERE OTRA COSA — el caso practico, al final.
Los datos siguen saliendo del codigo (SEMA_MAP, el resolver); las
constantes del motor citadas en prosa se verificaron leyendo la voz de
sema y las constantes del canal, no de memoria.
⚠️ Bug propio de la escritura: backticks anidados dentro de un literal de
plantilla rompian el SSR (500). Cazado al verificar, no despues.
VERIFICADO: las cuatro paginas responden 200 en SSR · check sin errores en
ninguna de ellas.
⚠️ Sigo sin poder MIRARLAS — el panel del navegador no compone en esta
sesion. La composicion visual queda pendiente de tu ojo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
05d422ef4a |
docs(web): cuatro paginas del sonido, escritas para quien llega de nuevas
Correccion del autor sobre la primera version: «la redaccion esta mal,
empieza como si la gente supiera ya que es lo que has pasado, la redaccion
tiene que ser didactica para un nuevo desarrollador». Tenia razon — estaban
escritas como notas de version para quien vivio el rediseño: se abrian
presumiendo el vocabulario y presumian de cuantas reglas se habian borrado,
que es mi historia, no documentacion.
Reescritas desde cero, en ese orden: primero la escena («guardas un
documento y oyes un repique»), luego el vocabulario construido pieza a
pieza (familia, verbo, intent) y solo entonces la maquinaria. Cada
concepto se explica antes de usar su palabra.
/uix/docs/sound como suena una interfaz — el modelo, con la
resolucion en vivo (familia + verbo + intent)
/uix/docs/sound/catalogo cada sonido que existe, audible, con la razon
de por que suena asi y que significa cada eje
/uix/docs/sound/packs traer tu propia voz: sintesis, wav, mp3,
registrar nombres nuevos, cambiar en caliente
/uix/docs/sound/gestos por que un arrastre suena por repeticion, con
un simulador de tres velocidades para oirlo
DERIVADAS DEL CODIGO: los sonidos, familias, intents y verbos salen de
`SEMA_MAP` y del resolver real. Solo la prosa esta escrita a mano, asi que
añadir una entrada al catalogo la hace aparecer con sus medidas.
Lo historico no desaparece, cambia de sitio: donde aporta —por que los
sonidos son tan distintos entre si, que se gana y que se pierde con el
trinquete— va al final de su seccion como razon, nunca como apertura.
VERIFICADO: las cuatro responden 200 en SSR con sus secciones · check sin
errores nuevos en las paginas.
⚠️ Sigo sin poder mirarlas: el panel del navegador no compone en esta
sesion, asi que la composicion visual queda pendiente de tu ojo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
86fa9a1b77 |
refactor(sema)!: el sonido se ELIGE, no se modula — evento.intent = sonido
Directiva del autor, sin matices: «todo el sistema de sonido es una puta
mierda... la unica solucion es evento sonido, evento.intent = sonido, y
punto y nada de mierdas de que si el intent modifica nada».
Tenia razon, y lo que lo destapo fue oirlo: los dieciseis nombres del
catalogo anterior sonaban practicamente igual. Medido despues: cinco de
ellos eran LA MISMA nota de 700 Hz a cinco volumenes, y las variantes de
direccion se separaban 20 Hz. El modelo de modulacion producia teoria
bonita y mush audible.
LA REGLA ENTERA, y no hay mas:
nombre = per-emit ?? cascada ?? pack ?? morfo ?? familia[verbo] ?? familia.default
sonido = pack[`${nombre}.${intent}`] ?? pack[nombre] ?? nada
Dos busquedas. El intent SELECCIONA un sonido entero; si no existe la
variante se desprecia y suena la base; si no hay base, silencio. Un
`threat` no es un `tick` con mas aspereza: es otro sonido —mas grave, mas
rasposo, descendente— y no se pueden confundir.
EL TIER DEL VERBO es lo que vacia los packs. La tabla de familia se
indexa por verbo (`emerge.close → 'close'`), asi que ni un solo overlay
escribe su cierre. De ~165 reglas de sonido quedan 30, en 16 de los 71
packs — y las 135 borradas no decian mas que el defecto. Para que el
verbo llegue, ahora VIAJA en la señal: se calculaba y se tiraba con un
`void` (hallazgo S-40), el tipo prometia que se emitia y era falso.
CATALOGO NUEVO: 10 bases + 6 variantes, cada una un sonido diseñado
entero. Medido en el grafo de audio real: de 240 Hz (`tick.loss`) a
1900 Hz (`step`), casi tres octavas, y las variantes evaluativas traen su
propio modulador de aspereza — texturas distintas, no un tono movido.
100% sintesis a proposito: el pack por defecto no lleva binarios, funciona
sin red y no puede dar 404. Los .wav quedan como pack de EJEMPLO.
EL GUARD QUE FALTABA: `distinguishability` exige que dos entradas se
separen en al menos DOS ejes perceptuales. Nacio en rojo contra mi propio
catalogo —ocho pares casi gemelos— y dirigio el diseño hasta separarlos.
Una instantanea no lo habria cazado: cada valor era exactamente el que la
tabla decia; lo que faltaba era una RELACION entre entradas.
GESTOS POR REPETICION (idea del autor). Un arrastre ya emite a su ritmo,
asi que toca `step` (18 ms) por emision: la velocidad del gesto ES la del
trinquete, como una rueda fisica. Mueren los tres resolvers que
sintetizaban tono desde posicion y velocidad —la ultima aritmetica del
sistema— y `handle` queda EXENTA de la memoria de frecuencia, que si no
estrangularia el trinquete al primer arrastre. Perdida firmada: el ritmo
lleva la velocidad, pero la posicion ya no mapea a altura.
NADIE ESCRIBE PARAMETROS, ni la app. `SemaSignatureOverride.sound` y
`SemaCascadeRule.sound` aceptan un nombre o SILENT. La rendija de autoria
inline «para la personalizacion» sobraba: un producto REGISTRA su sonido
(nombre + definicion, una vez) y lo nombra.
⚠️ BUG PROPIO durante la cirugia: al reemplazar el bloque del sonido borre
tambien la aplicacion de deltas de intent, y el HAPTICO dejo de recibirlos.
Lo cazaron sus tests. Restaurado — el haptico sigue modulando porque su
vocabulario es categorico y sus deltas cambian el KIND, que tambien es
seleccion.
MUERE: los deltas de sonido del intent, las bases de sonido de familia, la
capa 1.5, `op:'add'` para sonido, `mergeSoundOverride`, y
`d7-intent-survives.test.ts` — vigilaba una inversion que ya no puede
existir.
VERIFICADO: sema+morfo+soma+sound 1108/1108 · docs:check 0/618 · la pagina
`/uix/docs/sound` reescrita y medida en navegador (commit+threat →
tick.threat; commit+affirm → tick, el intent se desprecia; emerge+close →
close sin una linea de pack; sustain → silencio). Los 7 fallos de la suite
completa son los ajenos y preexistentes de contracts.test.
⚠️ NO SE HA OIDO. Todo esta medido en el grafo de audio, no con un oido.
Si `open` suena a lo que debe sonar un panel abriendose es tu decision, y
se afina en un fichero.
⚠️ El estudio `/temas/sema` compila pero edita firmas crudas: su modelo es
el viejo. Queda por reconvertir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
09f8d76e02 |
fix(web): los 16 botones del catalogo sonaban EXACTAMENTE igual
Fallo mio, reportado por el autor y confirmado midiendo en WebAudio. `play()`
emitia `{ name: 'probe', family, target }` y NUNCA pasaba el nombre
seleccionado: el motor resolvia contra los packs registrados, ninguno de los
cuales casa con el elemento pelado de esta pagina, asi que los dieciseis
chips reproducian la BASE DE FAMILIA. Dieciseis botones, un sonido.
Es el peor tipo de fallo en una demo: la pagina MEDIA una cosa (la tabla de
capas, que estaba bien) y REPRODUCIA otra, asi que enseñaba justo lo
contrario de lo que dice. Quien la abriera concluiria que el sistema no
distingue nada.
El signal lleva ahora el nombre como override por evento, que el resolver
aplica en la capa 1.5 — el mismo hueco donde cae la regla de un pack — luego
lo que se oye es exactamente lo que la tabla calcula.
MEDIDO en el grafo de audio real, interceptando createOscillator/createGain:
antes todos los chips daban freq=[700,1050]; ahora van de 550 a 1080 Hz con
picos distintos (subtle 0.03 · loud 0.0825 · air 1080Hz · settle 920Hz ·
snap 1040Hz/arc).
Añade tambien `playLayers()`: reproduce base → nombre → intent en secuencia,
para que la aritmetica de la tabla se pueda OIR capa a capa. Sin cablear
todavia a un boton.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
d161cedccf |
feat(web): /uix/docs/sound — la pagina que explica el canal del sonido
Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las
tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina
que copiara el catalogo envejeceria al primer gain que alguien tocara — que
es exactamente la deriva que este subsistema se reconstruyo para quitar.
SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues
de tantas correcciones»: (1) en una frase — un componente dice una palabra;
(2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA
de cada entrada y no por una lista a mano; (3) el orden con los numeros de
verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede
llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que
—la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber
como funciona—; (7) el tema.
EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA:
el layout no pasaba `preferences`, asi que los canales se quedaban en 'full'
y el switch solo movia el intent de prefs (que gobierna el lado visual).
Publicar paginas que suenan con un mute que miente es peor que no
publicarlas. Ahora pasa un objeto VIVO — ambos canales leen
`preferences.{sound,haptic}` por emision, asi que mutarlo silencia al
instante sin re-registrar nada. Cierra §3.3 de la cola.
MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los
chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con
`fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de
flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent
sobrevive al nombre, que es la tesis del rediseño entero.
⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta
sesion. Verificado por arbol de accesibilidad, consola y estilos computados
— pero no lo he MIRADO. Queda pendiente tu ojo.
⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document`
en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR.
El nav estrena seccion «El sistema perceptual». Quedan las otras siete
paginas, y los seis enlaces muertos que Foundations ya declaraba
(/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos,
/uix/getting-started) siguen sin pagina.
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 |
|
|
6c65caf3f8 |
feat(direction): el censo cierra — picker, gradient-picker y chat-log aceptan dir
Los tres ultimos del catalogo sin la prop, pospuestos en D4 y firmados hoy:
- `picker` (el generico transaccional): `dir?: Direction` publico; el wrapper
corre `activeDir(() => dir)` en vez del `() => undefined` que solo seguia al
ancestro. `PickerOpts` gana la opt y el TIPO arrastra a sus dos composers
(gradient-picker y natural-time-picker pasan su afirmacion) — el generico
condicional de P4 haciendo exactamente su trabajo.
- `gradient-picker`: prop + cadena + morfo `direction: {}` + runtime.
- `chat-log`: prop + cadena + morfo, y reenvia la afirmacion a los
`FeedProvider`/`VirtualListProvider` que compone en triple registro — que la
exigian en OPTS desde la normalizacion y nadie les pasaba nada. **Sus 2
errores de linea base MUEREN: check pasa de 77 a 75.** Solo la bolsa del
chat-log se spreadea en el elemento compartido, asi que el estampado es suyo
(morfo direction) y Feed/VirtualList reciben el valor para su matematica.
El censo-guard (direction-census.test.ts) los cuenta sin excepcion nueva: prop
y morfo declaran el mismo hecho en los tres. Demos: gradient-picker y chat-log
tenian el control del arnes sin cablear — ahora pasan `{dir}`.
Verificado en Chrome, pagina en LTR y direccion por el chip de la demo:
gradient-picker root dir="rtl" · panel portalizado rtl · 0 islas
chat-log auto -> atributo AUSENTE (hereda) · prop rtl -> estampa rtl
picker via sus composers (no tiene demo propia)
check 75 = 77 - 2 (los de chat-log, desviacion explicada: la base MEJORA) ·
censo + compile + suites de los ambitos 73/73 · rtl:check 1 (palabras) ·
docs:check 0/566. El handoff marca la cola D4 como CERRADA.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
973d2c886d |
feat(direction): la proyeccion prefs->DOM es del boot, y el DOM la siembra
La proyeccion cross-modal (dir · lang · data-motion/sound/haptic sobre <html>)
era cableado manual de cada app: solo 4 de ~13 boots de la demo la tenian, y el
estampado per-componente del valor de prefs actuaba de SUSTITUTO de la
proyeccion que faltaba. Para que el flip de P3 pueda soltar prefs del atributo,
el ambiente tiene que llegar al DOM una vez y siempre:
- createActiveUix la crea por defecto (opt-out `projectPrefs: false` cuando la
app posee <html>: i18n por routing, proyeccion propia). En attach es opt-IN —
el inverso — porque el host puede gobernar el documento.
- Se dispone la PRIMERA en dispose(), antes de que prefs/dom mueran debajo.
- Los 3 cableados manuales commiteables se borran EN ESTE MISMO commit (uix,
blocks, BootUix); el de web/routes/alpha se edita en arbol pero no se
commitea nunca (regla de la rama). Borrarlos junto a la automatica evita el
agujero medido del doble montaje: dispose() BORRA los attrs gestionados sin
restaurar.
Y LA SEMILLA, que es lo que hace la automatica segura: sin ella, el boot
REESCRIBIRIA un <html dir="rtl"> puesto a mano (el escenario
|
2 months ago |
|
|
aaced3ebec |
fix(media-player): los tres floats no estaban portalizados ni llevaban el dir del player
Una revision adversarial del trabajo del dia (5 lentes, 3 escepticos por hallazgo) devolvio 15 supervivientes de 19. Estos son los que resulto que eran ciertos y son mios; los verifique uno a uno antes de tocar nada. ## 1. Fuga de teclas — el panel no estaba portalizado MEDIDO: con el menu de subtitulos abierto, ArrowDown bajaba el volumen del player de 1 a 0.95. El `<DropdownMenu.Content>` de eidos NO portaliza por si solo — hay que componer `DropdownMenu.Portal`, exactamente como VolumeFloat ya hacia con `Popover.Portal`. Sin el, el panel colgaba DENTRO de `[data-media-player]` y cada tecla del menu subia al `onkeydown` del player. Portalizados los dos menus. Medido despues: cuatro teclas (ArrowDown x2, Espacio, ArrowUp) y el volumen sigue en 1, sin arrancar la reproduccion. Eso arregla de paso otras dos cosas que la revision reporto aparte: el auto-hide de la barra ya no se lleva el menu abierto a opacity 0, y los tres comentarios que decian «the panel is PORTALLED» pasan a ser CIERTOS — lo eran solo en VolumeFloat. ## 2. El `dir` del player no llegaba al menu Los tres floats montan un componente del catalogo EN SITIO y no le pasaban `dir`. El resolutor no tiene paso de padre, asi que cada menu arrancaba su propia cadena y aterrizaba en la preferencia global: lista LTR dentro de un player RTL. Es justo lo que `c73669cde` canonizo horas antes (direction-contract §1, «la composicion lleva la afirmacion») — y el TimeSlider del MISMO componente ya lo hacia bien en dos sitios. `dir` va ANTES del spread para que el valor explicito del consumidor siga ganando. MEDIDO con el player en RTL: el panel portalizado sale `rtl` y coincide con el `data-dir` del player. Antes habria salido `ltr`. ## 3. La demo mentia en cuatro sitios El `eidosSnippet` se habia quedado NUEVE partes por detras del preview vivo (paridad D-3.1): sin transporte, sin tiempos, sin captions, sin PiP, sin los tracks. La rama de audio ignoraba `peaks`, que es justo el control nuevo. La prosa decia «two eidos-only floats» siendo tres, y que al engranaje le quedaban «quality and track» cuando la pista ya tiene lista propia. Comprobada la paridad midiendo el DOM contra el snippet: coinciden. ## 4. La tabla de eventos sema del README (esto era ANTERIOR a hoy) Publicaba `commit-set-time`, retirado en D-AP2.13, y cuatro nombres que ya no existen (`enter-fullscreen`, `exit-fullscreen`, `enter-pip`, `exit-pip`), le faltaba `commit-set-rate`, y daba «provider» como diana de todos cuando el morfo apunta a la parte concreta. Reescrita desde el morfo, los once. 43/43 · eidos-lint invalid 0 · class-hooks 0 · docs:check 0 · check 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
21d817c1f6 |
feat(media-player): con dos idiomas de subtitulos no habia forma de llegar al segundo
`toggleCaptions()` solo alcanza la PRIMERA pista, y a proposito: es un toggle, y las demas se quedan en `disabled` porque poner una en `hidden` OBLIGA al navegador a descargar su VTT — encenderlas todas bajaria todos los ficheros de golpe. La consecuencia es que un video con dos idiomas no tenia ninguna forma de llegar al segundo. `<MediaPlayer.CaptionFloat>` es la lista: `Off` + una entrada por pista, mismo patron que el `RateFloat` de hoy (DropdownMenu + RadioGroup, con el `CaptionButton` real de disparador y su toggle anulado con `preventDefault()` en la costura). OPT-IN: el boton solo sigue siendo el control correcto para una fuente de una sola pista. Por debajo, dos anadidos a soma y ninguno al puerto: - `selectCaptionTrack(track | null)` — pone UNA en `hidden` y el resto en `disabled`, por la misma razon de descarga. - `captionTrackList` + `activeCaptionTrack` — espejos reactivos. El `TextTrackList` vivo es una coleccion del DOM y NO notifica, asi que un chrome que LISTA las pistas no se enteraba de una que llegase tarde. Los refresca `syncCaptions`, que ya corria en addtrack / removetrack / change. Cambiar de idioma NO dispara `commit-toggle-captions`: solo lo hace una transicion real de encendido/apagado. Pasar de ingles a espanol con los subtitulos ya puestos no es un toggle — la misma distincion que el commit de mute hace con `s.muted !== wasMuted`. De paso, tres claves de texto que se me quedaron ayer solo en el LANGS del provider y no en el `texts` del morfo (restart, skip-previous, skip-next), mas `captions-none` para la entrada «Off». El `texts` del morfo ES la superficie i18n que pinta la demo, asi que faltar ahi es faltar de verdad. Verificado en Chrome con dos pistas VTT reales: abrir no toca los modos · exacto UNA pista en `hidden` y el resto en `disabled` · las cues PINTAN el idioma elegido (English «Inline captions demo track», Espanol «Pista de subtitulos de prueba») y Off deja la caja vacia, no congelada · panel 94px. Test nacido en rojo. 17/17 player · morfo:check media-player PASS · eidos-lint invalid 0 · class-hooks 0 · check 0 errores propios (78, los mismos de antes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
52fd56c9f3 |
docs(media-player): la pestana API de la demo se habia quedado rancia
Tres desfases, todos de hoy: - La lista de partes se quedo en las 16 originales: le faltaban las seis del modo audio (Artwork · Artist · Identity · Transport · RateButton · LiveIndicator · AudioLayout), los dos floats eidos-only (VolumeFloat · RateFloat) y las dos nuevas del transporte. Ademas ahora dice que RestartButton y SkipButton son OPT-IN, que es la parte que un consumidor no puede adivinar. - La tabla de props no mencionaba ninguno de los seis callbacks nuevos. Van con las dos divergencias declaradas (el `pause` del final natural no se reporta; `onEnded` si dispara para un final por salto) y con la regla de que una direccion sin manejador se pinta deshabilitada. - «The settings menu (speed / quality / track) is the remaining v2 item» ya no era cierto: la velocidad tiene lista propia desde `ee51172d8`, asi que al menu de ajustes solo le quedan calidad y pista. Medido en el navegador: la tabla no desborda su contenedor (864 de 865) y la pagina no gana scroll horizontal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b41669c438 |
docs(direction): los charts ya no estan en la cola — la linea llevaba rancia
§10.2 seguia diciendo que la familia chart "no tiene prop ni provider y resuelve leyendo getComputedStyle(node).direction". Eso lo cerro 43a788083: los cinco que necesitan direccion declaran dir?: Direction y resuelven por createChartRtl(eidos, () => dir) -> activeEidosDir. Verificado por grep que no queda un solo getComputedStyle(...).direction en la familia. La linea me indujo a repetirlo. El registro historico de §9.20 se queda como estaba — describe lo que era cierto entonces —; lo corregido es lo que apunta hacia adelante. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
64df3d7b37 |
feat(direction): la propagacion pertenece a la capa flotante
Un panel portalizado se renderiza FUERA del subarbol de su dueño, asi que la
herencia no lo alcanza: la afirmacion del dueño solo llega si alguien la lleva.
Eso estaba delegado en cada consumidor — siete envoltorios Content componian el
enlace al padre a mano — y quien COMPONE un popover no tenia por donde hacerlo.
Medido con <DatePicker dir="rtl"> sobre una pagina LTR: el wrapper flotante
estampaba dir="ltr", el picker-shell y su pie se disponian en LTR, y solo el
calendario espejaba. Panel partido por la mitad, el fallo del §2 del contrato.
La capa lo compone ahora una sola vez:
el dir propio de la superficie -> el dir afirmado por el proveedor DUEÑO -> omitir
- FloatingProviderOpts.dir el dueño baja su afirmacion (ya pasada por
activeDir, o sea prop -> prefs)
- FloatingContent.assertedDir donde se encuentran; lo leen los DOS estampados
del wrapper (rama nativa y rama JS)
- createFloatingShellRoot lo reenvia; es la puerta de 8 de los 10
FloatingProvider. Los 2 submenus toman el del
menu padre.
FloatingContentOpts.dir pasa a ser la prop CRUDA del Content. Resolverla ahi la
haria casi siempre concreta (prefs) y el dueño no ganaria nunca el fallback.
Se borran los 7 enlaces compuestos a mano con su soma / activeDir / parentRoot.
Sobrevive uno a proposito: dropdown-menu-sub-content lo necesita para
effectiveSide, que es matematica y quiere valor resuelto.
popover, tooltip y link-preview aceptan dir en su raiz. No estampan nada — no
renderizan elemento —; el valor existe para cruzar el portal, y es lo que
permite que los 9 componentes que componen un popover empujen su direccion al
panel.
Borrados 9 resolvedDir muertos en los proveedores Content: nadie los leia (toda
la matematica direccional consume el de la RAIZ) y el cambio de semantica de
opts.dir dejaba su docblock mintiendo.
Demos: las 7 que aceptan dir lo pasan ahora a la instancia — time-picker,
time-range-picker y color-picker no tenian control ninguno; emoji-picker y
natural-time-picker lo tenian solo en el arnes. Sin eso el camino de la PROP,
el unico que ejercita el portal, no era observable.
Verificado en Chrome real, direccion movida por el control de la demo:
date-picker Clear/Cancel/Close 15/102/209 -> 200/92/15 espejo exacto
date-range-picker 448/217/17
sin regresion dropdown-menu (raiz y submenu, data-side=left en RTL),
context-menu, menubar, select, sidebar (flyout), y tooltip
en auto sigue estampando ltr — la cola de prefs sobrevive
check 78 = linea base (A/B con stash) · 158/158 en los 20 ambitos tocados ·
rtl:check 1 (palabras, preexistente) · docs:check 0/565.
Queda documentado en el handoff un defecto PREEXISTENTE de otra familia que
esto vuelve visible: un componente eidos que monta otro componente del canon en
SITIO no le reenvia el dir, y el hijo estampa ltr cortando la herencia del
panel (natural-time-picker/Slider, emoji-picker/Command+ToggleGroup,
color-field/Select). No hay precedente de ese reenvio en eidos: es doctrina
nueva y movimiento propio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
5cbb03904d |
feat(media-player): el player ya le cuenta a la app cuando empieza y cuando acaba
Hasta ahora el componente no exponia NINGUNA api de eventos: cero props `onX` en la raiz. La app solo podia enterarse de la reproduccion por dos vias malas — observar mutaciones de los `data-*` del provider, o enchufar un `MediaProvider` entero por `createProvider`, que es una escotilla pesada para «avisame cuando acabe». Y sin embargo el player YA distinguia esos momentos por dentro para sonarlos: `commit-toggle-play`, `commit-complete`, `commit-fail`. Faltaba sacarlos. Se abren cuatro, espejo de esos: `onPlay` · `onPause` · `onEnded` · `onError`. Montan sobre los eventos del ELEMENTO, no sobre los botones, asi que la tecla, el gesto, las teclas de medios del sistema y un `play()` programatico caen todos aqui una sola vez, cuando el medio cambio de verdad. ## Dos divergencias deliberadas respecto a la capa perceptual - El `pause` que acompania al final natural NO se reporta. El elemento lo emite justo antes del `ended`, y contar los dos haria que cada pista terminase dos veces. Es la misma guarda `!s.ended` que ya usa el commit. - `onEnded` SI dispara cuando el final viene de un salto al final, aunque `commit-complete` se lo salte. Esa exencion existe para que un salto no SUENE como una consumacion; una app que encola la siguiente pista necesita el final venga de donde venga. Las dos reglas van en el test. Leidos tarde (`Active`, como `onValueCommit` del Slider) para que cambiar el handler a mitad de reproduccion se respete. La demo los pinta en una lectura `lifecycle` al lado de `trace`, a proposito: `trace` es lo que el player SUENA, `lifecycle` es lo que le CUENTA a la app. Verificado en Chrome con un <video> real: play → onPlay, pause → onPause, final natural → onEnded SIN un onPause espurio delante. Test nacido en rojo (desconectadas las 4 llamadas: 0 invocaciones). player + waveform 19/19 · check 0 errores propios (78, los mismos de antes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
ee51172d8f |
fix(media-player): de 2x no se podia bajar, y la etiqueta movia la botonera
El `<RateButton>` sólo sube: su `onclick` es `cycleRate(1)` y el paso recorta contra el último preset, así que quien se pasaba de `2×` se quedaba ahí — bajar era exclusivo de las teclas `<` / `>`. Con ratón no había vuelta. `AudioLayout` pasa a componer `<RateFloat>`: el DropdownMenu del catálogo con un RadioGroup, porque la velocidad es una elección de estado dentro de un conjunto y la actual debe anunciarse `aria-checked`, no sólo parecerlo. Es lo que abren YouTube, Vimeo y Plyr. El `<RateButton>` sigue exportado para quien arme la botonera a mano. El disparador sigue siendo el RateButton de verdad — conserva su parte del morfo, su `aria-label` y el `data-rate` del que sale la etiqueta. Su ciclado se anula en la costura: `composeHandlers` se salta el handler de soma cuando el `onclick` del consumidor llama a `preventDefault()`, así que el clic abre la lista en vez de empujar la velocidad. Medido: 4 ciclos de apertura y cierre con la velocidad intacta. Los presets salen de `MediaPlayerProvider.RATE_PRESETS` (ahora público, y la clase se exporta como ya hace el calendar) — los mismos que recorren las teclas. Dos copias a mano habrían derivado. ## Los dos defectos del panel portado Mismo par que destapó el float de volumen, y por la misma razón: lo que se porta sale del ámbito del player. - El menú traía un suelo de `12rem` pensado para prosa («Show notifications»), con la etiqueta más ancha en `1.25×`: 192px de panel para 33px de texto. El marcador `data-media-player-rate-float` viaja con el panel y baja el token a cero → 77px. - El botón pasa a ancho fijo. Medido al tipo del control, `1×` ocupa 0,90em y `0.75×` 2,41em: cambiar de velocidad redimensionaba el botón y DESPLAZABA toda la botonera — el mismo defecto que tumbó el hover horizontal del volumen. En `em` para que siga a `data-size` sin una entrada por tamaño. Medido en los cuatro: 64 · 74 · 88 · 121 px, ancho único por tamaño y el mute clavado. Verificado en navegador: abrir no toca la velocidad · 2× → 0.5× → 1.25× con el elemento obedeciendo · panel 77px · botonera inmóvil en sm/md/lg/xl. check 0 errores propios (78, los mismos de antes) · player + waveform 18/18 · eidos-lint invalid 0 · class-hooks 0 · docs:check 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
9fd0de84d6 |
Reapply "feat(media-player): VolumeFloat — el volumen en panel, sin mover la botonera"
This reverts commit
|
2 months ago |
|
|
c798f7465a |
Revert "feat(media-player): VolumeFloat — el volumen en panel, sin mover la botonera"
This reverts commit
|
2 months ago |
|
|
26ddbaa1c9 |
feat(media-player): VolumeFloat — el volumen en panel, sin mover la botonera
Opcional: componer MuteButton + VolumeSlider al lado sigue siendo la forma en linea; esta es la otra y la elige el desarrollador. POR QUE UN FLOAT Y NO UN DESPLIEGUE EN LA FILA. Se probo lo segundo —el patron de YouTube, rail a inline-size 0 creciendo al hover— y se descarto con el usuario delante: un slider que se expande dentro de una fila flex EMPUJA todo lo que va detras (tiempo, scrubber, subtitulos, PiP, pantalla completa) y en una fila apretada desborda. YouTube se lo permite porque su barra ocupa la ventana entera; la nuestra no. El panel va fuera de flujo. VERIFICADO que no mueve nada: se compararon las posiciones de los DIEZ controles con el panel cerrado y abierto — identicas, cero diferencias. Es la promesa que incumpli con el hover y que esta vez se midio. Todo compuesto del catalogo, sin reinventar el flotado: Popover con openOnHover (ya existia; solo se acorto la apertura a 120ms —esto es un control, no un tooltip— y se alargo el cierre a 400ms para poder viajar hasta el), MuteButton como trigger via child (sigue siendo el boton de verdad: conserva aria-pressed, el atajo m y el glifo por nivel) y VolumeSlider vertical, el eje que el morfo declaraba desde el principio. El gesto: hover abre, CLIC SILENCIA y cierra, que es el remate natural de esa accion. En tactil no hay hover, asi que el toque hace las dos cosas; es ademas la unica via por la que un usuario tactil llega al slider. TRAMPA ENCARADA: los tokens --_mp-* NO cruzan el portal (se declaran en [data-media-player] y el contenido sale fuera), asi que el tinte del slider lleva fallback al rol que envuelve. Sin el, el control sale sin color dentro del panel. Demo con chip volume inline|float para compararlos en la misma barra. check 0 errores propios · player 14/14 · eidos-lint invalid 0 · docs:check 0 · prettier limpio. Panel 48x131, slider 32x96 vertical, visto en Chrome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f9e372e6fe |
feat(media-player): captions propias + el eje de dirección, canonizado
Dos hilos que comparten ficheros, así que van juntos. ## Captions — el player pinta las cues La parte estaba declarada en el morfo desde el principio y nunca implementada. El render nativo no servía: con la pista en mode='showing' el navegador dibuja su caja de cues en bottom:0 del <video> — la misma franja de 49px que la barra de controles — y contra el z-index:4 de la barra al 82% de opacidad llegaba el 18% del texto. ::cue no puede moverlo (la spec no admite propiedades de posición ahí). Ahora la pista se mantiene en 'hidden': la spec deja la pista activa —cues parseadas, activeCues mantenida, cuechange disparándose— y el navegador no pinta nada, así que el texto se monta en la parte Captions, por encima de la barra. Es el movimiento de Plyr, y por el mismo motivo. Tres trampas de la especificación, encaradas por escrito: - Pasar una pista a 'disabled' desactiva sus cues SIN disparar cuechange. Nadie avisa de que hay que borrar, así que se limpia a mano — si no, el último subtítulo se queda congelado sobre el vídeo. - Los eventos de cue no corren mientras está puesto el show-poster flag, así que el primer pintado se siembra leyendo activeCues. - cues/activeCues son null en 'disabled'. De paso cierra un defecto latente: captionsOn era un espejo de una sola dirección que nunca releía track.mode, así que si el SO activaba subtítulos salían cues mientras el botón decía que estaban apagados. Ahora se DERIVA de los modos reales, y una pista que el navegador encienda se adopta (degradada a 'hidden', para que no pinte encima de la nuestra). Decisiones declaradas: el marcado inline se conserva (se monta el DocumentFragment de getCueAsHTML como nodos, nunca por innerHTML — el sumidero de XSS que abre Plyr al serializarlo de vuelta a cadena); la metadata de posición (line/position/align/vertical/snapToLines/regiones) se DESCARTA. Esto no es una implementación de WebVTT y el README lo dice. ## Dirección — el player estaba partido por la mitad Con dir="rtl" por prop y sin dir ambiental, el cromo se disponía en LTR y sus dos Sliders en RTL: el wrapper pasaba la prop cruda y sólo la reenviaba a los hijos, sin resolver la cadena ni estampar. En la demo estaba enmascarado porque el stage estampa dir además de pasar la prop. Canonizado con el patrón de CONTINUE-direction.md §9.16: activeDir en el wrapper, resolvedDir en el provider, dir crudo + data-dir resuelto en los props. El censo del §9.15 no lo tenía — era un cuarto caso que su búsqueda no cubre (acepta dir y no lo escribe en ningún sitio). Y los glifos direccionales no se volteaban: en RTL el transporte se leía ▶▶ ▶ ◀◀, con las dos flechas exteriores apuntando HACIA DENTRO — el síntoma que carousel.css ya documentaba. Se voltean con scale:-1 1 (no rotate:180deg: sólo coinciden en glifos verticalmente simétricos, y rotar el PiP le llevaría el recuadro a la esquina de arriba), anclado a data-dir, el atributo propio del componente. Alcance elegido: seek, play, volumen y PiP; captions y fullscreen NO. El teclado pasa a resolver por getDirectionalKeys, así que las flechas concuerdan con el glifo y con el Slider embebido. ## Verificación check 0 errores propios · vitest del player 14/14, con los tres guards nuevos vistos en ROJO por mutación · eidos-lint invalid 0 (morfo-backed 25 → 29) · morfo:check sin fallos del player · component:audit PASS · smoke PASS · docs:check 0 · prettier limpio en lo propio. A ojo en Chrome real: claro y oscuro, LTR y RTL, y las dos regresiones (el espejo del SO y el subtítulo congelado). Nota: el README de soma se lleva una línea ajena — la fila `dir` de la tabla de props, del barrido de otra sesión, que describe justamente esta cadena. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b906dd4077 |
fix(direction): el atributo `dir` se estampaba VACIO cuando nadie lo afirmaba
El contrato de `013ceac57` dice que `dir` se OMITE cuando nadie ha afirmado una
direccion. `6.5` habia visto `dir=""` en sidebar y tree-view sin averiguar de
donde salia. Es el shorthand `{dir}`: para `dir` Svelte emite una asignacion de
PROPIEDAD ademas del atributo, y con `undefined` el resultado es la cadena
vacia en vez de la omision. Atrapado con un trap sobre `setAttribute` + el
descriptor de la propiedad: dos escrituras, la segunda por propiedad.
No estaba en el SSR (`dir=""` no aparece ni una vez en el HTML servido) — lo
escribia el cliente al hidratar.
Un valor invalido HEREDA, asi que nada pintaba mal; pero contradice la letra
del contrato y hace que `[dir]` matchee.
Arreglado con spread condicional, que si omite:
{dir} -> {...dir ? { dir } : {}}
- `tree-view` (eidos): el wrapper LO NECESITA cuando el valor es concreto — el
recipe selecciona con `[data-tree-view-root]:dir(rtl)` y ese div es el
ANCESTRO del provider que estampa el attr. Con el valor ausente hereda, que
es lo correcto.
- 26 demos: el arnes estandar (`data-uix-stage-area` / `data-uix-canvas-inner`).
Verificado: en `auto` solo quedan `<html>` (la proyeccion de prefs) y el
provider con su valor resuelto — cero `dir=""`; en `rtl` los tres elementos lo
reciben concreto; al volver a `auto` desaparece limpiamente. Las 26 rutas
sirven 200 y ninguna trae `dir=""`.
METODO: mi primer regex tambien matcheaba dentro de `dir={dir}` y dejo
`dir={...}` — dos ficheros con parse error que `check` cazo (76 vs 74). Contar
ficheros modificados NO basta; hay que mirar el diff.
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 |
|
|
b0f995a3bc |
refactor(metrics)!: placement 'right' pasa a 'end', que es lo que siempre hizo
El CSS de `metrics` es integramente logico (`inset-inline-end`, `padding-inline-end`), asi que `placement="right"` ya ponia el chart a la IZQUIERDA en RTL. Medido: fromRight 0 en LTR, fromLeft 0 en RTL. El comportamiento era el bueno; mentia el nombre. Y era una ISLA: `layout: 'icon-start'`, `align: 'start'` y el resto del vocabulario del componente son logicos — `right` era el unico termino fisico de toda su superficie. No es el caso legitimo del `toast`, que es fisico y coherente CONSIGO MISMO; aqui la incoherencia estaba dentro del componente. BREAKING: `MetricsChartPlacement` pasa de `'below' | 'right' | 'full'` a `'below' | 'end' | 'full'`. Sin shim (doctrina del proyecto): actualizados el tipo, los dos selectores del recipe, el README y la demo. Fuera de ellos no habia consumidores. No tocado y señalado: el token `--metrics-chart-right-width` conserva el nombre fisico. Describe un ANCHO, no un lado, y es superficie publica de theming — su renombrado es otra decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
1857c7854d |
fix(demos): drawer, slider y float-panel no dejaban ver su mitad logica en RTL
Los tres eran agujeros de OBSERVABILIDAD — el componente estaba bien, la demo
no permitia mirarlo. Y cada uno lo era por un motivo distinto:
drawer — faltaba el control: solo listaba top/right/bottom/left, y
start/end son justo los que ejercitan RTL. Medido: start da
left en LTR y right en RTL; left fisico NO voltea, correcto.
slider — faltaba el control: secondaryValue solo se veia en la demo del
waveform, que es horizontal, asi que vertical+RTL no era
observable. Cableado como un mando de porcentaje, con
SecondaryRange montado siempre (sin valor pinta a tamano cero).
Horizontal espeja; vertical sale IDENTICO en las dos
direcciones, que es lo correcto: el eje de bloque no gira.
float-panel — el control existia (useAnchor, con side y align) pero otro
estado lo anulaba: posA arranca en {x:24,y:24} y seedRect()
solo siembra con position vacia, asi que la semilla anclada
nunca corria y el align era inerte. releasePosition() la suelta
al tocar useAnchor / side / align, que son excluyentes con una
posicion fija por diseno.
Y aun asi seguia sin verse, por un CLAMP: el disparador nacia
pegado al borde del stage, asi que align=end pedia x=-173 y
volvia a 0. Centrada la fila de disparadores. Entonces si:
start->dLeft 0 / end->dRight 0 en LTR, espejado en RTL, y
center no gira.
Con esto el arreglo de
|
2 months ago |
|
|
ae756ee578 |
fix(scroll-area): el eje horizontal no se espejaba en RTL — el thumb salia del carril y aria-valuenow era negativo
En RTL el navegador DESCANSA scrollLeft en 0 con el contenido pegado a la derecha y lo lleva NEGATIVO hacia el final de lectura (modelo negativo del CSSOM). Nadie lo traducia, asi que resolvedDir era codigo muerto y toda la matematica del eje inline estaba rota: aria-valuenow -50 / -99 con aria-valuemin="0" -> 0 / 50 / 100 thumb left: -167px, FUERA del carril -> dentro y espejado data-at-left siempre puesto -> solo en el borde izq. data-at-right nunca -> en reposo click canalon al 10% desde la izquierda -> 0 -> -232, el 90% logico Dos traducciones privadas en el provider y cada consumidor toma la suya: lo que sigue el eje de LECTURA (offset del thumb, aria-valuenow, click en el canalon) y lo que se queda FISICO (data-at-left / data-at-right, cuyos nombres lo son y cuyo uso documentado —sombras de scroll— tambien). El ancla del thumb pasa a inset-inline-start porque el offset es progreso de lectura: es la decision opuesta a tabs / sliding-indicator, donde el JS media una coordenada fisica. La regla no cambia: mira de que lado esta la medida. El arrastre no necesito nada, y no por suerte: subir scrollLeft mueve el viewport a la derecha en los dos marcos. Verificado, no supuesto. Ambos extremos llevan ahora la misma holgura de 1px, porque solo uno cae en un cero exacto y cual de los dos depende de la direccion. De paso LTR gana el data-at-right del extremo, que tampoco se encendia. Medido en Chrome en las dos direcciones, con el toggle del topbar. El test nuevo sale ROJO con el provider anterior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
01b891b645 |
fix(chart,docs): el radar volcaba sus etiquetas sobre el poligono en RTL, y los bloques de codigo se volvian ilegibles
RADAR-CHART. No debe espejarse: sus etiquetas se colocan en ANGULOS alrededor del poligono, no sobre un eje de lectura. Pero el anchor de cada una se elige por el coseno —`start` en la mitad derecha, `end` en la izquierda— y `text-anchor` es logico, asi que bajo RTL todos se invertian y las etiquetas caminaban hacia dentro: medido, "Reliability" pasaba de [254,308] a [201,255] y "Economy" de [-6,47] a [46,99], las dos encima del grafico. Anchor fisico con el layout sin espejar, que es el caso del eje Y. De paso, el ternario del anchor se anota como union en vez de inferirse `string`. BLOQUES DE CODIGO DE LAS DEMOS. En RTL el algoritmo bidi reordenaba la puntuacion neutra y las muestras salian ilegibles: `<script lang='ts'>` se leia `<'script lang='ts>`, el `;` final de una linea saltaba al principio y `color="affirm"` salia `"color="affirm`. El codigo fuente es LTR sea cual sea la direccion de lectura — no es prosa —, asi que `pre/code/kbd/samp` lo declaran. Es la version ESTRECHA del pin que llevaba `<main data-uix-canvas>` y que quite en ec533845d: acotado al CODIGO, que es inequivocamente LTR, en vez de a todo el canvas, que tambien contenia las demos vivas y las congelaba a las 51 en LTR. Verificado MIRANDO capturas en RTL: el radar con las seis etiquetas fuera del poligono, y el snippet alineado a la izquierda con la puntuacion en su sitio. check 74 = linea base exacta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
8086f21d6e |
fix(chart): el formato del eje Y no llegaba al tooltip, asi que ambos etiquetaban distinto el mismo punto
Reportado por el usuario con una captura: el eje decia "55k" y el tooltip "55"
para el mismo punto de Marzo.
El chart NO calcula mal. Los datos de la demo son decenas (42, 48, 55...) que
representan miles, el trazado es correcto y el punto cae donde debe. Lo que
fallaba es que el formateador del valor Y llegaba SOLO al eje.
EN EL FRAMEWORK, y afecta a los cinco presets. line-chart, area-chart,
bar-chart, bubble-chart y scatter-chart aceptan `formatY` y lo pasan a
<YAxis format={formatY}> y a nadie mas. El tooltip lo activan con la prop
booleana `tooltip` del Chart, que deja tooltipConfig en null, asi que el valor
cae al formatNumber por defecto. Resultado: `<LineChart formatY={money} />` da
eje "51k" y tooltip "51", y el consumidor NO puede arreglarlo porque el preset
no expone el formato del tooltip. Ahora cada preset declara
`{#if tooltip}<Tooltip format={formatY} />{/if}`: los dos rotulan el MISMO valor
y deben decir lo mismo.
EN LA DEMO. El chart compuesto —el de la captura— pasaba `format={money}` al
YAxis y dejaba `<Chart.Tooltip />` sin formato. Corregido, y tambien el snippet
de ejemplo que la pagina muestra, para que no ensene el patron incompleto.
Medido en Chrome disparando el pointermove sobre los tres SVG a la vez. Antes:
compuesto "May revenue 67 expenses 44", LineChart "Apr revenue 51", AreaChart
"Apr revenue 51" — todos contra un eje 0k..100k. Despues: "67k / 44k",
"51k / 35k" y "51k", coherentes con su eje.
Verificado: check 74 = linea base exacta, 54 warnings tambien. No hay tests de
chart en el repo. Prettier: 19 avisos en esos .svelte ANTES de tocarlos, 15
despues — ninguno nuevo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
ec533845d1 |
feat(direction): un guard para el eje logico-fisico, y el <main> que congelaba las 51 demos
Cierra la §2.1 del handoff. El guard nacio en rojo con 24 hallazgos y al arreglarlos aparecieron dos defectos mas grandes que el que buscaba. 1) EL GUARD RTL-1. Nada cruzaba un ancla LOGICA con un desplazamiento FISICO en la misma regla CSS: eidos-lint clasifica SELECTORES, no declaraciones, y la fila RTL del contrato de construccion decia "rule (LIVE)" — revision humana. Por eso el defecto del slider llego a produccion y lo cazo el ojo del usuario. Ahora `npm run rtl:check`, con la logica en src/uix/eidos/rtl-lint.ts (14 tests) para que sea testeable, calcando el par lint.ts / scripts/eidos-lint.ts. El test ancla el caso historico: slider.css ANTES de |
2 months ago |
|
|
5469e05df9 |
feat(direction): las demos dejan de clavar ltr, y la prosa inglesa se declara inglesa
Cierra el eje de direccion abierto en
|
2 months ago |
|
|
013ceac574 |
fix(direction): getDir() no era reactivo, y 33 componentes estampaban su valor congelado
El slider no respondia al cambio rtl/ltr. Resultaron ser dos defectos
independientes, y el segundo era del ecosistema entero.
1) EL SLIDER. slider.css emparejaba un inset LOGICO con un transform
FISICO en las tres reglas verticales (inset-inline-start: 50% +
translateX(-50%)). Los transforms no se voltean, asi que en RTL el rail y
el relleno quedaban fuera de eje un ancho entero. Medido en Chrome:
raiz/pulgar/ticks en x=961, rail y relleno en x=955. El Tick se libraba
porque ya usaba margin-inline-start — ese era el idioma correcto del
propio fichero y es el que aplico. Mismo defecto en accordion.css:
text-align: left en el trigger, que era el UNICO text-align fisico de
todo eidos; ahora quedan cero.
2) LA RAIZ. makeDimension().get() leia engine.snapshot() esquivando la
celda $state, asi que prefs.<dim>.get() — y con ella el
soma.prefs.getDir() que la guia manda usar — era una lectura sin
tracking. Los ~64 componentes que resuelven su direccion desde prefs la
congelaban al montarse. La proyeccion DOM se salvaba porque usa
onChange: misma dimension, dos caminos de lectura, solo uno reactivo.
Arreglado con un contador de cambios POR dimension, bombeado desde el
effectiveDiff que el motor ya calcula, de modo que cubre tambien el
cambio por DERIVACION (direction siguiendo a language) y no solo el
intent. Un contador por clave y no una celda global: con una sola celda,
tocar motion despertaria las 64 derivaciones de direccion. El test
negativo lo clava.
3) VALOR NO ES AFIRMACION. 33 de 37 componentes estampaban dir en su raiz
con un valor siempre concreto, asi que una app que ponga html dir=rtl sin
registrar la preferencia se encontraba 33 islas volteadas del reves.
Investigadas las cinco librerias de referencia: MUI y react-aria no
estampan nunca, Radix estampa siempre (sus mantenedores arrastran
discussions/1405 por esto), y Zag estampa prop("dir") — presente si
alguien lo pidio, ausente si no. Adoptado el modelo de Zag pero uniforme:
el atributo NUNCA tiene defecto. La herencia la hace el navegador por
AUSENCIA de atributo, sin ninguna lectura del DOM, asi que la cadena
prop -> prefs -> 'ltr' se queda intacta.
4) HOMOGENEIDAD. Habia DIEZ formas distintas de resolver lo mismo en los
wrappers y CINCO de declarar el tipo, con un cuarto escalon semantico
—heredar del menu padre— enterrado en dos expresiones sueltas.
Normalizado a activeDir(getter, soma) en los 36, un solo tipo
Direction | undefined, y el escalon de los submenus declarado en la
llamada, donde se ve.
5) LA SHELL. El toggle de direccion pasa de 2 a 3 estados: auto hace
clearIntent, que es lo unico que deja ganar al derive. Y entra el arabe
en el catalogo de idiomas, porque sin un idioma RTL la derivacion no era
disparable. Con las dos cosas, elegir arabe voltea la direccion sin tocar
el control — la cadena documentada funcionando de punta a punta por
primera vez.
Verificado: check 74 = linea base exacta, cero nuevos. 602/602 tests en
101 ficheros. En Chrome real: el topbar mueve al slider y al accordion en
vivo, y el arabe voltea por derivacion.
QUEDA: 48 demos siguen clavando dir='ltr'; el interrogante desplazado del
accordion necesita dir="ltr" lang="en" en el contenedor de la demo (no es
un fallo nuestro, es bidi correcto sobre texto ingles en parrafo RTL);
html lang no sigue al idioma; smoke y perm:check sin ejecutar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3ceda5b593 |
fix(media-player): el boton de velocidad no decia su velocidad, y los sliders ignoraban la direccion
Dos defectos que solo aparecieron al MIRAR la pagina en un navegador real
(el panel embebido va a innerWidth 0 y sus medidas mienten: durante esta
misma sesion invento tres "overflow" inexistentes).
1) EL BOTON "x". El usuario pregunto que era ese boton que "no hace nada".
Si hacia: playbackRate llegaba a 1.5. Lo que no hacia era DECIRLO. El morfo
declaraba `data-rate` con propRef pero sin `emit: 'value'`, y sin enum el
compilador lo trata como bandera de presencia (compile.ts: hasEnum false y
emit !== 'value' -> 'html-presence'), asi que emitia data-rate="" para
siempre. El wrapper eidos pinta `{data-rate ?? 1}x` y `??` NO atrapa la
cadena vacia, de modo que el control mostraba un "x" pelado mientras la
velocidad cambiaba por debajo. Un boton que parece muerto. Anadido
`emit: 'value'` (el patron que chronos.ts ya practicaba). Verificado en
Chrome: "1x" -> click -> "1.25x" con playbackRate 1.25.
2) LA DIRECCION. Ni el TimeSlider ni el VolumeSlider recibian `dir`: cero
menciones en ambos ficheros. Caian al default del Slider, que resuelve de
la config de soma — que es GLOBAL de la app. Resultado medido: cromo del
player en ltr con sus DOS sliders en rtl, pulgares anclados al borde
equivocado. Es el mismo defecto que el Waveform tenia hoy ("dir no se
reenviaba al Slider embebido") y el mismo arreglo: el player gana `dir` y
lo reenvia a los dos. Verificado en Chrome: stage/player/ambos sliders
coinciden, tiempo en left:0% con valor 0 y volumen en left:100% al maximo.
La demo reenvia ahora su eje `dir` a los dos roots (AudioPlayer y
MediaPlayer), asi que el RTL del player es comprobable POR PRIMERA VEZ: el
arnes fijaba el stage y los roots no lo recibian.
Verificado: check 75 = baseline, 0 propios; 19 tests; component:audit
--only media-player PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
77f8a4e46f |
docs(waveform): la demo carga solo la pista real, y `sample` deja de fingir que es una prop
Fuera los earcons de sema del selector: eran material de INTERFAZ (blips de 80-220 ms; `pop` pintaba casi plano) en la demo de un componente de CONTENIDO, y estaban ahi solo porque eran el unico audio del repo. Con una pista real disponible, ensenaban lo que el componente no es. Y con una sola fuente, un radiogroup de una opcion es ruido: `sample` deja de ser un control y pasa a ser un dato en modo lectura. Eso hace visible en la pagina la frontera que el usuario pregunto explicitamente — debajo (`shape`, `value`, `step`, `secondaryValue`, `readonly`, `disabled`, `dir`, `size`) son PROPS del componente; `sample` y `buckets` NO lo son: son como la demo fabrica los `peaks`. El contrato del waveform es `peaks: number[]` y nada mas (D-WF.2): la app provee los datos, el componente nunca descarga ni decodifica. Verificado: check 75 = baseline, 0 propios; sonda en vivo — la onda sigue pintando los picos reales de la pista (path de 4031 chars), shape/bars vivo. 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 |
|
|
df86a4e509 |
fix(slider,media-player): el anillo del commit no enmarca contenedores, y saltar al final no es completar
Cuatro defectos que solo aparecieron cuando el usuario MIRO la pagina.
1) EL BORDE DE LOS SLIDERS ("se viene arrastrando desde el principio").
No era del arquetipo ni del slider: es la firma GLOBAL de la familia
commit. BUILTIN_SIGNATURES.commit monta commit-settle, cuyo 30% pinta
box-shadow 0 0 0 3px. El commit-set del Slider apunta al PROVIDER, asi que
cada paso de flecha y cada release dibujaban un aro alrededor de la caja
entera (medido en vivo: 544x40). Un aro lee como feedback en un boton y
como borde parasito en un rail ancho y transparente. Exencion acotada en
slider.css — el patron que gradient-builder.css ya practicaba. Alcanza a
todo slider embebido (waveform, media-player, color-picker, time-picker)
porque todos llevan [data-slider]. `animation` es la palanca, no
`box-shadow`: las declaraciones de keyframe ganan a las normales, asi que
solo reiniciar el NOMBRE de la animacion detiene la pintura.
2) EL MISMO ANILLO EN EL PLAYER: commit-complete / commit-fail apuntan al
provider -> borde alrededor de los ~700px del reproductor. Misma exencion
en media-player.css. Los botones conservan su aro pequeno (20x36), que es
donde el feedback pertenece.
3) SALTAR AL FINAL ANUNCIABA UNA COMPLECION FALSA. Los botones de salto no
tienen evento propio: seek() delega a proposito (D-AP2.13, "el release del
scrubber ya ES el commit-set del Slider") — cierto para el scrubber y
VACIO para los botones, que no tienen Slider detras. Al saltar mas alla del
final el elemento dispara `ended` -> commit-complete en el provider ->
sonido (es audible por D-AP2.7) + anillo, mientras el boton de retroceso
quedaba mudo. Saltar al final no es completar: es abandonar la obra en el
final. seek()/scrubTo() marcan el salto que aterriza en el final y `ended`
no anuncia; un `play` posterior limpia la marca, asi que el final natural
se sigue anunciando. Los dos botones quedan simetricos y mudos, que es la
doctrina del transporte.
4) MEDIASESSION SIN OVERLAY DEL SO: el modo audio YA funcionaba (metadata
completa, medida en vivo); el roto era VIDEO, el modo por defecto de la
demo. <AudioPlayer> deriva metadata de title/artist/artwork; el
<MediaPlayer> compositivo no puede leer su hijo Title sin volverse
data-driven, asi que la declara el consumidor — y la demo no lo hacia.
El arreglo es de la demo; el art se comporta como D-SR.6/D-AP2.5 lo firmo.
Guard nuevo (no anunciar compleccion al saltar al final; si al final
natural) VISTO EN ROJO antes de darlo por bueno.
Nota de doctrina, tras leer el corpus a peticion del usuario: la exencion
acotada en la receta ES la puerta, no un olor. motion.md §4 fija que la
firma es generica por decision estructural ("un commit asienta igual en
todo el sistema"), eidos es el UNICO dueno del canal visual, los packs de
sema solo transportan sound/haptic, y el VisualChannel no consulta
activeChannels — no hay palanca declarativa para callar solo lo visual.
Queda anotado, menor: commit-settle escribe 3px a pelo donde sus hermanos
announce-pulse-* usan var(--focus-ring-width).
Verificado: check 75 = baseline, 0 propios; 11/11 media-player;
motion.test.ts verde (la firma global intacta); sonda en Chrome vivo —
slider real -> animation none, elemento no-slider con el mismo sello ->
sigue commit-settle, aro de foco intacto.
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 |
|
|
6036560ff0 |
fix(blocks): la galeria emite — packs de sema y canales perceptivos en el arnes
El motor SI emitia: pulsar un boton de un block estampa `data-event=contact-activate` con `family=contact` y `phase=active`, porque el morfo del `Button` declara ese evento (verbo `activate`, secuencia `pre`, sin intent a proposito — una pulsacion no carga intencion evaluativa). Lo que faltaba era el otro lado: la seccion arrancaba UIX **sin `events` y con cero packs de sema**, frente a los 69 de `/uix`. El evento salia a una sala sin altavoces. Viene de F0, cuando el arranque se monto como «espejo minimo» de `/uix` y sema se quedo fuera. Un framework semantico cuya propia galeria no suena no esta demostrando lo que existe para demostrar, asi que los canales van ENCENDIDOS aqui: `sound` y `haptic` a true en las dos cadenas de arranque (galeria y previews). La lista de packs es DERIVADA del barrel (`Object.values(packs)`), no escrita a mano, y eso es deliberado: la lista a mano del otro arnes se quedo atras dos veces — `69c108d2a` tuvo que anadir «los 25 packs que faltaban en la composicion de demos». Una lista derivada no puede quedarse atras. El barrel recomienda importar solo los que usas por tree-shaking; aqui no aplica, porque esto no es un producto sino la galeria del framework y un block compone lo que quiera del canon. Verificado espiando la Web Audio API en un clic real dentro del block: al cargar 1 AudioContext y 0 fuentes; tras pulsar, 2 fuentes y 3 nodos de ganancia mas. Y las 15 demos cargan limpias con los 69 packs, cero errores de pagina. Gates: blocks:check verde (15) · svelte-check sin errores en lo tocado · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |