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 }
463 Commits (dc7c4920d640e1fd387aabf3f3ccf0c186d7603e)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
dc7c4920d6 |
blocks(cookie-consent): la ley europea como tipo, y las dos salidas al mismo peso
Cierra el tramo C de F2b. El único block cuya fase 0 fue jurídica antes que visual: las referencias shippean el aviso como markup, y el markup no puede impedir las tres cosas que lo vuelven ilegal — un rechazo debilitado, categorías premarcadas, y cerrar tratado como consentir. Aquí son el tipo y el constructor. `essential: true` hace inescribible una UI que ofrezca apagarlo; `initialConsent()` no admite semilla, así que una primera pregunta no puede llegar marcada; `onDecision` es obligatoria, así que un aviso que no informa de vuelta no se puede montar. Las cinco reglas están numeradas en consent.ts y afirmadas por 8 tests, porque son justo lo que un refactor rompe en silencio mientras todo sigue pintando bien. Desviación de la ficha, y a favor: UNA llamada requerida `onDecision(consent, via)` en vez de `onAcceptAll` + `onRejectAll`. Con dos callbacks el consumidor podía pasar un no-op a «rechazar»; aquí el camino de rechazo lo renderiza el block y el API no expone knob para degradarlo. Medido: las dos acciones tienen idéntico relleno, tinta, borde, tamaño, peso y alto — 5.96:1 en claro, 8.79:1 en oscuro. Tres cosas se arreglaron por mirar, no por deducir. La barra era un `Card` outline y midió 1.00:1 contra la página: se disolvía en ella, así que va al plano `overlay` del sistema, el mismo que estampa cada superficie flotante del canon. A 375 las tres acciones envolvían con «rechazar» en una fila y «aceptar» en la siguiente, leyéndose como dos botones ajenos justo donde la pareja más importa: ahora son un `Group`, y el markup lo dice. Y el panel devolvía el foco a un nodo muerto — se devuelve aquí, un frame después de que la trampa se desarme, con un pestillo para que el asentamiento del montaje no le robe el foco a nadie al cargar. Tres gaps de canon salieron al componerlo, todos en el README: `Button` acepta `ref` y no lo reenvía (forma de A-94), `Switch` no tiene parte de etiqueta, y el `Dialog` restaura el foco a un nodo muerto cuando su disparador se desmontó. El instrumento mintió antes que el código, otra vez: el pane del navegador no compone frames, así que la animación de salida no termina y un diálogo cerrado sigue pintado — el foco medido allí era ficción. Y `fillStyle` no normaliza oklch, así que todo color leído del estilo computado se volvió negro y dio 1.11:1 sobre texto perfectamente legible. Las cifras de esta ficha se miden por píxel, en Chrome real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
2 months ago |
|
|
313e4d52fa |
blocks(article-grid): la banda editorial, y el elemento que el canon no podía dar
Lo que las referencias llaman blog. Sus dos preguntas de fase cero quedan resueltas, y ninguna necesitó tocar el canon. El nombre. Blog nombra una sección entera de un sitio, y esto es la fila de artículos recientes dentro de otra página; el tier nombra por función de página y el elemento que renderiza es un article, así que article-grid hace coincidir nombre, función y markup. La palabra blog sobrevive donde un consumidor la busca: la primera línea del readme y la etiqueta del catálogo. El artículo semántico. Un teaser tiene que ser un article o se lee como una caja de texto suelto, pero la tarjeta del canon es un div fijo y eso parecía obligar a elegir entre añadirle un eje de elemento o declarar un hueco. Construirlo enseñó la tercera vía, que no cuesta nada: el bloque renderiza el article y compone la tarjeta dentro. El tier lleva desde su primer bloque renderizando sus propios elementos de seccionado, así que el elemento es del bloque y la superficie sigue siendo del canon. La regla amuralla al primitivo, no a la composición. Y tres decisiones más que sí son de diseño. La tarjeta entera no es un enlace, porque el teaser lleva además un chip y una firma y anidar interactivos dentro de un enlace es marcado inválido: es justo donde acaban las referencias que hacen clicable toda la tarjeta, así que el enlace vive en el título y hay exactamente uno por teaser. El realce al puntero viene encendido, al revés que en precios, porque un teaser sí es algo a lo que se va. Y el bloque no formatea fechas: las compone la app con el componente del canon. Dos errores míos en la tanda, los dos cazados mirando la captura y no la sonda. El ancho mínimo por defecto sólo cabía dos veces en la medida, cuando las referencias lideran con tres columnas; medido y corregido, ahora da tres, dos y una según el ancho. Y el segundo intento de arreglarlo no llegó a aplicarse sin que me diera cuenta, porque lancé el reemplazo sin aserción y el formateador había juntado las props en una línea: culpé a la caché del servidor antes de mirar el fichero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
b7474e499d |
blocks(logo-cloud): la banda de marcas vuelve, y su parte difícil no era la disposición
Se difirió en F2 sobre una premisa que las referencias desmienten —que su versión honesta pedía un marquee o quedaba en un wrap trivial—: Tailwind Plus, Untitled y Flowbite lo shippean estático. Una decisión cuya premisa cae se corrige. Pero el argumento para construirlo no es la paridad. Lo difícil de un muro de logos es la tinta: veinte marcas en veinte paletas gritan sobre la página y entre ellas. Ese tratamiento acaba de entrar en la fundación como contexto, así que el bloque queda fino y es exactamente la forma correcta — cuando lo difícil es un tratamiento, el tratamiento va a la fundación y el bloque sólo lo estampa. Aquí no hay una línea de CSS, que es además lo que el contrato exige. Dos decisiones medidas, no elegidas. Las marcas van en un wrap y no en una rejilla, porque llegan con proporciones dispares y unas pistas iguales dejan huecos irregulares alrededor de las estrechas. Y la parte del ítem se gana su sitio defaulteando el eje de bloque, que es lo único que un logo nunca trae consigo: un vectorial carga su viewBox, un raster sus píxeles, y uno al lado del otro aterrizan a cinco alturas distintas. Verificado con ocho marcas de anchos y colores deliberadamente dispares: las ocho a una sola altura conservando sesenta y cuatro píxeles de diferencia de ancho, que es cada una guardando su proporción. El marquee se queda como hueco con disparador y con su forma decidida: si algún día entra será un preset de movimiento sobre la fila, no un bloque nuevo, y con la preferencia de movimiento reducido parándolo. Y el hueco de la utilidad para nombres sólo audibles sale aquí por tercera vez en la ola. Tres consumidores reales lo piden ya. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
00489be263 |
blocks(tramo B): las cinco primeras matrices de equivalencia
El entregable central de la ola, y el que responde a la pregunta que la abrió: no competir en número de dumps, sino demostrar variante a variante qué composición nuestra logra cada una de las suyas — y sacar a la luz, con nombre, lo que no alcanzamos. Cada block gana una sección de equivalencias con cinco columnas: la variante de la referencia, quién la shippea, la receta exacta, dónde se ha visto, y su estado. La regla que gobierna la tabla es que una receta que no se ha visto en el navegador no entra: la columna de demostración es la prueba, no la promesa. Y la primera tabla ya justificó el tramo. El hero declaraba desde julio, en sus gaps, que el mockup de móvil estaba HECHO — y la demo sólo había enseñado nunca el cromo de navegador. El primitivo ofrece tres, así que se añadió el control, se midió el cromo de teléfono a trescientos veinte y sólo entonces se escribió la fila. Una receta declarada no es una receta demostrada, que es exactamente lo que este tramo existe para separar. Van cinco de quince: hero, pricing, cta, testimonials y faq, que son los que esta sesión ha trabajado y cuyas recetas están medidas. Entre las filas quedan nueve gaps con nombre —desde la elevación estática que Card no tiene hasta el carrusel de citas— y dos «no se ofrece» con su motivo: un carrusel en la apertura es movimiento automático sin control de pausa, y filtrar preguntas es estado de datos del app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e7677d6d87 |
blocks(pricing): la tabla de comparación, y las palabras que una marca no dice
La brecha recurrente del dossier en pricing, y la última fila del primer tramo de la ola. Es una PARTE y no un hermano porque tiene que obedecer al periodo de la sección, y sólo una parte puede leer ese contexto: B-10 le prohíbe a un hermano importar el block que lo posee. Vivir dentro de la sección es además lo que permite que la app meta el precio en una celda y cambie con el conmutador, sin que esta página cablee nada. La app construye la tabla y el block la compone. B-5 admite datos justo donde el canon compuesto ya es data-driven, y la tabla del canon lo es: exige la instancia que devuelve createTable. Así que la parte no inventa un formato de matriz propio ni traduce nada de ida y vuelta, que es el acoplamiento que esa regla existe para evitar. Las tarjetas y la tabla conviven, como en las referencias, alimentadas por el mismo array de planes. Dos cosas que el plan había esbozado y que construirlas demostró equivocadas, y quedan escritas en vez de calladas. No hay un Record de ejes visuales por plan: acentuar una columna es componer canon, que es justo lo que ya permite el snippet de cabecera, y ese Record habría sido una segunda fuente de lo que la app declara en su plan. Y el block no fija la columna: el motor es del app, y meter mano ahí sería el block decidiendo cómo se comporta la tabla de otro. Lo que sí posee es lo que la app no puede: el nombre accesible de la tabla y las palabras que una marca de visto o de guion no dice en voz alta. Ese glifo lleva todo el significado de la celda y es silencio para un lector de pantalla, así que va oculto y la frase viaja en su etiqueta, desde un mapa exhaustivo con respaldo en inglés y su test en las dos direcciones. Y absorbe el cast del canon siendo genérico: la tabla tipa su ranura sin parámetro mientras el motor sí lo devuelve, así que todo consumidor castea, empezando por la demo del propio componente. Aquí se castea una vez a la entrada y se deshace a la salida, para que el snippet de celda devuelva la fila con la forma del app. Medido a 1280 y 375: ocho filas por cuatro columnas, tabla nombrada, marcas que se anuncian, y en móvil la tabla desplaza con la columna de características quieta y la última columna alcanzable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f4ce006423 |
blocks(faq): la lista estática, y un tipo que deja de prometer lo que no da
La brecha del dossier para este block, y la mayoría del formato: seis de siete en Tailwind Plus no son acordeón, sino una rejilla de preguntas y respuestas siempre abiertas. El eje vive en la LISTA, no en la raíz. Primero porque la lista es lo único que cambia —la cabecera se lee igual—, y segundo porque esa colocación es la que permite que el tipo diga la verdad: pasa a ser una unión discriminada. Bajo acordeón atraviesa toda la superficie del Accordion del canon; bajo lista no se ofrece ninguna de esas props, porque una rejilla estática no tiene estado abierto que enlazar, nada que colapsar ni modo simple o múltiple. Ofrecer una superficie y descartarla en silencio es justo lo que A-94 dejó dicho que no se hace, y aquí es donde iba a morder después. El ítem lee la disposición del contexto: no puede discriminar sobre la unión de la lista sin obligar a la app a repetir el arreglo en cada pregunta. Su value y su disabled quedan documentados como del acordeón, la misma convención que el fondo del hero y la media del cta. Y bajo lista la pregunta es un encabezado de verdad, porque ahí ES el encabezado de su respuesta; bajo acordeón el canon la mete dentro del disparador y esa semántica es suya, así que el block no inventa una segunda. Medido a 1280: en acordeón, cinco disparadores, ningún encabezado, medida de setecientos sesenta y ocho y respuestas ocultas hasta pulsar; en lista, ningún disparador, cinco encabezados, tres pistas de trescientos nueve, medida de mil veinticuatro y todas las respuestas visibles sin tocar nada. A 375 cae a una columna conservando ambas cosas. El acordeón queda idéntico. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f3bfcf2dd9 |
blocks(testimonials): una cita sola no va en una tarjeta
La brecha que el dossier le apuntaba a este block: la cita única es la variante modal de todas las referencias y el grid la minoría, dos de ocho en Tailwind Plus. Entra como disposición, no como hermano. La anatomía no cambia —cita, autor, avatar—, sólo cómo se colocan, y el precedente es el fondo del hero, que también redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide rotación o carrusel, que es otro componente. Viaja por contexto de configuración, el patrón del align de feature-grid y team, no la coordinación de pricing. Y viaja porque el spotlight no es una piel sobre las mismas cajas: la rejilla deja de serlo y de estampar el ritmo estructural —que es para una FILA, no para una cita—, el ítem pierde la tarjeta, la cita sube de tamaño y se centra, y el autor pasa debajo, donde ya no hay una hilera de caras que alinear. Pedirle a la app que repita esa decisión en cinco partes es el agujero que A-84 cerró en feature-grid. Enmarcar una cita sola la hace parecer un ítem de una lista que no está ahí, así que ahí no hay tarjeta y el tipo lo dice, en vez de aceptar una superficie que luego descarta. La cita se centra con un párrafo de verdad: sobre una caja en línea el centrado no aplica, y sería mentira. Slot nuevo para la marca de quien firma, en las dos disposiciones: es lo que hace que una cita se lea como evidencia y no como una opinión, y es la pieza que ponen todas las referencias. Medido: en grid, cinco tarjetas, veinte píxeles, medida de mil veinticuatro y ritmo escalonado; en spotlight, cero tarjetas, una cita de veintiocho centrada en párrafo, medida de setecientos sesenta y ocho, autor centrado y sin escalonado; a 375 la cita cae a veinticuatro. El grid queda idéntico a antes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
0646d5ebde |
blocks(pricing): el plan dice dónde estás, y el que no puedes elegir dice por qué
Firma del usuario: faltaba la decoración del elegido, y faltaba poder marcar un plan como no disponible — si estoy cambiando de plan, el que ya tengo no debería poder seleccionarse. Lo segundo cambia la posición doctrinal del block. Su README decía desde julio que nada en una tabla de precios puede bloquearse y que por tanto no hay nada que explicar. Eso vale mientras la sección es sólo una comparación; se cae en cuanto el visitante ya está en un plan. Y ahí manda lo que costó contact: una acción bloqueada debe su frase, o es un agujero — algo que no se puede hacer y nada en pantalla diciendo por qué. El plan gana un estado: disponible, actual o elegido. Un enum y no tres booleanos, porque tres booleanos admiten lo que no existe, como elegido y no disponible a la vez. Y ortogonal a featured, que es la recomendación de quien vende: el plan recomendado puede ser justamente el que ya tienes. Se deriva una sola vez en el plan y viaja por contexto. La acción se lo entrega al botón del app por parámetro de su snippet, así que nadie vuelve a deducir en el punto de uso si se puede pulsar, y las palabras del botón siguen siendo del app. La frase, en cambio, es del block: idlangref con respaldo en inglés dentro de un Record exhaustivo, el mismo mapa que bloquea la acción es el que escribe el motivo, así que un estado que bloquea en silencio no se puede ni expresar. Su test lo afirma en las dos direcciones. Y una parte nueva la dice y posee el id al que apunta el aria-describedby del botón deshabilitado, sin renderizar nada cuando no hay nada que decir. La decoración la pone el canon: el elegido reenvía el anillo de Card, el actual su tratamiento deshabilitado. Nada de un data-pricing-selected propio: sería un atributo que nadie lee, porque un block no pinta CSS. Medido de punta a punta en el escenario de cambio de plan: el actual sale con cursor de no permitido, botón deshabilitado y aria-describedby que casa con el id de la frase, traducida por el arnés; al elegir otro, su tarjeta gana el anillo de dos píxeles con el primary de su paleta y las demás no. Queda anotado un gap del canon que salió al medir esto y que no toco: Card disabled nunca atenúa. La regla existe y su cursor sí aplica, pero la animación de montaje fija la opacidad en uno con prioridad de animación y se come la del estado; aislado con no-emerge, atenúa. Es el patrón que la doctrina de motion llama frágil-conocido —dos dueños peleándose por una propiedad en un nodo— y alcanza a toda Card deshabilitada del ecosistema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
db026e48f5 |
blocks(pricing): la tarjeta responde al puntero, y el guard deja de leer prosa
El plan reenvía el eje nuevo de Card. No es el `interactive` del canon: eso convertiría la tarjeta en un botón y `.PlanAction` ya tiene uno dentro. Apagado por defecto, porque una fila de planes es una comparación y una tarjeta que responde al puntero sugiere que la tarjeta misma es el control; se enciende donde la tarjeta entera ES la elección. Control vivo en la demo y en la URL de la vista previa. Y al escribir el comentario que explica por qué NO se usa un nativo aquí, el guard del tier se puso rojo sobre mi propia prosa: la regla B-2 leía comentarios como código. Su barrido quitaba sólo los de UNA línea, mientras el que entiende todas las formas y conserva los saltos ya existía tres líneas más abajo, usándose sólo para los imports. Es la lección de la fase cinco aplicada a una regla y no a la otra. Arreglado con dos fixtures nuevos —un nativo nombrado dentro de un comentario multilínea es prosa; un nativo de verdad debajo de ese mismo comentario sigue siendo error— y probado en rojo sobre el árbol real: un button inyectado en el hero sale señalado con su línea exacta, y el árbol queda restaurado después. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
44e4c9328e |
blocks(hero): un vídeo en un hero es paisaje, y que se repita lo decide quien lo pone
Corrección del usuario, y tenía razón: la demo componía el reproductor completo con su barra de controles, y nadie enseña un botón de play en una sección de apertura. Un vídeo de hero es decorativo — mudo, en línea, arrancando solo y sin controles. Y si se repite o no es una decisión, no algo horneado: en bucle el movimiento no termina nunca, así que sale como control vivo propio. Medido: cero botones, sin nodo de controles, y el loop siguiendo al conmutador. De paso queda registrado lo que aquello escondía. La barra que había se pintaba directamente sobre el fotograma sin fondo propio, así que en un clip claro eran siete botones a opacidad uno que nadie podía ver; es del canon y media-player está en la lista de intocables de este plan, así que se documenta por si algún día vuelve. Y un hallazgo más gordo, medido al cablear lo anterior: la preferencia de movimiento no llega a prefs en este arranque. Un vídeo que se repite sin barra de controles no se puede pausar, así que su autoplay tiene que ceder ante la preferencia declarada del visitante, igual que ya hacen las entradas de Motion del propio block. La puerta está escrita en app-land y hoy es inerte: con prefers-reduced-motion emulado en reduce, el valor efectivo sigue resolviendo allow, la raíz sigue estampando data-motion allow y el clip sigue corriendo. Queda puesta —empezará a funcionar el día que el eje lo haga— y anotada como lo que es: un problema de WCAG 2.2.2 a nivel de framework, no un detalle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
fd9df269ef |
blocks(hero): un slot apagado se pasa vacío, no se renderiza a cero
El usuario dijo que en la demo hay propiedades que no hacen nada. Medí los diez
controles uno a uno: nueve tienen efecto comprobable y uno funcionaba a medias.
Apagar el eyebrow quitaba el contenido y dejaba el hueco: el stack de copia
seguía con seis hijos y el primero medía cero de alto, comiéndose los veinte
píxeles del gap.
La causa era de la demo, no del block, y contra una regla que el propio handoff
del tier tiene escrita: el snippet se declaraba con un {#if} DENTRO, así que
siempre llegaba, el {#if eyebrow} del block seguía siendo cierto y sobrevivían el
envoltorio de Motion y su parte del ritmo con nada dentro. Ahora el contenido se
declara fuera y se pasa como undefined cuando no toca — lo mismo para la media,
el formulario y el aviso de confirmación.
Medido: cuatro hijos con el chip puesto, tres sin él, y ningún hijo de altura
cero en ninguna combinación.
Queda registrado además el defecto que el usuario vio en el vídeo del hero: los
controles del MediaPlayer están —siete botones, todos con caja, opacidad uno—
pero se pintan sin fondo propio sobre el fotograma, así que en un clip claro son
ilegibles y se leen como ausentes. Es del canon, y media-player está en la lista
de intocables de este plan, así que se documenta sin tocarlo. Con la nota de
método: contar nodos y leer la opacidad no dice nada de la legibilidad sobre
contenido en movimiento.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
a514ef929d |
blocks(cta): el panel gana una segunda columna, y las acciones no se sueltan de su frase
Tercera fila de la ola F2b. El README lo tenía diferido con la razón escrita: recurre en las referencias, es un tercer arreglo y el Mockup del canon ya existe para la media. Entra ahora porque la demo lo pide. Dos decisiones que la ficha del plan no traía y que salieron al colocarlo. La copia conserva sus acciones en la MISMA columna: separarlas dejaría el botón debajo de la captura, lejos de la frase que lo pide, y una llamada a la acción que no toca a su argumento pierde la mitad de su fuerza. Y split sin media cae a justified, no a una rejilla con una pista vacía — la misma regla que ya sigue el split del hero, porque un layout con un hueco reservado miente sobre lo que tiene. La media es del app, como en el hero: el block no nombra cromo propio. La demo compone el Mockup del canon con un panel falso dentro. Medido: a 1280, dos pistas de 436px en el panel de 992; a 375 una sola pista con el CTA POR ENCIMA de la media al apilar, que es el orden del argumento; idéntico en claro y oscuro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
fd6c55a13b |
blocks(pricing): un plan solo no se sostenía — la fila necesitaba un tope
Segunda fila de la ola F2b, y la que existía para comprobar una sospecha. El README llevaba desde julio diciendo que el plan único «ya lo cubre el layout», diferido por eso. La ficha exigía medirlo antes de darlo por gratis, y no lo cubría. La causa está en la lista de pistas, no en cuántos planes hay: la fila es repeat(auto-fill, minmax(17rem, 1fr)), y auto-fill RESERVA toda pista que quepa, la ocupe alguien o no. Medido a 1280 sobre 992px de fila, un plan solo daba una tarjeta de 315px pegada al borde inicial con 677px de vacío al lado; fijar columns a 1 solo cambiaba eso por una tarjeta de 992px, que es un cartel. Meterlo en un Container sm tampoco: a 640px vuelven a caber dos pistas. Lo que faltaba era un tope sobre la FILA, y la parte no lo dejaba pasar: sus props están cerradas a propósito desde A-94, porque el envoltorio esparce el resto antes de las props que fija y un width del consumidor tipaba y se descartaba en silencio. Así que se abre nombrando: maxWidth entra en el tipo y viaja explícito al AutoGrid, que es exactamente lo que A-94 dejó dicho — el tipo dice lo que la parte honra. El marginX auto va con él sin condición: sin tope la fila ya llena la medida y auto no resuelve a nada. El número lo pone la app. Un block que horneara uno estaría decidiendo cuánto mide un plan; la demo pasa 28rem para uno y 44rem para dos, que es lo que las referencias le dan a cada arreglo, y gana un control vivo de recuento que también viaja en la URL de la vista previa. Medido después: un plan 448px centrado, dos planes 704px en dos pistas de 340 centradas, y tres planes 992px idénticos a antes — el tope es opt-in y no toca el caso que ya existía. A 375 es inerte, porque allí una sola pista ya toma el ancho. Queda anotada la lección en el README y en el handoff, porque no es de este block: una fila «diferida» puede ser una medición equivocada y no un aplazamiento. Nadie había mirado pricing con menos de tres planes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f11b198f56 |
blocks(hero): el alta por correo entra por una ranura, y el block no toca el formulario
Primera fila de la ola F2b. El hueco lo registraba el dossier desde julio y su README lo tenía esperando firma: las referencias ponen un alta por correo en el hero cuando la llamada a la acción ES un correo. La forma se eligió contra el precedente, no de cero. Newsletter recibe el handle del formulario porque lee la validez de la sección y con ella decide qué se puede pulsar; eso es coordinar, y un block que coordina tiene que poseer una máquina y las palabras de sus estados. Un hero no coordina nada, así que toma la otra puerta —la de site-footer.signup—: UNA ranura, la app compone Form + Field + Form.Submit enteros, el block no ve el handle y sigue sin poseer nada. Aterriza donde aterriza la llamada a la acción: dentro del cúmulo de copia, tras la descripción y antes de actions, así que hereda el ritmo estructural del stagger y su propia entrada. Y con medida propia: una fila de correo tan ancha como la prosa es inusable. El cap es sm y por ser un máximo resulta inerte en split, donde la columna ya es más estrecha — 480px en center contra una sección de 1264, 472px en split, que ES la columna. Los dos slots conviven si llegan los dos. En las refs el formulario sustituye al par de botones y así lo enseña la demo, pero hacerlos excluyentes en el tipo prohibiría un arreglo real: un alta junto a un «hablar con ventas». Medido en Playwright headless sobre píxeles pintados: tinta escrita 15,88:1 en claro y 16,28:1 en oscuro; el relleno del campo contra el lienzo de background 9,61:1; la fila colapsa a 375 y comparte a 1280; Enter llega al handler del app y un correo inválido lo para con el error traducido y aria-invalid; RTL espeja la fila. Dos gaps de canon quedan registrados, no tapados: Form.Submit no reenvía al Button ni el eje intent ni ninguno de anchura, así que el botón no llena su celda al colapsar —newsletter mide lo mismo y el justify de Grid es justify-content, no justify-items—, y la demo usa lo único que hay, el style que sí viaja. Y el contexto de inversión mordió por segunda vez: un Text sin color explícito bajo background mide 1,62:1 contra 10:1 con on-solid. Queda anotado también cómo se mide esto, porque dos sondas dieron cifras seguras y falsas antes de la buena: un parser rgb sobre un sistema que emite oklch, y un canvas cuyo fillStyle tampoco lo normaliza aquí, midiendo NEGRO contra todo. La señal de que la sonda es honesta es que la etiqueta del submit sale a 5,18:1, la cifra que este repositorio ya documenta para primary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4caf1100a1 |
refactor(blocks)!: el eje de tamano dice de QUIEN es — size deja de significar dos cosas
LA AMBIGUEDAD, medida. En el canon `size` no es ambiguo: es «el eje de tamano de
ESTE componente», y cada uno lo materializa a su manera — padding de bloque en
`Section`, densidad de la tira en `Banner`, escala tipografica en `Text`. La
ambiguedad la creaba el tier: un block no es ninguno de esos componentes, y al
reenviar `size` estaba reenviando el eje de una pieza INTERNA. Desde fuera:
<FeatureGrid size="xl"> cambia el AIRE de la seccion
<SiteBanner size="lg"> cambia la ALTURA de la tira
Misma prop, dos cosas, y una con un valor menos en la escala (`BannerSize` no
tiene `xl`). Un consumidor que aprende el eje en trece blocks se equivoca en el
catorceavo.
EL RENOMBRADO:
- `size` -> `sectionSize` en los 13 que envuelven `Section`
- `container` -> `containerSize` en los 14 que envuelven `Container`
- `size` queda LIBRE y solo lo usa `banner`, donde si es el tamano del block
- `minColumnWidth` -> `minChildWidth` en `site-footer`: el mismo concepto tenia
dos nombres en el tier, y ninguno era el del canon
El patron no se invento: el tier YA nombraba un eje por su pieza (`container`).
`containerSize` + `sectionSize` lo hace explicito y simetrico.
LA REGLA, en `architecture/blocks.md` §Conventions, para que no reaparezca: un
block que reenvia el eje de tamano de una pieza interna lo nombra `{pieza}Size`,
y `size` se reserva para el tamano del block. Corolario: un eje que el canon ya
nombra se reenvia CON SU NOMBRE.
METODO — se renombraron los TIPOS primero y se dejo que `svelte-check` senalara
cada consumidor, en vez de buscar a mano. Cazo los cinco call sites que quedaban
(`CtaSite`, `HeroSite`, `SiteHeaderSite`, `TeamSite` y un uso suelto en
`cta.svelte`). ⚠️ Y cazo tambien un error propio: el primer patron era demasiado
ancho y renombro `size` en SIETE sub-partes donde ese `size` es el del `Card` o
el `Text` que envuelven (`testimonials-item`, `contact-reason`,
`team-member-role`…). Revertidas antes de seguir.
DOCUMENTACION revisada entera: corregidas las tres filas de README que describian
props del block con el nombre viejo (`hero` x2, `site-header`); conservadas las
seis que hablan del `size` de un componente del canon (`Banner`, `Accordion`,
`Heading`), que no cambia. `PLAN-blocks.md` se deja intacto: es bitacora, y
reescribir el registro historico para que cuadre con el presente lo falsearia.
LEDGER — el contraste doctrinal llega a 15/15, y cinco filas nuevas:
- A-95 `banner`: con `affix="top"` el aviso tapa la cabecera pegada (medido:
`elementFromPoint` sobre el header devuelve la tira). NO se arregla aqui: la
pieza que falta es la que POSEE las alturas de pagina — Mantine lo resuelve en
`AppShell`, que declara `header={{ height }}` y desplaza el resto; nuestro
equivalente es el block `app-shell`, sin construir.
- A-96 `banner`: `affixOffset` es prop publica sin control vivo en la demo.
- A-97 `feature-grid`: RETIRADA el mismo dia. Se midio el texto `muted` con el
4,5:1 generico de WCAG y el proyecto tiene OTRA vara — «floors APCA >= 60 ∧
WCAG >= 3», con guard propio, y la tinta secundaria es «marginal sub-4.5:1 by
design». Re-medido con `src/arts/color/apca.ts`: Lc 63,7 y 3,70:1, cumple las
dos. (El primer intento con `apcaLc` devolvio 102666 porque le pase 0..255
donde pide 0..1: un valor fuera de rango no es un hallazgo, es un formato.)
- A-98 `hero` + A-99 `Group`: las acciones no apilan ni envuelven. La causa NO es
del tier — `Group` no aplica el `wrap` que su README promete en TRES sitios:
`group.svelte` nunca se lo pasa a `Flex`, el recipe no lo declara, y
`Omit<FlexProps,'wrap'>` impide compensarlo desde fuera. El `align: center` si
se cumple, lo que descarta que el recipe no cargue.
Y A-25 cae (la demo ya no promete elevacion) y A-47 corrige su disposicion: el
anillo de foco es «a config axis» por doctrina — endurecerlo es una decision de
VALOR en `color.focus.ring`, «never a per-component CSS change», que es
exactamente lo que hacia el intento revertido en `e468e764b`.
Gates: blocks:check verde (15 blocks / 115 ficheros) · svelte-check 72 errores /
59 avisos = linea base exacta, ninguno en blocks · docs:check 0/0 sobre 629 docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
68a48a4401 |
refactor(skin-media-player): un cableado y cuatro pinturas, con el movimiento del sistema
Reescritura completa de la versión A (`bf12b8a57`), que el autor valoró como
duplicación. La medición le daba la razón: `fit()` idéntico en dos ficheros,
`hold()` y `stop()` en otros dos, y `MediaPlayerProvider.require()` en los
cuatro. Eran cuatro componentes compartiendo carpeta, no un skin.
## La forma
Un componente cablea, los aparatos sólo pintan.
- `skin-media-player.svelte` — EL punto de cableado: un `require()`, una
derivación de todo, y las teclas generadas recorriendo un mapa.
- `controls.ts` — datos por aparato: lienzo, teclas `[{id,x,y,w,h,action}]` y la
ranura del volumen.
- `actions.ts` — el vocabulario único (`play·pause·toggle·stop·seek-*·rpm·none`),
con `stop()` y el veto por `preventDefault()` en un solo sitio.
- `type-fit.svelte.ts` — el ajuste de `<text>`, una vez.
- Los cuatro cuerpos: props entran, SVG sale. Cero provider, cero `$effect`,
cero contexto. Añadir un quinto aparato es un SVG y una fila de datos.
`<SkinMediaPlayer>` deja de montar player: es una PIEZA dentro de uno. No monta
`Media`, no monta barra y no decide la forma — `inline/bar/row/card` es el eje
`variant` de `AudioPlayer`, y la piel viaja dentro como hija.
## Movimiento: el del sistema, nada a mano
La versión A traía tres `@keyframes` locales y siete transiciones con números
crudos, ignorando `docs/theming/motion.md` (leída ahora, entera).
- Todo giro continuo es el preset **`spin`**; el respirar de la cinta, **`pulse`**.
El periodo va en la ranura del propio loop (`--motion-loop-spin`: 1.333s a 45
rpm, 1.8s a 33).
- Las transiciones consumen `--duration-*` / `--ease-*` por una ranura `--_smp-*`
por eje.
- DEFECTO FUNCIONAL corregido: el generador apaga los loops por
`[data-motion='reduce']` **y** por la media query; el bloque a mano sólo tenía
la segunda, así que la preferencia UIX en `reduce` sin ajuste del SO no paraba
el plato. Medido en navegador: ahora `animation-name: none`.
- Queda un `@keyframes` local, la aguja del vúmetro, anotado `functional:` y con
el periodo en token: ningún preset representa un instrumento analógico.
## Tres defectos que sólo se ven en movimiento
Los tres pasaban toda comprobación de atributos y sólo aparecieron mirando la
imagen moverse:
1. **La sombra rotaba con el disco.** El `feDropShadow` colgaba del grupo que
gira, así que su desplazamiento orbitaba (se leía como la sombra subiendo y
bajando) y la penumbra de la región del filtro barría las marcas del plato,
que parecían parpadear. El filtro pasa a un padre quieto — un disco es un
círculo, su silueta no cambia. Igual en el brazo.
2. **Las marcas del plato salían disparadas en arco.** Al rehacer se perdió la
regla del estrobo; sin `transform-origin` el preset gira alrededor del centro
del viewBox (500, 440) en vez del centro del plato (406, 468.5).
3. **El tamaño no llegaba.** `max-inline-size` sólo encoge, así que `lg` y `xl`
eran inertes en cuanto la columna era más estrecha (medido: md/lg/xl los tres
a 720px). Ahora `inline-size: min(100%, …)`, base por objeto y factor por
tamaño, una sola fuente en el recipe — la base estaba declarada dos veces,
inline y en CSS, y ganaba la inline.
## Volumen
Primero se compuso el `Knob` del catálogo y era inmanejable: mapea el valor al
ángulo ABSOLUTO del puntero alrededor del centro (`Gesture.rotate`), o sea
orbitar el ratón en torno a un dial de 40px. Sustituido por el `VolumeSlider`
del propio player corriendo en una guía pintada por la máquina. El re-tinte va
sobre el MISMO descendiente que retinta el player (`media-player.css:420`) o su
regla gana por estar más cerca del thumb.
La banda inferior de la pletina se reordenó para alojarlo: vúmetro a media
anchura con el dial recentrado (aguja intacta), deslizador a 42 unidades del
bisel (20px a `md`), contador y piloto +20 a la derecha, la etiqueta REPROD del
piloto fuera, y «MODELO CR-77» al mismo ancho que «Levante» por `textLength` +
`lengthAdjust="spacing"`.
## Prop `labels` y la decisión de idioma
`labels` (default `true`) apaga la serigrafía entera de la máquina. Los rótulos
NO se traducen, y es decisión firmada: son parte del OBJETO, como la marca del
badge. Los nombres accesibles sí siguen el idioma, porque salen del morfo de las
partes compuestas.
## Demo canónica
`web/routes/uix/components/skin-media-player/+page.svelte` con la plantilla v2
de 9 pestañas + su entrada en el sidebar. El banco provisional de
`/otros/players` se borra: duplicaba la demo con DOS players independientes, que
es lo que produjo dos lecturas falsas («no está sincronizado», «fondo negro»).
Los cuatro bocetos del autor se quedan en la carpeta, ya sin ruta.
`preload="none"` en la demo a propósito: un preload de metadata abre una
petición de rango que el navegador aborta, y `smoke` cuenta ese aborto como
fallo same-origin.
## Auditoría — el hueco que queda
`component-guide.md` §5 dice que incluso un átomo pasivo es morfo-first, con un
morfo mínimo `scope: ['eidos']` y `## Passive justification`. Este componente no
lo tiene porque el alcance acordado excluía `src/uix/morfo/`, y la consecuencia
no es cosmética: **tres máquinas no lo ven** — `component:audit` enumera por
ficheros de morfo y no le da fila, `eidos-lint` lo salta («no morfo file») y
`morfo:check` no visita su demo. Los guards transversales sí pasan; los
específicos de componente no se han ejecutado sobre él ni una vez.
Pendiente además: `data-perm-step` en la demo (A37), sin el cual `perm:check` la
salta en silencio.
Verificado: `npm run check` sin errores nuevos (base 72/59) · 77 tests de eidos
(recipe-css-contract · component-api-contract · component-visual-attrs · motion)
· `rtl:check` 0/179 · `docs:check` 0/0 · `layer:check` 0 · `smoke` PASS ·
`morfo:check` 159/159. Y en Chrome, con el ratón: transporte en los dos
sentidos, brazo siguiendo el surco, bobinas por conservación de área, contador
de cinta, y el volumen moviendo `audio.volume` de verdad.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
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 |