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 }
1652 Commits (ae7e3462e5067df4344cebf71158b17614be6b5d)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
ae7e3462e5 |
feat(barcode): el ISBN es un PERFIL de entrada sobre EAN-13, no una simbología
Un código de barras de ISBN ES un EAN-13: ISO 2108 mete los libros en el mismo espacio GTIN-13 que todo lo demás bajo los prefijos Bookland 978/979 — por eso el mismo lector lee una novela y una lata de garbanzos. Así que `isbn` NO entra como noveno valor de `symbology`: declararlo haría que `data-symbology` afirmara un estándar que no existe. Entra como perfil de entrada sobre `ean13`, que es la forma que adoptan las implementaciones serias. - **Separadores tolerados** en las simbologías NUMÉRICAS: un ISBN viaja con guiones y un GTIN con espacios. Code 128 y Code 39 quedan fuera a propósito — ahí un guión es dato codificable y quitarlo cambiaría la cadena en silencio. - **ISBN-10 → Bookland**: un valor de 10 caracteres en `ean13` se lee como ISBN-10, se le valida SU dígito de control (pesos 10…1, mod 11, con `X` valiendo 10) y se convierte con prefijo 978 y mod-10 recalculado. La validación previa es la parte que importa: sin ella, un ISBN-10 mal tecleado se convierte en un EAN-13 perfectamente válido que apunta a OTRO libro — el modo de fallo más caro del dominio. Falla con `invalid-check-digit`. - **La hyphenación NO es nuestra** y queda documentada como tal: las posiciones de los guiones no se calculan desde el número, dependen de las tablas de rangos de la International ISBN Agency, que son dato versionado con fecha de caducidad. Meterlas aquí pondría un reloj de mantenimiento dentro de una librería de cero dependencias. Si algún día se pinta la línea `ISBN 978-…` sobre las barras, el string con guiones lo trae el consumidor. Y un defecto que el ISBN destapó, anterior a él: **el nombre accesible mentía**. `aria-label` anunciaba el valor CRUDO mientras el símbolo codificaba otro — pasaba ya con cualquier EAN al que se le añade el dígito de control, y con el ITF-14, y con Code 39 al mayusculizar. Ahora anuncia lo que el símbolo lleva de verdad. - Vectores publicados en los tests: `0-306-40615-2` → `978-0-306-40615-7`, y los dos casos de control `X` (`0-8044-2957-X`, `0-9752298-0-X`). - `D-1.2` estaba ROTO en el commit anterior y no lo vi: prettier parte la unión `type Tab` a 102 caracteres y el audit exige la forma de UNA línea que documenta la guía de demos. Restaurada con `prettier-ignore`; el audit vuelve a PASS con 0 errores. - `F-1.4` se rompió al escribir la sección ISBN: mencionaba `## Gaps` en línea y el regex del audit engancha esa primera aparición. Reescrito. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d2e841c65d |
fix(sound): la auditoría del art, cerrada — niveles por reproducción y opciones que no mienten
Los tres hallazgos que quedaban de `PLAN-sound-engine` §16, firmados y aplicados.
Dos con código, uno con una medición que cambia la doctrina.
**A-5 — `gainScale` es de la reproducción, no del documento.**
`play()` escribía `master × gainScale` en el nodo master, del que cuelgan TODOS
los earcons. Un parámetro transitorio corrompía un mando de política: el nivel
se filtraba entre reproducciones, una nota en vuelo saltaba de volumen, y los
dos consumidores que motivan el motor compartido —la reducción de sema y el
ducking de un reproductor— se pisaban sobre un único valor. Ahora el factor
multiplica el pico de la envolvente de ESA reproducción, y **también la
profundidad del AM**: dejarla absoluta habría hecho que un earcon atenuado
sonara relativamente más áspero, cambiando el timbre en vez del volumen, que es
lo contrario de lo que significa una reducción.
**RC-5 se reescribe, y es legítimo**: un contrato de regresión protege
comportamiento, no defectos. Lo que pineaba —«la ganancia se fija ANTES de que
la síntesis reviente»— sólo importaba porque el valor iba a un nodo compartido.
La cobertura no se pierde, se coloca donde vive cada responsabilidad:
`sound-port.test.ts` ya pineaba que el canal resuelve el NIVEL y lo entrega;
`sound.test.ts` pinea ahora que el master **no se mueve**; y el art estrena un
contexto falso que sí construye el grafo, para ver el pico de la envolvente.
**A-4 — las opciones del canal, imposibles de equivocar.**
`SoundChannelOptions` pasa a unión discriminada: `{ engine, preferences?,
logger? }` **o** la forma de construcción (`audioContextFactory` / `fetcher` /
`dom` / `masterGain` / `timers`). Nunca ambas — un motor llega ya construido, así
que sus opciones de construcción no significan nada a su lado, y hasta hoy se
aceptaban y se tiraban en silencio. La defensa primaria es el tipo (doctrina de
la casa), con un guard `@ts-expect-error` que hace fallar `check` el día que la
unión deje de rechazar la mezcla; el aviso por logger es la red para JS.
`engine.ts` pasa de un spread a tres ramas en orden de precedencia.
Y **`fetchFn` → `fetcher`**, para alinearse con `$perm`: un nombre por concepto.
`audioContextFactory` **se queda** — es el patrón `idFactory` que ya usan
`$logger` y `$bus`, y nombra el tipo exacto que fabrica (`context` a secas
colisiona con el contexto GL de `$scene`).
**A-3 — S-1 aceptado, medido: 2,2 KB.**
El art entero son 6.410 bytes minificados / 2.177 gzip. Y mi propia propuesta
para cerrarlo era falsa: quitar el fallback del canal no saca el art de ningún
bundle, porque `createActiveUix` y `defineEngineSound` lo importan
incondicionalmente. Cerrarlo de verdad exige carga diferida, que choca con que
`prime()` deba ser síncrono dentro del gesto, y volver `uix.sound` perezoso —
cambio de superficie pública por 2,2 KB. Se acepta: el art es servicio de
núcleo, como `motion` y `scene`. Queda corregida la doctrina: S-1 era el más
débil de los cuatro síntomas; los que justificaban la extracción eran S-2 y S-4.
**La voz, declarada como diseño.** La quinta a 1.5×, el ADSR recortado a
15 %/40 %, el contour de ±400 cents y el mapeo del AM son decisiones perceptivas
calibradas contra el vocabulario de sema, no maquinaria. Escrito en el README
del art y en la cabecera de `engine-sound.ts`, con la regla: el día que un
segundo consumidor quiera otra voz, ése es el momento de partir el art en dos
—gobierno de contexto / voz—, no antes.
Verificación: art + sema **17 suites / 204 tests** · `contracts.test.ts` 35/38
(los 3 rojos, ajenos) · `check` en la baseline exacta (73, 0 propios) ·
`docs:check` **0 errores** · los guards de A-5 **vistos fallar** al reintroducir
la escritura al master · navegador `/temas/sema`, 4 ciclos de editar→disparar:
**1 contexto, 16 osciladores, master en 1, cero errores de consola**.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
75017e5515 |
feat(blocks): `team` — las personas, y un contexto que sirve para una sola cosa
F2.12. Compound porque `Member` SE REPITE (el app mapea sobre N), como
`feature-grid`. Pero es el primero del tier cuyo **contexto existe para una sola
cosa**: el `align` de la sección viaja a cada `Member` y a su fila de enlaces, así
que la foto, el nombre, el cargo y los enlaces comparten eje sin que el app repita
la alineación en cada tarjeta. Un `align` explícito en una parte siempre gana.
Esa es la diferencia con `feature-grid`, cuyos ítems solo se repiten y por eso no
llevan contexto: **la regla de forma se aplica parte por parte, no block por
block**.
## Dos decisiones de forma
- **Sin `.MemberAvatar`.** El plan la dibujaba; no tendría nada que añadir sobre
`<Avatar size radius>` y escondería su API (`Image`, `Fallback`, los estados de
carga). El app compone el `Avatar` del canon directamente dentro del `Member`.
Se envuelve lo que el block DEFAULTEA —tipografía, retícula, eje—, no lo que
solo reenvía.
- **El nombre es un `Heading level={3}`** apagado con `size="sm"`: una persona en
una retícula tiene nombre, y las referencias lo marcan igual. El nivel es
estructura del documento; el tamaño, tipografía. Así un lector de pantalla puede
saltar de persona a persona.
Y un detalle que separa cumplir de sobresalir: la fila de enlaces va pegada al
fondo de la tarjeta (`marginTop: auto`). Con biografías de distinto largo las filas
quedaban a alturas distintas y la retícula se leía descuadrada; sin biografías las
tarjetas ya miden lo mismo y la regla no cambia nada.
## Encontrado al verificar
**Un `columns` fijo no colapsa.** A 420px, cuatro columnas dejan celdas de ~90px
con el nombre partido en tres líneas y el avatar desbordando. El default del block
es fluido (`minChildWidth`), así que el fallo era de la demo: ahora pasa
`columns={{ base: 2, md: N }}` — para eso están los props responsivos. Anotado en
el README.
Verificado en navegador, esperando el atributo que pone la runtime del morfo y no
el reloj: 2/3/4 columnas, `align` center/start propagándose por contexto al eje de
cada miembro, biografía sí/no, claro/oscuro/RTL y 420px, seis avatares, doce
botones de icono **con nombre propio por persona** («Escribir a Ada Lovelace», no
«correo»), escalonado estructural con índices 0,1,2…, cero desbordamiento
horizontal y cero errores de página. Gates: `blocks:check` verde (13 blocks) ·
`svelte-check` sin errores propios · `docs:check` 0 · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
c57b66dc92 |
feat(blocks): `banner` — el aviso de arriba, y tres defectos del canon que no existían
F2.11. El block más FINO del tier a propósito: cuando el canon ya tiene la pieza,
el block es colocación y nada más. Reenvía la superficie entera del `Banner` con
`Omit<BannerProps, 'children'>` —sin re-declarar `intent`/`variant`/`size` ni
estrecharlos—, lo mete en columna con `Container` (`width="100%"` +
`paddingX={0}`: a sangre por fuera, en columna por dentro), envuelve la fila con
`Wrap` en vez de aplastarla, y cablea `Banner.Close` solo si llega `onDismiss`.
La visibilidad NO es suya: es la decisión que ya tomó el componente
(«composición, no un booleano `dismissible`»), así que el app envuelve en su
`{#if}`. Un block que guardara ese estado sería una segunda fuente de verdad.
Tampoco emite sema: el canon declara cero eventos para `Banner` a propósito, y un
aviso persistente sin descarte es tan legítimo como uno descartable.
La demo lo enseña **encima del `site-header` de verdad**, con página para
desplazarse. Un aviso dentro de un recuadro no se parece a un aviso.
## El hallazgo que la fase 0 anticipó
`Banner` estampa `role="banner"` DESPUÉS de sus rest props, así que no se puede
relajar a una región normal. Con el `site-header` en la misma página —que es la
única colocación que shipean las referencias— quedan **dos landmarks `banner`**.
Medido en la vista previa: dos. Lo único que puede hacer un app hoy es nombrarlos
con `aria-label`, y eso hace la demo.
## Y una lección de método que costó una sesión
Reporté tres «defectos del canon» —`aria-label` ausente en `Banner.Close`, `type`
desaparecido de todo `Button`, `IconButton` tragándose el `onclick`— y **los tres
eran falsos**. La causa era la misma: medir demasiado pronto.
Los `data-variant` / `data-size` los escribe el componente de eidos al renderizar,
así que están desde el primer frame; `type`, `aria-label` y los handlers los
aplica la **runtime del morfo en un efecto posterior**. A ~1s: `type` ausente en
3 de 3 botones y ningún clic disparando. A ~6s: `type="button"`,
`aria-label="Descartar"` y el descarte funcionando.
La espera válida es un atributo que solo pueda haber puesto la runtime
—`waitForFunction(() => boton.hasAttribute('type'))`—, no que el nodo exista ni
que se vea. La regla queda afinada en `CONTINUE-blocks.md`, que ya avisaba de
esperar la condición y no el reloj: fallé al elegir la condición.
De paso, un aviso de soma que sí era real y era mío: `Tabs.List` sin nombre
accesible en `BlockDemo.svelte`. Afectaba a las 12 demos del tier.
Verificado en navegador: los 4 intents, los 3 tratamientos, `descartable` sí/no
(con `no` el botón deja de renderizarse, no se oculta), claro/oscuro/RTL y 420px,
la tira siempre por encima de la cabecera, cero desbordamiento horizontal, el
descarte con la página recolocándose, y los controles de la demo moviendo la
vista previa en línea. Gates: `blocks:check` verde (12 blocks) · `svelte-check`
sin errores propios · `docs:check` sin errores míos · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
cf37aee002 |
docs(sound): el aviso de segundo contexto dice lo que hace, no lo que promete
A-2 de la auditoría (`PLAN-sound-engine` §16). El contador de contextos vivos sólo ve **los que crea el art**, así que un `new AudioContext()` en cualquier otro sitio le es invisible: detecta cableado doble de ENGINES, no el antipatrón del contexto crudo que su propio mensaje describe. Ese hueco es exactamente lo que dejó al estudio de sema abrir un segundo contexto en silencio hasta ayer (A-1) — y lo que hizo inútil el plan del handoff anterior, que era «abre la página y mira el warn». Cerrarlo de verdad exigiría enganchar el constructor global, es decir parchear la plataforma. El art no lo va a hacer. Así que se escribe donde prometía de más, en `consts.ts` y en el README, y se apoya en la salida real: los consumidores que necesitaban su propio contexto ahora tienen puertas —`decode` para las muestras, `context` para el grafo— que no les obligan a abrirlo. Sin cambios de comportamiento. Art 13/13 · `check` en la baseline exacta (73). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
08ee18a8e1 |
fix(sound): `decode()` — la puerta que faltaba, y el estudio deja de abrir su contexto
A-1 de la auditoría (`PLAN-sound-engine` §16): `analyze.ts` hacía `new Ctor()` propio para decodificar el `.wav` importado. Medido inyectando un WAV real por el `input[type=file]`: **1 → 2 contextos**. Transitorio —se cerraba al acabar el decode— pero es exactamente el antipatrón que el art existe para impedir, en la misma página que usé como prueba de la invariante. Y el aviso de «segundo contexto» no lo vio, porque sólo cuenta los contextos que crea el art (A-2). No era un despiste de esa página: **la superficie del art no ofrecía la operación**. `preload(urls)` decodifica y esconde los `AudioBuffer` en una caché privada, así que quien quiere las MUESTRAS —un waveform leyendo picos, un analizador midiendo un fichero— no tenía puerta y se abría la suya. - **`EngineSound.decode(data): Promise<AudioBuffer | null>`**. Segunda puerta, simétrica de `context`: una para quien quiere un GRAFO, otra para quien quiere las MUESTRAS. Las dos llevan al mismo contexto. - **No pasa por `getOrCreateContext()`**: decodificar funciona sobre un contexto suspendido, y `resume()` fuera de un gesto puede quedarse pendiente para siempre — un `decode()` que se espera desde la UI no puede colgar de eso. Se añade `ensureContext()`, crear sin resumir. - **No cachea** (los bytes crudos no tienen clave; la caché por URL es de `preload`) y **devuelve `null` en vez de lanzar**. - **Desviación declarada** de lo que §16.2 proponía: NO se añade el acceso a los buffers cacheados. Hoy no tiene consumidor —`Waveform` no existe— y añadir superficie sin consumidor es justo lo que §16.1 le reprocha al art. - `analyze.ts` toma el motor por puerto estructural; `AnalyzePanel` lo lee de `getActiveUix()`, que es el idioma de la casa, no prop-drilling. MEDIDO en Chrome: importar un WAV crea ahora **1 contexto** en vez de 2, y el earcon posterior reutiliza ése. Y sigue analizando bien: un tono de 440 Hz a 8 kHz, decodificado sobre el contexto compartido a 44,1 kHz, se mide como **441 Hz** — el resampleo no falsea la medida. Guards nuevos en `engine-sound.test.ts`: decodifica sobre el contexto compartido SIN llamar a `resume()`, un segundo `decode` no crea otro contexto, y devuelve `null` con bytes indecodificables o sin contexto. Verificación: art + sema 17 suites / 201 tests · `check` en la baseline exacta (73 errores, 0 propios). Quedan abiertos A-2 (el aviso es ciego a los contextos crudos), A-3 (S-1 no está cerrado), A-4 (opciones del canal tiradas en silencio) y A-5 (`gainScale` en el master compartido) — los tres últimos cambian comportamiento público y esperan decisión. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
15621632be |
fix(color): un solo vocabulario de rol, y la documentación del color reconciliada
Había DOS tipos exportados con el mismo nombre. config-types.ts:39 declara
`ColorRole = HierarchyColorRole | Intent` derivado del const (9 roles, con
tertiary) y existía desde el 2026-05-13; eidos/lib/types.ts:125 lo re-escribió
a mano siete días después con 8 miembros —sin tertiary— y nada lo justifica:
ni el commit (
|
2 months ago |
|
|
c0a7050f54 |
docs(process): registro de lectura — guías + theming leídos, e inventario corregido
Cuarta sesión de lectura del corpus. Leídos completos sobre el original:
component-guide (1568), component-audit (369), demo-authoring (266),
book-deviations (851) y todo theming salvo el changelog (gradient-finish,
guide, notes, channels). 4.247 líneas.
El hallazgo que más cambia el trabajo es sobre el propio registro: el
inventario estaba TRUNCADO. Contaba «30 documentos / 19.000 líneas»; el corpus
que el mapa declara son 59 ficheros / 27.348 líneas. Faltaban dos que el propio
corpus llama vinculantes — guides/demo-authoring.md (LOCKED, parte del «paquete
mínimo» de todo brief) y spec/delegation-contract.md (NORMATIVE, RFC-2119) — y
theming/changelog.md (1879), el mayor sin leer. Inventario ya corregido.
Segundo hallazgo de método: una lectura CADUCA. sema.md creció 1024 -> 1051 el
mismo día que se dio por leído (extracción del motor Web Audio al art $sound).
Comprobar wc -l contra el inventario al arrancar cada sesión.
15 hallazgos nuevos (nº 24-38), todos con doble anclaje: documento + línea sobre
el original, y comando contra el código. Los que más pesan:
- Sema tiene CUATRO canales en el código (AnnounceChannel incluido, cableado en
engine.ts desde
|
2 months ago |
|
|
b27f2345fe |
docs(process): el motor cierra sus fases y abre 5 hallazgos; el player, al gate
`PLAN-sound-engine` pasa a CERRADO (F0…F5) y `PLAN-audio-player` a DESBLOQUEADO. Pero cerrar las fases no es dejar el diseño incuestionado, así que el plan gana una auditoría propia que lo contradice donde toca. - **§15 — la invariante, cerrada y medida.** Incluye lo que la medición DESMINTIÓ: el handoff anterior predecía un aviso por consola en `/temas/sema` que no podía aparecer, porque el aviso cuenta contextos VIVOS y allí sólo había uno. Y **E-8**, el agujero de attach que E-1 dio por cableado sin estarlo. - **§16 — 5 hallazgos ABIERTOS sobre el art**, tres con decisión pendiente: `analyze.ts` sigue abriendo su contexto (medido 1 → 2 al importar un `.wav`); el aviso de segundo contexto es ciego a los contextos crudos; **S-1 NO está cerrado** y §7 afirmaba lo contrario —queda tachado en su sitio—; el canal tira en silencio `masterGain` y compañía cuando le inyectan el motor; y `gainScale`, que es de UNA reproducción, se escribe en el master compartido. - **§16.1 — la pregunta de fondo**: el corte separa módulos, **no conocimiento**. La quinta a 1.5×, el ADSR recortado a 15 %/40 %, el contour de ±400 cents y el mapeo de `roughness` son diseño sonoro calibrado contra el vocabulario de sema, viviendo dentro de lo que el plan llama «maquinaria». El precedente de `$motion` —datos en la capa, mecanismos en el art— no se replica tan de cerca como §3 afirma. Y la superficie no sirve a dos de los tres consumidores que la justificaban: `Waveform` y el analizador necesitan los `AudioBuffer`, y `preload(urls)` los esconde en una caché privada. - **`sema.md` corregido**: «sema no importa el art» era cierto para la INSTANCIA y falso para el grafo de módulos. - **`PLAN-audio-player` con las 5 correcciones de su cabecera incorporadas al cuerpo**, que era la condición para presentar nada: segmentos en la matriz §2 y en el contrato §7.5; **G-2 sube a bloqueante** (buffer, ventana de clip, capítulos); las dos invariantes de §7.6 (`prefs.sound` NUNCA toca el volumen del contenido; el silencio de UI debe alcanzar al `Slider` compuesto). **D-AP.11 escrita contra el art** —su premisa, «no hay motor», era falsa— y **D-AP.12** (segmentos `clip`) añadida; las dos existían como referencia sin fila en §4. **D-AP.7 reescrita** como ducking. - El handoff añade dos reglas que costaron caro: **un guard que no has visto fallar no vale**, y acoplarse al dev server vivo del usuario en vez de levantar otro. D-AP.1…D-AP.12 siguen SIN FIRMAR: el gate es lo primero de F0 y no se escribe una línea de código del player antes. `docs:check` deja 1 error ajeno (`blocks/banner/README.md:15`, «8 roles» por 9). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
be3cbf680e |
fix(sound): la invariante de un solo AudioContext, cerrada en attach y medida
`PLAN-sound-engine` daba por cableada la propiedad del motor «en los dos modos de arranque». En attach no lo estaba: `defineUixServices` registraba `motion` y `scene` junto a `dom` pero NO `sound`, y `defineEngineSemantic` no reenviaba `soundEngine`. Una app en attach con sonido activo tenía el motor privado del canal MÁS `uix.sound` — dos motores, y dos contextos en cuanto el reproductor primase el suyo. `attachActiveUix` DOCUMENTABA el footgun en un comentario en vez de cerrarlo, mientras `contracts.ts` ya prometía `singleContextPerDocument`. - `defineUixServices` declara `sound` junto a `motion` / `scene`, y `defineEngineSemantic` lo toma como `serviceDependencies: ['dom', 'sound']` y lo reenvía. Degrada igual que `dom`: sin el servicio declarado llega `undefined` y el canal vuelve a crear el suyo (construcción directa y tests). - La nota de `attachActiveUix` pasa de describir el footgun a describir el cierre, y deja escrito el único caso que sigue vivo: un esquema de servicios a mano que declare `events` sin `sound`. - Fila `events` de `contracts.ts` con `serviceDependencies`, y el assert DERIVA de la tabla en vez de hardcodearla — que es lo que su propia doctrina pide. - El estudio de sema (`/temas/sema`) enchufa `uix.sound`. Era la única página capaz de ejercitar la inyección y no lo hacía. MEDIDO en Chrome, no deducido. Con el motor privado, cada edición del draft reconstruye el `EngineSemantic`, el `dispose()` del canal cierra el contexto y el siguiente disparo abre otro: 9 ediciones dejaron **10 contextos creados y 9 cerrados**, perdiendo además la caché de samples decodificados en cada una. Con el motor compartido: **1 contexto, 0 cierres**, y 24 osciladores construidos en 6 ciclos — el earcon sigue vivo. Dos guards, y los dos VISTOS FALLAR sin el cableado. Es la lección de la retirada de F2, donde una medición vacua se presentó como prueba: - `active-uix.svelte.test.ts`: el `events` de la app prima el motor de la app (quitando el reenvío: `expected null not to be null`). - `sound-e2e.test.ts`: un motor inyectado sobrevive a 3 reconstrucciones con UN contexto y sin `close()`; sólo lo cierra quien lo creó. Verificación: `client` 130/130 · 842/842 · `server` 262/264 · 3718/3722 — los 3 rojos son ajenos (`aura`, `menubar`, `radio-group`) y el cuarto fue un timeout de `soma-attr-audit` por contención, verde aislado en 3,1 s · `check` en la baseline exacta (73 errores, 0 propios). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
dd7efae9cd |
docs(process): el handoff de blocks, reescrito para entrar en frío mañana
El fichero se había convertido en una pila cronológica: la cabecera seguía
contando el estado de hace tres blocks y los gates decían «8 blocks». Ahora es un
punto de entrada.
- **Estado honesto arriba**: 11 de **15** (el plan fijó 14; `feature-split` entró
como hermano con scope-approval, así que el denominador real es 15), qué queda
con su §, y dónde vive cada cosa (plan, contrato, dossier, deuda, READMEs).
- **F2.11 `banner` con fase 0 en pasos**: leer primero el README del componente
`Banner` que YA existe —si el descarte, la persistencia o el `role` son suyos,
el block los compone, no los reinventa—, contrastar ≥2 catálogos, decidir la
forma, y enseñarlo ENCIMA del header, que es donde vive.
- **La regla de forma del tier** en su propia sección, con los ejemplos ya
shipeados de cada lado (repiten / coordinan / slots).
- **Los 8 hallazgos de canon (F15–F22) en una tabla**, cada uno con su número
medido, en vez de repartidos por cinco párrafos. Más los dos huecos viejos que
siguen abiertos (`flex` que no crece, `Group` que no apila).
- **Tres reglas nuevas de las que costaron sangre**: esperar la CONDICIÓN y no un
timeout (con Form+Field+SIUM la hidratación tarda ~2,5s y un screenshot
temprano fotografía el panel invisible); Playwright headless SÍ vale para foco y
teclado reales, `page.fill()` no; y un slot que se apaga se pasa como
`undefined`, con el aviso de que `{#snippet signup()}` sombrea un prop del mismo
nombre.
- **Gates remedidos, no heredados**: `blocks:check` 11 blocks, `svelte-check` sin
errores propios, y los 3 fallos de `contracts.test.ts` verificados uno a uno
como ajenos (escrituras DOM en soma, data-attrs hardcodeados, claves camelCase
de `aura`) — ninguno apunta a `blocks/`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
b7139cd825 |
docs(process): handoff en frío — el registro deja de exigir leerse entero
El registro había crecido a 450 líneas y una sesión nueva tenía que atravesarlas para saber dónde está y qué hacer. Ahora arranca con «Empieza aquí»: dónde está (21/30 documentos, ≈10.400 L), qué leer y en qué orden, las tres reglas que no se saltan, lo único que espera decisión, y el estado del árbol. Y se retira lo que ya se leía como TODO vivo estando cerrado: - La sección de contaminación queda marcada como histórica — se conserva porque la lección (una edición revertida citada como doctrina) vale más que el incidente, pero deja de pedir una relectura hecha. - Los tres «NO resolver antes de leer §25» de los hallazgos 7, 9 y 12 apuntan al nº 13, que los cierra. Un lector que llegue a ellos ya no cree abierta la pregunta del color. - El hallazgo nº 1 pierde su aviso de RELEER: el párrafo se confirmó sobre el original (`active-uix.md` 61–63). - El inventario dice explícitamente que no queda ningún documento en RELEER. Sin cambios en los hallazgos ni en el material leído. `docs:check` sigue con su único error preexistente (nº 21). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
6920684d09 |
refactor(sound): extraer el motor Web Audio de sema al art `$sound`
`sema/chans/sound.ts` tenía 407 líneas de las que ~255 (≈63%) eran maquinaria Web Audio —ciclo de vida del AudioContext, síntesis, samples, desbloqueo por gesto— mezclada con doctrina perceptiva. Deuda arrastrada, con tres síntomas medidos: el import era estático, así que toda app con sema metía el sintetizador en el bundle aunque el sonido estuviera apagado (que es el default); faltaba ciudadanía que `$scene` ya resuelve; y la costura de inyección (`audioContextFactory`) llevaba ahí sin usar desde el principio. Mismo movimiento que `$motion` hizo desde eidos: el art se lleva el RUNTIME, la capa conserva sus DATOS y su doctrina. - `$sound` / `EngineSound`: UN AudioContext por documento (los navegadores los limitan y el gesto de desbloqueo es por contexto), síntesis de earcon, samples con caché y fallback a síntesis, `autoSuspend` OPT-IN —suspender con la pestaña oculta es correcto para earcons y erróneo para contenido, así que es decisión de quien compone— y aviso cuando un segundo contexto va vivo. Puertos `SoundDom` / `SoundTimers` inyectados; no importa ningún otro art ni nada de `$uix/sema`, que es la prueba objetiva del corte. - `SoundChannel`: 407 → 136 líneas. Solo doctrina: el gate de `prepare`, la política de reducción y el reparto «el canal resuelve el NIVEL, el motor aplica la ganancia». Sema no gana ni un import: recibe el motor por puerto. - `uix.sound` en standalone y attach con `ownsSound` (idioma ya shipped: `ownsMotion` / `ownsScene`), fila `sound` en la tabla ejecutable `contracts.ts`, y `defineEngineSound()` para el camino de app. - Tests nuevos: `engine-sound.test.ts` (11), `sound-port.test.ts` (guard de deriva de tipos + la regla de propiedad) y `sound-e2e.test.ts`, que recorre `emit -> cascada -> canal -> art -> grafo real`: el camino que las 16 suites previas no cubrían porque paraban en canales falsos. Sin `diagnostics.ts` ni `errors.ts`, y es decisión: espejo de `$scene`, aquí todo fallo es degradación documentada, no error de programador. Verificación: 17 suites / 198 tests · `check` en la baseline exacta (73 errores, 0 propios) · `sound.test.ts` verde SIN tocar un solo assert, que era el criterio de que el movimiento fue value-preserving. Planes: `PLAN-sound-engine.md` (completo, con el registro de la revisión adversarial E-1..E-7), `PLAN-audio-player.md` (aparcado tras el análisis del reproductor, con sus correcciones en cabecera) y `CONTINUE-sound-engine.md` (handoff). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
37061a7371 |
docs(process): canon + theming completos — el corpus ya tenía escrita la respuesta
Tercera sesión de lectura: el canon entero (tsc · vocabularies · recipe-contract), lo que faltaba del núcleo (active-app · agent · blocks) y `theming/reference.md` COMPLETA. 21 de 30 documentos leídos sobre el original (≈10.400 L de 19.000). ## La pregunta del color: el corpus la tenía respondida, dos veces y mejor `reference.md` §1.bis se titula «Theming lives in Eidos, not in Morfo (by design)» y se abre con «This is the most frequent architectural question». Existe literalmente para impedir el error que cometió la sesión revertida. Su apartado «What SHOULD enter morfo regarding theming» es inequívoco: un componente que expone `color` como prop DEBE declarar `data-color.values` en su morfo; los attrs puramente visuales (`data-variant`, `data-size`) NO. Y §39 (2026-07-15, la doctrina más reciente) da el criterio mecánico y el porqué: «morfo declares an attr only when its driving prop crosses the soma boundary», con el modo de fallo medido — si declaras en morfo un attr cuyo prop no llega a soma, el runtime emite `undefined` y `mergeProps` pisa el sello del wrapper. La frontera deja de ser convención: es de dónde se resuelve el valor. ## Y el agujero real, ahora seguido extremo a extremo `resolveComponentColor` (`eidos/lib/component-color.ts`) considera canónicos los 9 roles MÁS las 33 escalas donantes = 42 nombres; sólo un color CSS crudo se desvía a `data-color-custom`. El wrapper de switch pasa ese `dataColor` a soma, y el provider lo devuelve verbatim. El morfo declara 6 valores. `<Switch color="teal">` emite `data-color="teal"`, fuera del enum — y no es un caso aislado: avatar 8 · button 8 · card 8 · switch 6 · toggle 6 · dialog 3, contra 42 posibles. NO verificado si salta hoy en CI: `morfo:check` sólo ve lo que las demos renderizan de hecho, y no lo he ejecutado. Ese es justamente el agujero — el guard de «lo declarado se cumple» depende de que una demo elija el valor infractor, en el eje que la decisión de 2026-07-18 abrió a 42. Es el único hallazgo que pide decisión de usuario. ## Dos derivas más - `active-app.md` enumera a mano 12 service factories; hay 15 (faltan `agent`, `motion`, `scene`, en la tabla y en el árbol). Misma enfermedad que el «8 roles» y contra la ley que el propio corpus escribió: catálogo hardcoded → puntero. - `canon/tsc.md` anuncia el scope `event:` sin nota, mientras `reference.md` §13 lo marca «Superseded» y `motion.md` «descartado». El tipo existe, cero recetas lo consumen: las tres frases son ciertas, pero quien construya desde el canon lo leerá como disponible. Nada arreglado (regla 5). Quedan ≈6.200 líneas: las guías, `decisions/` y los RFCs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
d8ff647d7c |
docs(process): los 12 contaminados, releídos — y la pregunta del color, cerrada
Segunda sesión de lectura. Los 12 documentos que el revert dejó marcados
`RELEER` (≈5.000 L) leídos enteros sobre el original, más `theming/reference`
§20–§28, que era donde el propio registro decía que estaba la mitad que
faltaba.
Paso 0, antes de leer nada: comprobar que el árbol no sigue contaminado.
`git diff c39170abb^ HEAD` sobre los docs doctrinales devuelve vacío — HEAD es
byte a byte el estado pre-auditoría. Cuesta un comando y es lo que separa la
doctrina de la autocita; queda escrito como regla 7.
## La pregunta aparcada: `data-color` SÍ va en el morfo
Tres anclajes independientes, ninguno citándose a sí mismo: la tabla §6 de
`active-architecture` lo pone en la casilla de `dom.apply (effect)` — la misma
que `data-state`, no la del wrapper — y `git log -L` traza esa fila a
`d4dfdaffa`, muy anterior a cualquier agente; las reglas E-3.5 / R-3.1 / R-3.3
/ G-1.2 del checklist; y 12 morfos que lo declaran con `v.propRef('color')`.
La conclusión de la sesión revertida era falsa.
Debajo había una pregunta mejor, y esa sigue abierta: el morfo de `switch`
declara `data-color` con 6 valores cerrados, mientras la decisión de 2026-07-18
(§25) manda que `color` acepte el sistema completo — 33 escalas y valor CSS
crudo — con el tipo abierto `ComponentColorProp`. Y `morfo-check.ts:167` falla
cuando un attr con `values` emite algo fuera del conjunto. NO verificado si hoy
salta: pide una demo con valor fuera del enum y `morfo:check` con dev server.
Es lo único de los nueve hallazgos que pide decisión de usuario.
## Lo demás, verificado contra el código, que es lo que no puede haber escrito un agente
- `provider.commitState()` / `emitEvent()`: cero coincidencias en todo el repo.
Los documentan dos docs. La sesión revertida acertaba en el hecho y falló en
el método; entre sí los dos docs no se contradicen — la deriva es doc↔código.
- El guard de tests de provider no existe: `NO_MISSING_PROVIDER_TESTS` sólo
aparece en docs, sin comprobación en `scripts/` ni invariante en
`contracts.test.ts`. Cierra por la puerta buena la pregunta que quedó abierta.
- `clsx` (soma-architecture stale contra soma.md y contra el código), Aura y
`uix.scene` (packs y glossary describen un futuro que ya ocurrió), el
frontmatter de CANON apuntando a un PDF que no está, el entry huérfano de
`package.json`.
- `docs:check` lleva en rojo desde el revert y por el revert: `c39170abb`
arreglaba `callout/README.md:23` y `3097cfcb6` lo deshizo con todo lo demás.
El revert deshizo también arreglos buenos; su diff merece una pasada.
Nada arreglado — la regla 5 lo prohíbe hasta acabar, y quedan ≈11.900 líneas.
Lo no hecho queda enumerado en el propio registro.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
eb13fff409 |
feat(blocks): `site-footer` — el pie de la página, con el selector de idioma vivo
F2.10. Es el block donde la regla de forma del tier se ve mejor: `Column` SE
REPITE (el app mapea sobre N grupos), así que es parte compound; la marca, la
banda de alta, lo social, lo legal y el `extra` no se repiten ni coordinan, así
que son slots. El landmark es un `<footer>` de verdad: `contentinfo` sin pedir
nada.
Las columnas van en `AutoGrid minChildWidth`, **sin un solo breakpoint**: el mismo
código sirve para 2 grupos o para 5, y en móvil caen a dos. La entrada es un
`Motion trigger="viewport"` solo en la región superior y **sin escalonado** — una
línea de copyright que aparece con fundido es teatro, y nadie lee un pie columna
por columna.
## Los dos huecos que nombraba el dossier, cerrados
- **La banda de alta al boletín EN el pie** (3 de 7 en Tailwind Plus) como slot
`signup`, con su propia fila a lo ancho: en la columna de la marca (~230px) la
fila de correo tendría que apilarse. El formulario es del app; este block NO
importa el block `newsletter` (B-10) ni inventa validación.
- **El selector de idioma** como `extra` (el slot libre de D-BLK.6), y está VIVO:
escribe `uix.prefs.setIntent('language', …)`, el eje real del ecosistema.
Verificado — al elegir «English», `document.documentElement.lang` pasa a `en`.
## Tres números que salieron de medir, no de suponer
- El `container` bajó de `xl` a **`lg`**: con `xl` el pie no se alineaba con
ninguna sección de la página compuesta encima.
- La rejilla superior pasó de `1fr 2fr` a **`1fr 3fr`**: con `2fr`, un pie de
cuatro grupos se partía en 3+1.
- El gap entre columnas es **6, no 8**: con 8, cinco grupos no comparten fila al
ancho `lg` (728px justos). Horizontal más apretado que vertical, porque el gap
horizontal es el que decide cuántos grupos caben.
## Dos hallazgos de canon (F21/F22 en `PLAN-blocks-quality.md` §6)
- **Los primitivos de layout no pueden cambiar de elemento.** `Text` y `Heading`
aceptan `as`; `Box` —y por tanto `Stack`, `Flex`, `Grid`, `Group`, `Wrap`,
`Container`, `Section`— renderiza un `<div>` fijo. Consecuencia: una columna de
enlaces no puede ser `<ul>/<li>`, que es como la marcan las referencias. Un
block solo puede elegir entre divs o escribir markup que luego no puede estilar.
- **Un `Select` controlado muestra el VALOR crudo hasta que se abre una vez.**
`getDisplayText()` resuelve contra un registro de etiquetas que llenan los
`Select.Item` al montarse, y con el `Content` en un portal cerrado no hay ninguno
montado: `value=['es']` pintaba «es» en vez de «Español». Rodeado con el `child`
de `Select.Value`.
Verificado en navegador: 2/3/4/5 columnas, banda de alta sí/no, claro/oscuro/RTL
y 420px (2×2 columnas, fila de correo apilada), cero desbordamiento horizontal,
separador `aria-hidden`, los tres `IconButton` con nombre obligatorio, y el
idioma cambiando de verdad. Cero errores de página. Gates: `blocks:check` verde
(11 blocks) · `svelte-check` sin errores propios · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
00118dd9e4 |
docs(process): eidos.md completa — 'eidos-only' es clasificacion valida
`architecture/eidos.md` (1055 lineas) leida completa, limpia. Dos piezas que casi cierran la pregunta aparcada sobre `data-color`: - §Selector-drift defense define **eidos-only** como «the marker is there, but at least one `data-*` is not declared in the morfo. **Valid by convention** (visual tokens like `data-variant`, `data-size` come from the wrapper)». Solo *invalid* es bug. - THM-4 da la disposicion inversa: un attr «declared FOR a visual axis and that nothing anywhere consumes» es deuda, y se «consume it or prune it from the morfo». Juntas apuntan a que un `data-color` del wrapper ausente del morfo es legitimo, y que uno declarado solo por eje visual es podable. Pero falta `theming/reference.md` §25, que `eidos.md` designa canon del color: la pregunta NO se cierra hasta leerla. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
5f01746426 |
docs(process): eidos.md hasta la 750 — media respuesta a la pregunta aparcada
`eidos.md` leida hasta la linea 750 de 1055 (limpia, no la edito la sesion revertida). Lo relevante: existe una categoria SANCIONADA de data-attrs visuales que estampa el WRAPPER y que no van al morfo — linea 489: «The wrapper adds the visual token data-attrs (`data-variant`, `data-size`, `data-block`, `data-icon-only`)». Y la tabla «From morfo» enumera lo que eidos consume del contrato: `data-color` NO aparece. Eso reconcilia a medias la regla de `morfo.md` 878 («lo que el provider emite, se declara») con la pregunta sobre `data-color`: la regla habla del provider de soma; hay otra via legitima por el wrapper. `data-size` es el precedente verificable (`switch.css` lo usa y no esta en el morfo). Falta la otra mitad — si `data-color` pertenece a esa categoria. La lista de la 489 es ilustrativa y no lo incluye; `eidos.md` remite el canon del color a `theming/reference.md` §25. Anotado como NO resolver antes de leer §25. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f69cf76cfd |
feat(blocks): `newsletter` — el alta al boletín, y el reset que el framework daba por hecho
F2.9. Slots de snippet (eyebrow · title · description · field · submit · note ·
children), `center` y `justified`, y `panel` como interruptor real: el panel de
marca es la cara de las referencias, pero el control, la ayuda y el error están
calibrados para la superficie de página, así que apagarlo es un estado de primera
y no un fallback.
La fila es `Grid templateColumns={{ base: '1fr', sm: '1fr auto' }}`: el campo se
queda la pista libre y la acción abraza su contenido —eso es lo que hace que un
alta se lea como UN gesto— y bajo `sm` pasa a una columna, porque un botón al lado
de un campo de correo deja inservibles a los dos.
El block NO valida y NO emite sema. `Form` posee el runtime, el esquema, dirty/
touched, la agregación de errores y el foco al primer error; `Field` posee el
cableado ARIA; el morfo del `Form` ya declara `commit-submit` y `signal-invalid`.
El app posee esquema, valores y handler. No hay ni un `if` sobre un correo aquí.
## Superación del dossier: la etiqueta
El único hueco que el dossier nombraba era la nota de privacidad, y está. Pero la
diferencia de verdad es la etiqueta: las referencias shipean la fila escondiéndola
con `sr-only` o dejando solo un placeholder. El canon tiene `Field floatingLabel`
—arranca dentro del control y sube al borde al enfocar o rellenar—, así que la fila
queda alineada CON etiqueta real y asociada. Verificado con pulsaciones de teclado
de verdad: 10px dentro en reposo → −11px sobre el borde al enfocar y al rellenar.
## Tres hallazgos de canon más, medidos
- **La fundación de eidos no trae reset de modelo de caja y lo asume del app.**
`[data-field-control]` declara `inline-size: 100%` + padding, así que bajo
`content-box` el control mide 30px más que su contenedor: el campo se metía por
debajo del botón de envío. Campo 480 / control 510 en la galería frente a
502 / 502 en los docs de componentes, que sí resetean (igual que
`web/routes/active/styles.css`). Arreglado en app-land con
`web/routes/blocks/_lib/reset.css`, con A/B sobre los 10 previews y 5 páginas de
shell: cambia el newsletter y NADA más. Hay que importarlo dos veces porque la
galería arranca UIX en línea en vez de pasar por `BootUix` — deuda del arnés,
anotada en el handoff.
- **`onValidSubmit` es un no-op silencioso** cuando se pasa un `form` ya
construido: el componente solo lo reenvía al `createForm` que hace él mismo. El
envío validaba, limpiaba el error y no anunciaba nada. Por eso el block no
expone el prop: el handler va en el `createForm` del app.
- **Los mensajes de SIUM son idlangref.** La vía correcta es
`uix.langs.t(issue.message, issue.params)` —verificado, sale «Debe ser una
dirección de correo válida»—, pero la demo de docs del propio `Form` parte la
cadena a mano tras el `|`, así que el único ejemplo del repo enseña el patrón
equivocado y siempre muestra inglés.
Los tres quedan en los gaps del README y en `PLAN-blocks-quality.md` §6
(F18/F19/F20), sin tocar nada fuera del tier.
Verificado en navegador el arco completo: correo inválido → error traducido con
`role="alert"`, `aria-invalid`, `aria-describedby` y foco al primer error; correo
válido → confirmación del app (`Callout` afirmativo con la dirección) y error
limpio. Claro/oscuro/RTL, tres colores, `panel` sí/no, 420px. Cero errores de
página. Gates: `blocks:check` verde (10 blocks) · `svelte-check` sin errores
propios · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
25f8126e0a |
docs(process): morfo.md leída — y una regla que cuestiona la conclusión revertida
`architecture/morfo.md` (888 lineas) leída completa en su estado original. Era la prioridad del registro: la capa del contrato, la mas tocada en la auditoria fallida, y en la que se escribio criterio sin haberla leido. Lo que sale: - El hueco documental de §Step 2 (omite `'private'`, la lista de referencia si lo documenta) es REAL, confirmado contra el original. Lo que estaba mal era el criterio que se escribio ahi como arreglo, que clasificaba mal a los primitivos de API plana. El hueco sigue abierto. - `emit: 'value'` es forma legitima para attrs que llevan valor real. - Y una regla que CUESTIONA la conclusion revertida sobre `data-color`: «Provider emits a data-attr not in the morfo → morfo-check fails in CI. Either add the attr to the morfo or rename it to `data-_*`», mas el incidente 2026-05-20. La sesion revertida concluyo que `data-color` no debe ir en el morfo por ser vocabulario de eidos; esta regla dice que lo emitido se declara. Queda anotado como NO resolver hasta leer `eidos.md` completo y `theming/reference`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
33d4e81daf |
docs(process): registro de lectura del corpus doctrinal
La lectura del corpus no cabe en una ventana de contexto (~19.000 lineas). Sin registro, cada sesion la reempieza o se la salta — que es como se auditó a ciegas el 2026-07-29/30 y hubo que revertir 7 commits (`3097cfcb6`). Lo importante del fichero no es el inventario, es el aviso de contaminación: la lectura del 2026-07-30 se hizo sobre el árbol CON las ediciones de `c39170abb` dentro, así que 7 de los 10 documentos leídos devolvían la escritura del propio agente como si fuera doctrina. Dos casos medidos: - `testing-and-tooling.md` — el original dice que el guard de tests por provider existe; la edición lo había cambiado a «convention, not a guard», y se citó esa frase para justificar una decisión de arnés. Autocita. - `overview.md` — la edición sustituyó «Three Soma scenarios» (`commitState`/`emitEvent`) por «One door: runtime.trigger», y con eso se «descubrió» una contradicción contra `soma-architecture.md` que la propia edición había creado. Quedan por tanto 3 documentos de lectura válida (`README`, `sema`, `eidos` parcial) y 7 marcados RELEER. `morfo.md` (888 líneas) sigue sin empezar, y es la prioridad: es la capa del contrato y se llegó a escribir criterio en su §Step 2 sin haberla leído. Se registran los 4 hallazgos que sí se sostienen (todos de documentos limpios y verificados como originales) y se retiran los 2 que eran autocita. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
03d6796582 |
feat(blocks): `cta` — el panel que pide el siguiente paso, y tres mentiras que destapó al medirlo
F2.8. Slots de snippet (eyebrow · title · description · actions · children),
misma forma que `hero`: las partes de un CTA no repiten ni coordinan, así que un
compound no se gana nada. Dos disposiciones: `center` para cerrar una página y
`justified` —copia al inicio, acciones al final— que es el hueco que nombraba el
dossier. Un solo `Motion trigger="viewport"`: el panel llega ENTERO, porque un
CTA es una sola afirmación y repartir sus tres partes se leería como duda.
Lo que salió al verificarlo en navegador, medido y no a ojo:
- `variant='soft'` **fuera de la API**. Su track queda a `oklch(0.9932)` contra
un `--color-surface-default` de `oklch(0.9911)`: 0.002 de luminancia, o sea
ningún panel en claro. Y `Surface` no tiene borde al que caer. Cortar el prop
es más barato que shipear un estado que se esfuma.
- La ranura `contrast` de la paleta es blanco en TODO escalón sólido, así que un
lienzo de luminancia media deja el cuerpo por debajo de AA: `primary` 5.18 ·
`indigo` 5.21 · `plum` 4.75 pasan en ambos modos; `neutral` 3.32 · `teal` 3.07
fallan en claro. El block reenvía cualquier `color`; la demo solo ofrece los
que pasan.
- `Text align` es inerte por defecto: renderiza un `span`, y `text-align` no hace
nada sobre una caja inline. `align="center"` dejaba la copia a la izquierda
dentro del layout centrado, sin avisar. Rodeado con `as="p"`.
Y `Group` no apila: a 420px la etiqueta de la acción secundaria se parte contra
el botón primario, así que las acciones van en `Flex direction={{ base:
'column', sm: 'row' }}`. `hero` compone las suyas con `Group` — anotado.
Los tres hallazgos de canon quedan en los gaps del README del block y en
`PLAN-blocks-quality.md` §6 (F15/F16/F17), sin tocar nada fuera del tier.
Verificado: `center` y `justified` en claro/oscuro/RTL y a 420px, tres colores,
entrada disparada, descripción en `<p>` centrada, cero errores de página.
Gates: `blocks:check` verde (9 blocks) · `svelte-check` sin errores propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
3097cfcb69 |
revert: deshacer la auditoría entera — se hizo sin leer la doctrina
Revert de los 7 commits de la sesión del 2026-07-29/30: |
2 months ago |
|
|
352ca8bbec |
style(soma): formato Prettier en el test de gradient-picker
Dos envolturas de línea que el formateador del proyecto no acepta, en un fichero que escribí yo. Detectado auditando mi propio trabajo contra las reglas duras. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
17a1f167b7 |
test(soma): chat-list — el puente de selección y el runtime por instancia
Sexto de los 13 providers sin red. Dos cosas que este provider decide y que nadie comprobaba: - **El puente de una conversación activa al `string[]` del Listbox.** En las dos direcciones: sin conversación abierta la selección es `[]` y no `[undefined]`; deseleccionar todo cierra la conversación en vez de dejar un id obsoleto; y el consumidor recibe siempre un id plano en `onValueChange`, nunca el array. - **El runtime POR INSTANCIA del Item.** Dos filas, una con `unread` y otra sin, no se pisan — es el patrón AccordionItem, y existe precisamente porque una fuente de props compartida no puede llevar estado por instancia. Sin este test la regresión sería silenciosa: ambas filas leerían la misma. También queda fijada la doble registración sobre el mismo elemento (id + ref compartidos, el precedente de chat-log): el Listbox aporta `role=listbox`, teclado y selección; chat-list añade el contrato de fila. Sobre el arnés: el Listbox compuesto se construye DE VERDAD, y su `TypeaheadBuffer` exige `uix.timers` — va con un `createEngineTimers` real sobre `createFakeTimerClock`, el vehículo de test del propio framework, en vez de un servicio inventado. `prefs.getDir` sí es un lector puro y sigue el patrón de la suite existente. Quedan 7: `chronos`, `gradient-builder`, `month-grid`, `nav-tree`, `password-field`, `sidebar`, `year-grid`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
e70397fcab |
test(soma): field-langs y gradient-picker — el invariante, no la superficie
Dos providers más de los 13 sin red. Como los anteriores, apuntados a lo que cada uno decide y nadie comprobaba: - **`field-langs`** (8): la resolución de `switcher: 'auto'` por número de idiomas (segmentado hasta 3, `<select>` desde 4, y un modo explícito nunca se reinterpreta); el guardado de `activeLang` contra el conjunto vigente — sin él un idioma retirado deja el editor sin dirección; que un valor de sólo espacios cuente como incompleto e invalide el campo si ese idioma era requerido; y que los proxies `activeValue` / `activeLangArray` escriban en el hueco correcto, incluido que deseleccionar en el ToggleGroup NO deje el editor sin idioma. - **`gradient-picker`** (7): que `css` serialice el gradiente COMPROMETIDO y no el de trabajo — es lo que hace que el chip del trigger no se mueva hasta Aceptar en modo diferido; que `showDelete` exija que el preset guardado se haya SELECCIONADO y no sólo que coincida con el valor; el tope y el rechazo de duplicados en `canSave`; y que el provider dispare él mismo `commit-save` / `commit-remove`, porque el Button sólo emite el `contact.activate` neutro. Los dos fallos de `gradient-picker` en la primera pasada eran de la aserción, no del código: `$state` devuelve un proxy profundo, así que comparar gradientes con `toBe` falla por identidad. Comparados por valor. Quedan 8: `chat-list`, `chronos`, `gradient-builder`, `month-grid`, `nav-tree`, `password-field`, `sidebar`, `year-grid`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
c9b83d0257 |
docs(morfo): qué hace pública a una parte, y tres hallazgos retirados
`morfo.md` §Step 2 — la guía de AUTORÍA, donde el autor decide — documentaba sólo `'public'` y `'virtual'` y omitía `'private'`. La sección de referencia sí tenía los tres, pero nadie declara partes leyendo la referencia. El criterio que queda escrito: **lo que hace pública a una parte es que el consumidor la decida, con independencia de la sintaxis**. La composición (`<Dialog.Trigger>`) es la vía habitual, no la única: un primitivo de API plana gobierna sus partes por props y snippets — `<Badge dot>` decide la parte Dot, `icon` la parte Icon. Ambas son públicas. `'private'` queda para lo que nada de lo que escribe el consumidor alcanza (el Track de `Spinner`, el Sentinel de `Sticky`). Importa el matiz porque la primera versión de esta nota decía lo contrario — que el test era mecánico y que sin `<Componente.Parte>` la parte no es pública — y eso habría reclasificado mal a Badge y a todo primitivo de API plana. ## Handoff: tres hallazgos retirados Los tres salieron de heurísticas de una sola vía dadas por hechas sin abrir el código. Es el mismo fallo que la auditoría denuncia en los guards, cometido al redactar el informe: - **D-3 `AgentTimersPort.once`** — el puerto SÍ está satisfecho: `defineActiveAgent()` adapta `core.timers.schedule` a `once` en la raíz de composición, que es el patrón previsto. No había nada roto. - **«5 derivas de scope»** — `avatar-group`, `path-trace` y `rotate-align` están implementados; el framework co-loca sub-componentes dentro del directorio del padre y la heurística asumía «directorio == kebab». - **«29 morfos con `kind:'public'` irreal»** — medía si existe `<Componente.Parte>` e ignoraba props y snippets. Sin recuento fiable hasta cruzar las tres vías. Añadido: `npm run lint` nunca ha pasado. Separando el ruido CRLF del checkout Windows (`--end-of-line auto`) quedan 1544 ficheros con deriva real de contenido. Es decisión del usuario porque el arreglo toca todo el repo de golpe. Y la lección de método: **un analizador que no puede resolver algo tiene que decir «no verificado», no emitir un hallazgo.** Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
feb4a84241 |
test(soma): los primeros providers que no tenían red
13 providers de soma no tenían test, y el guard que dos documentos afirmaban que lo impedía (`NO_MISSING_PROVIDER_TESTS`) no existe. Van los tres primeros, cada uno apuntado a la doctrina que el provider codifica y que hoy no cubría nadie — no a su superficie trivial. - **`button`** (7): la resolución `color`↔`intent`. Un intent evaluativo gana sobre la jerarquía Y suprime el marcador custom (la estética no puede contradecir a la semántica). Cubre también que `onPress` devuelve el intent como contexto VISUAL, no como carga del evento — el contacto no lleva intent (libro cap. 22 §11). - **`anchor-nav`** (6): la escalera de `activeId` (primero en banda → último pasado → primero en orden DOM) y el ref-counting que impide que un remount keyed tire un target aún referenciado por su hermano. - **`textarea`** (9): que el warn de overflow se emita SOLO en la transición —la proyección es `untilFix` y persiste, re-emitir sólo la reemplazaría—, su retracción al bajar del tope, y la matriz completa de `submitOn` (`enter` / `mod+enter` / `false`). Dos fallaron a la primera y en ambos el código tenía razón y la aserción no: `data-color-custom` es flag de presencia (el color viaja por la var CSS `--color-custom`, que estampa eidos), y `clearTarget` no dispara sin la parte `input` montada porque la retracción apunta a ese elemento. Arnés: el patrón ya establecido en los 87 tests existentes. Donde hace falta `langs`, el stub devuelve el fallback embebido del idlangref en vez de copia inventada — es lo que hace el resolver real cuando no encuentra entrada. Quedan 10: `chat-list`, `chronos`, `field-langs`, `gradient-builder`, `gradient-picker`, `month-grid`, `nav-tree`, `password-field`, `sidebar`, `year-grid`. Al cerrar la lista, escribir el guard que la mantenga cerrada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
f8e35b8fd8 |
fix(morfo): el eje intent/color — el morfo no puede nombrar lo que es de eidos
El `data-color` de los morfos declaraba enums de 3 a 8 valores cuando la prop acepta 42. No era descuido: es que **el morfo no puede nombrar ese vocabulario**. `COLOR_ROLES` y `PALETTE_SCALES` viven en eidos; `INTENTS` vive en `src/uix/intent.ts`, propiedad de UIX y de ninguna capa. Y `src/uix/morfo/**` no importa de eidos jamás — su único import cruzado es `../../intent`. Nadie podía traer la lista real, así que se copió a mano un subconjunto y se fosilizó: `<Switch color="teal">` siempre emitió `data-color="teal"`, un valor que su propia declaración negaba. ## Dos ejes, no uno - `data-color` = **direccionamiento de paleta**: qué fila de la tabla lee la instancia. `renderSharedPaletteLayer` emite un bloque por nombre canónico y el recipe consume los `--palette-*` resultantes. Soma lo resuelve aplicando la doctrina «el intent evaluativo gana a la jerarquía». - `data-intent` = **estado evaluativo persistente**, cuyo gemelo transitorio es `data-event-intent`. Ya lo declaraban `knob`, `timeline` y `toast`. Estaban fusionados en un solo atributo (`switch`/`toggle` llegaban a listar `primary, secondary` junto a cuatro intents en el mismo enum), y por eso no se podía asignar limpiamente a ninguna capa. ## Cambios - `intentsExcept()` en `src/uix/intent.ts`: narra un subconjunto por EXCLUSIÓN, tipado contra `Intent`. Un componente toma los 6 o menos, pero nunca vuelve a escribir el vocabulario — si mañana entra un séptimo intent canónico llega solo a todo el que no lo haya rechazado. `knob` migrado (conjunto idéntico, verificado). - `dialog` → `data-intent`. Aquí SÍ era intent mal etiquetado: 0 usos de `--palette-*`, sin `resolveComponentColor`, y reglas propias cuyos valores eran todos intents. El provider declaraba `intent` **y** `color` con la misma expresión exacta — una copia, no una traducción. Migrados morfo (×2), provider y las 4 reglas CSS. - `switch`, `toggle`, `button`, `card`, `avatar`: enum de `data-color` fuera, `emit: 'value'` en su lugar. El DOM no se mueve: `enumValues` no lo consume nadie en runtime y `evalAttrPlan` hace `case 'enum': case 'string': return raw`. Ambos comprobados antes de tocar. - De paso, los 10 morfos que escribían `expression:` con comilla doble en un corpus de 173 que usa comilla simple. ## Verificación 514 tests de morfo + contracts + eidos · 7/7 del provider de button (cubre `data-color='primary'` por defecto y `'threat'` con intent evaluativo) · `eidos-lint` con `invalid: 0` en los seis componentes tocados · `check` sin errores nuevos. En navegador: `dialog` con `risk` y `threat` tiñe borde y título con los valores exactos de `--color-risk-border` / `--color-threat-border`; `switch` resuelve fila de paleta distinta para `neutral`, `primary`, `threat` y `teal` — esta última una de las 33 escalas donantes que el enum viejo negaba. `rose`, que no es canónica, sale vacía y sirvió de control negativo. Nota: `chronos` ya declaraba la forma correcta (`emit: 'value'`, sin enum). Lo tenía excluido de la auditoría, así que deduje desde cero un patrón que ya estaba en el árbol. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
4e785d32bf |
feat(blocks): F2.7 `stats-band` — la cifra que se cuenta sola
La fila de números que respalda lo que la página acaba de afirmar. Compound
(`.Stat` repite, sin contexto): `<StatsBand>` + `.Stat` + `.Value` + `.Label`.
- **Compone, no reinventa**: la semántica de KPI es el `Metrics` del canon, leído
de su API y pasado tal cual (precedente `faq`/`Accordion`). `Metrics` es
surfaceless, que es exactamente lo que una banda necesita: no son tarjetas de
panel. Un delta, un icono o un sparkline son `Metrics.Delta`/`.Icon`/`.Chart`
que la app compone dentro — el block no los re-expone.
- **La superación literal del dossier**: `<StatsBand.Value count={12500} />`
compone `CountUp`, así que la cifra se cuenta sola al entrar en pantalla.
NINGUNA referencia puede shipearlo: todas entregan markup estático. El formato
locale-aware y el salto directo bajo reduced-motion vienen del `CountUp`, no
de aquí.
- **`count` es opt-in, nunca default**: una cifra que se anima sin que el lector
lo pida es ruido, y algunas no son contables («99,98 %»). Sin `count`, la
cifra la pone la app por children.
- **Ni una palabra ni un separador salen del block** (B-7): las palabras por
children, el formato por `uix.format` dentro del `CountUp`.
- Escalonado estructural: `data-stagger` en la banda y cada `.Stat` es un
`Motion trigger="viewport"`, así que las cifras aterrizan una tras otra sin un
solo milisegundo escrito a mano.
Verificado en navegador (con la banda bajo el pliegue, para cazar el conteo desde
el primer fotograma): 4 cifras con índices estructurales 0,1,2,3 contando
—9377→11.678 · 255→318 · 36→45—, el porcentaje quieto por no ser contable, y el
separador de millares por locale (`11.678`). Cero errores de página.
`blocks:check` verde (8 blocks) · `svelte-check` sin errores propios.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2 months ago |
|
|
c39170abbf |
fix(uix): la auditoría del sistema — lo que los guards no veían
Auditoría clean-room de todo ActiveUIX (excluido `web/`), componente a
componente. Lo que sale de aquí no es una lista de bugs: es un patrón.
El framework validaba que lo escrito fuese VÁLIDO, no que lo declarado
se CUMPLIESE — y sus guards fallaban ABIERTOS.
## El colapso de las uniones de props (95 → 0)
Un `Props` de eidos es `{ …props propias… } & <atributos nativos>`.
Cuando el elemento declara un atributo homónimo, la intersección funde
ambos y una unión estrecha contra el `string` nativo COLAPSA a `string`.
Causa: `Without<T, U> = Omit<T, keyof U>` invocado como `Without<T, {}>`
— `Omit<T, never>`, un no-op — 433 veces en soma; sólo 3 con argumento
real. Invisible para `svelte-check`: ensanchar un tipo no es un error,
es una garantía perdida.
Medido: 95 props en 72 componentes. `<Avatar color="nonsense">`
compilaba. `ComboboxInput.size` chocaba con el `<input size>` numérico
y era inusable. Migrado con codemod sobre AST (nunca regex) a
`Own & Omit<Nativos, keyof Own>`: 92 tipos en 73 ficheros + carousel a
mano. `check` no se movió.
Garantía nueva: `eidos/prop-surface.test.ts` (PROP-1) compara los
literales de la anotación del autor contra los de la propiedad pública.
Verificado que falla reintroduciendo el defecto.
## Los cuatro guards que fallaban abiertos
- `translations:check` crasheaba en CADA ejecución de su historia — un
stripper de comentarios borraba `//` dentro de strings. Sustituido por
import dinámico. Al arrancar destapó 8 slots `texts` sin traducción.
- `soma-attr-audit` agotaba el timeout de 5 s: sin veredicto, verde por
omisión.
- `component-audit` D-7.4 hacía `continue` mudo cuando el tipo no
resolvía. Ahora resuelve con el checker de TypeScript
(`scripts/prop-unions.ts`): puntos ciegos de 124 → 3.
- `component-audit` R-1.1: el regex casaba `[data-motion='reduce']` y
daba PASS por el motivo equivocado.
Regla adoptada: un guard que no puede evaluar TIENE que decirlo. El
informe lleva ahora bloque «Not verified» y recuento en el resumen.
## D-1 · tooltip y D-2 · card, cableados
`tooltip` declaraba 3 eventos `emerge` que nadie emitía. Ahora emiten;
`present` pasa a `sequence: 'post'` — con `'pre'` el hold de ~240 ms
gateaba el montaje del propio overlay.
`card` declaraba `commit-select` sin emisor posible (scope sin soma).
Puente headless en `soma/components/card/` con la forma ya establecida
por `menu-dial` / `onion-menu`: eidos posee estado y render, soma posee
sólo el `SomaRuntime` que emite.
## Documentación: 22 mentiras corregidas
`docs/` afirmaba guards inexistentes (`NO_MISSING_PROVIDER_TESTS`),
APIs con firma equivocada y un modelo de Motion que el código no
implementa. Corregido en CANON, arquitectura, glosario, theming/motion,
checklist y los README de `motion` / `callout` / `arts/motion`.
## Además
- CardGroup: la descripción se metía en la primera celda del grid.
- Motion: `data-state` siempre estampado, salida real en `leave()`,
token fantasma `--motion-stagger-each-default` eliminado.
- `engine-motion`: `handoffState` Map → WeakMap (fuga por nodo).
- `mockup`: primitivo crudo → token de rol (R-4.6).
- 5 catálogos de traducción que faltaban.
Handoff: `docs/process/CONTINUE-audit-2026-07-29.md`.
Batería: check 0 errores en src · vitest server 3701/3701 ·
docs:check 0/0 · component:audit 161 PASS / 2 NEEDS-WORK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2 months ago |
|
|
d4c49aedbf |
feat(eidos): `Barcode` — el código de barras 1D, con encoder propio y cero dependencias
Ocho simbologías desde su estándar ISO publicado (Code 128 con auto-switching A/B/C, EAN-13/8, UPC-A/E, Code 39, ITF/ITF-14) en `$libs/barcode`: tablas transcritas de la especificación o derivadas de su regla de construcción, sin una sola dependencia npm añadida. JsBarcode y bwip-js son referencia de corrección, jamás import — la misma doctrina con la que `$libs/qr` usó a Nayuki. Ningún headless del mercado trae barcode: el terreno lo ocupan encoders imperativos sin anatomía ni a11y. - **La física manda sobre la ergonomía visual.** La dimensión X (`moduleWidth`) es el ancla de escaneabilidad y la altura de barra un eje libre — un escáner lee una franja horizontal. Por eso la geometría es px absolutos en el viewBox (precedente: la familia `chart`) y no el token `size` cuadrado del QR: colapsar los dos ejes en uno haría que la X dependiera de la altura, que es justo lo que no puede flotar. La zona muda va en módulos, con el mínimo de cada estándar. - **Lo que hace reconocible a la familia.** Las guardas EAN/UPC bajan hasta la línea base y los dígitos se imprimen dígito-bajo-dígito sobre su celda de 7 módulos, en mono (la HRI del estándar es OCR-B, de ancho fijo). ITF-14 lleva su barra portadora por fuera de la zona muda, que no puede comerse. - **Un valor no codificable es un ESTADO, no una excepción.** `encode` lanza un `BarcodeError` tipado; el wrapper lo captura, marca `data-invalid`, avisa por `onInvalid` (deduplicado por razón: `encoded` es un objeto nuevo por pulsación) y deja un marco placeholder decorativo. Un EAN a medio teclear es un paso normal de edición; dejarlo reventar dentro de un `$derived` tumbaría la página. - **`'text'` entra en `MorfoElement`** — y también en el schema de sium, que tiene su propia lista literal. Añadir solo la unión de TypeScript compila y falla en runtime; lo cazó `morfo:check`, no `npm run check`. Verificación: 48 tests de encoder en tres capas (invariantes estructurales de cada tabla · vectores publicados transcritos aparte · decoders independientes que releen la retícula), la lectura real confirmada con escáner por el usuario, y la retícula decodificada desde el SVG ya pintado en navegador. `component:audit` PASS con 0 errores; los 2 warnings (D-1.5 / D-4.3) son los mismos que da `button`, la demo canaria: sus regex buscan la forma v1 inline del MutationObserver y del `emit`, que la v2 movió al harness compartido. Demo v2 canónica de 9 pestañas. El alcance diferido (Codabar/MSI/Pharmacode, GS1-128, add-ons EAN-2/5, y las 2D como componentes aparte) queda registrado en `next-features.md` §10 — el hueco de numeración se cierra cuando el track de blocks commitee sus §8/§9, hoy sin commitear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
2 months ago |
|
|
cdc00bc117 |
fix(blocks): lo que la auditoría encontró DENTRO del tier
Una auditoría multi-agente (7 dimensiones doctrinales + refutación adversarial) confirmó 32 hallazgos sobre la pasada de calidad. Esto corrige los que caen dentro de `blocks/`; la deuda de fuera queda REGISTRADA, no tocada (`PLAN-blocks-quality.md` §6). - **B-7 roto en `pricing`**: el block traía tres strings castellanas propias como defaults del toggle — dos visibles y un nombre accesible. Hardcodeaba un idioma DENTRO del framework y esquivaba `langs`, en el mismo block cuyo README predica «cero strings propias». Ahora los tres son props REQUERIDAS y las pone la app. - **El block pintaba**: `pricing.Plan` metía `box-shadow` por `style=` porque `Card` no expone elevación. Retirado; el hueco queda registrado como candidato a canon en vez de falseado. - **El escalonado estaba roto y el comentario mentía**: puse `spring-pop` (driver JS) en los planes, y un driver JS no lee el `animation-delay` donde vive el stagger — entraban TODOS a la vez mientras el comentario afirmaba lo contrario. Vuelto al preset CSS: verificado, índices 0,1,2 a 70ms. - **Comentario falso en la demo del hero**: atribuía al `intent` visual una consecuencia sonora/háptica que ningún camino produce (el morfo del Button dice explícitamente que `contact-activate` NO lleva intent). Reescrito. - **Composition maps mentían**: los 6 blocks que ahora componen `Motion` no lo decían; el hero seguía diciendo `Heading` (es `Display`), sin `Backdrop` ni `decor`, y declaraba DIFERIDO un preset de cromo de media que ya está shipeado (`Mockup`). Al día, sin filas duplicadas ni datos rancios (el chip de feature-grid es `solid`, no `soft`). - **Handoff y plan**: el plan seguía prescribiendo un `<Reveal>` que esta misma sesión creó y retiró; el handoff daba `feature-split` por «sin decidir» estando hecho, y los gates con números falsos. Verificado: `blocks:check` verde (7) · `svelte-check` sin errores propios · en el navegador, pricing escalona de nuevo, las etiquetas vienen de la app y ninguna tarjeta pinta sombra. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
a25be8903b |
docs(agent): el handoff deja de ser un diario y pasa a ser un punto de arranque
Se había convertido en un log por sesiones — la forma equivocada para abrir en frío, donde lo que hace falta es «qué hago ahora», no «qué pasó». Reordenado por lo que necesita quien llega: 1. lo PRIMERO al abrir (tres comandos; si no están verdes, eso es el trabajo) 2. lo BLOQUEADO — las dos firmas de §D, con el porqué de que §D.2 bloquee F4b (la escalada lleva razón pero no pregunta; la resolución no lleva respuesta) 3. lo DESBLOQUEADO — los dos manifiestos de F4b, que no dependen de §D 4. estado, flecos y lo que está sin firmar pero no bloquea 5. verificación 6. disciplina y gotchas, agrupados (git · verificación visual · trampas de código que ya mordieron) 7. la historia, comprimida al final Los gotchas ganan tres entradas nuevas que costaron tiempo esta sesión: el HEAD puede retroceder bajo tus pies en una rama compartida (comprobar con merge-base antes de alarmarse), `createEngineTimers()` como `timers` del motor tipa pero revienta, y `asserts condition` no se puede desestructurar. Sin cambios de código. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
8b89c7f6b7 |
feat(agent): el kit de conformance del contrato de delegación — portable de verdad
El segundo vehículo que la spec exige (§0.5 · Annex C.1), y el que de verdad acredita: `agent-check` es un lint de NUESTRO repo; esto lo corre un tercero contra SU implementación. PORTABLE POR CONSTRUCCIÓN El kit no importa `createEngineAgent`. Habla con un `ConformanceSubject` estructural —start · run · authorize · reject · resolveEscalation · stop · disable— que el implementor adapta sobre su motor; `subject.ts` es el ejemplo trabajado de ese adaptador, no una dependencia. Vive fuera del barrel (precedente `adapters/*`, D-AG.1c): quien no corre conformance no lo paga. DIEZ CASOS, CADA FALLO CITANDO SU ID AG-2 · AG-4 (el control vuelve aunque el transporte reviente — la lectura más dura del requisito) · AG-5 · AG-6 (escalada no terminal + razón tipada) · AG-9 (interrumpir es volver, no un error) · AG-10 (kill global) · AG-21 (una capacidad inventada vuelve como resultado corregible, no mata el run) · AG-25 (lo irreversible no se ejecuta sin revisar ni con el techo más suelto) · AG-26 (clamp de iniciativa) · AG-50 (degradación sin transporte). Un caso que falla solo se falla A SÍ MISMO: una implementación a la que le falte un requisito recibe igualmente el informe completo del resto, que es la diferencia entre una suite de conformance y un smoke test. Y EL KIT SABE FALLAR Un kit que no puede fallar no prueba nada, así que hay un test que rompe AG-4 a propósito y comprueba que el informe lo nombra. No es lo mismo que `engine-agent.test.ts`: aquella suite prueba el MOTOR con acceso a sus interioridades; ésta prueba el CONTRATO por la misma puerta que tiene un tercero, y es la que nos pillaría enviando un eje que nuestra propia especificación rechaza. AG-35 y AG-41 quedan FUERA a propósito mientras §D de la spec siga abierta — un kit que afirmara un requisito sin decidir estaría inventando la decisión. Hay un test que fija esa ausencia. TRAMPA CAZADA, QUE ES LO QUE HACEN LAS SUITES Tres casos fallaban por lo mismo, y no era el motor: pasar `createEngineTimers()` como `timers` TIPA pero revienta — `EngineTimers` expone `schedule`, no `once`, así que el primer timeout llama a `undefined` y el run cierra como `transport-error`. La factory de `active-app` adapta el puerto justamente por eso. Anotado en el handoff. Verificado: agent 33/33 (10/10 conformance) · arts:check 24/24 · agent:check limpio · docs:check sin errores nuevos · check de vuelta en los 73 preexistentes (los 17 que aparecieron eran míos: `asserts condition` exige llamarse por un nombre anotado, y yo lo desestructuro del contexto). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
8db3eea852 |
feat(eidos): `Motion` entrega la entrada al MOTOR — los drivers JS ya corren
`Motion` solo estampaba `data-animation-style`. Eso cubre el camino declarativo
(los presets `css`), pero deja fuera la mitad del sistema: un preset JS —
`spring`, `waapi`, `rect` (FLIP)— necesita que alguien llame al motor. Con la
versión anterior, `<Motion motion="spring-pop">` ponía el atributo y NO animaba
nada. La trampa exacta de quedarse en la capa declarativa.
Ahora, al entrar, `Motion` llama a `eidos.motion.run(node, 'enter')` — el mismo
punto de entrada que usa el `Presence` de soma. El motor lee el
`data-animation-style` del nodo y despacha: un preset `css` asienta al momento
(su regla ya está sonando), uno `js` ejecuta su driver. Al desmontar, `cancel`.
`pricing.Plan` estrena `spring-pop`: las tarjetas llegan con física, no con
curva.
Verificado midiendo la animación en vuelo (con la tarjeta bajo el pliegue, para
cazarla desde el primer fotograma):
scale: 0.767 → 0.926 → 1.010 → 1.057 → 1.064 → 1.044 → 1.019 → asienta
Eso es overshoot real, escrito como propiedades independientes (`scale:` /
`opacity:`) por el integrador vía `requestFrame` — lo que ninguna cúbica
expresa. Nota de método: medirlo en `transform` daba `none` y parecía que no
corría; `scale` moderno es propiedad propia, no entra en `transform`.
`blocks:check` verde (7) · `svelte-check` sin errores propios. (La suite de
eidos tiene 3 fallos AJENOS: `components/barcode/` sin trackear, con solo un
README, rompe el contrato de API — de otra sesión.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2 months ago |
|
|
ceab195ae1 |
feat(blocks): el arco semántico completo — contacto al pulsar, commit al elegir
Elegir un plan ya no era solo un click: ahora demuestra el momento `--event` entero, que es lo que ninguna referencia puede shippear. - Pulsar emite `contact-activate` — gratis, por composición: es el evento del morfo del propio `Button`. Eso ya funcionaba en todos los bloques. - Lo que faltaba es la CONSECUENCIA: la app emite `commit-select` con intent `affirm` (CANON §2: una elección activa es afirmativa) vía `uix.events`. Un block no puede emitir eventos propios —no tiene morfo, contrato B—, así que la consecuencia es necesariamente app-land; la demo la ejerce para que el arco se vea. El commit va DESPUÉS del resultado (`await tick()`), no en la pulsación. Es la doctrina (un commit es lo que quedó fijado, no el contacto) y además evita una colisión real que medí: emitidos a la vez sobre el mismo elemento, la proyección del contacto pisa la del commit. Verificado con MutationObserver usando `attributeOldValue` —sin él la medición MIENTE, porque el callback se agrupa en microtarea y `getAttribute` ya devuelve el valor sobrescrito—: la secuencia es `contact-activate` → `commit-select` (family `commit`, intent `affirm`), cero errores de página. `blocks:check` verde (7) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
19e8dbfb63 |
feat(scene): `portal` — la presencia leída como esfera a contraluz (variante)
Encargo del usuario a partir de un componente de referencia de pago cuya fuente NO está disponible (la pestaña «Code» es de clientes). Así que implementación propia — que es la regla de la casa de todos modos: solo el vocabulario de parámetros se dejó cercano para poder compararlos. QUÉ ES Una esfera a contraluz: filo encendido, interior abierto, y capas de estelas que la envuelven y giran — cada una sobre su eje inclinado, con su ritmo, con motion blur a lo largo del barrido. El fragmento se mapea sobre la esfera unidad y las estelas viven en ese espacio 3D, que es lo que las hace curvarse con la superficie en vez de leerse como rayas planas. ES UNA VARIANTE, LA ELIGE EL DESARROLLADOR No sustituye al `orb`: conviven. `orb` está afinado para 32-48px (silueta simple, movimiento perceptible a ese tamaño); `portal` tiene mucho más detalle y luce en superficies grandes — a tamaño de componente pierde justo la irregularidad que lo define. La demo gana un switch **studio** (220px) para poder juzgar eso, porque un shader no se juzga a 40px y forzarlo desde la consola no deja nada para la sesión siguiente. EL INTERIOR ES TRANSPARENTE No se pinta nada dentro: lo que se ve a través es la página. Eso es lo que hace la variante agnóstica al tema SIN una sola rama claro/oscuro — un interior relleno tendría que elegir bando, y elegirlo fue lo que en el primer intento la convirtió en una bola de cristal. Los tres colores salen de los mismos slots de rol que el orb (`solid` en el filo, `solid-hover` en la cara cercana, `element` en la que se aleja): retintas el tema y cambia entera. TRES LECCIONES DEL FORMATO, no de este efecto — anotadas en el README porque las va a repetir quien toque el siguiente shader: - Las estelas no pueden salir de senos. Un `sin()` da anillos de latitud PERFECTAMENTE regulares que se leen como jaula de alambre por mucho que se afinen; la irregularidad (longitudes distintas, trazos sueltos) exige ruido anisótropo — aquí 1 : 9 en longitud/latitud, lento a favor del flujo y rápido a través. - `sin(q.y * freq)` sobre la esfera unidad da `freq/π` ciclos: ~9 son tres trazos finos por capa, 22 es malla, 2 es un borrón. El número sale de la cuenta, no del tanteo — tanteando me pasé de largo en ambos sentidos. - El motion blur PROMEDIA: 5 muestras × 0.16 rad son 46° de barrido, que borra las bandas en vez de estirarlas. Verificado en Chrome real, claro y oscuro, en `acting` y `escalated`, a 48px y en studio. Tests 370 (scene 9 + eidos 361) · eidos-lint aura 0 inválidos · docs:check sin errores nuevos · check en los 73 preexistentes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
aad0155d55 |
refactor(eidos): `Reveal` era un fork de `Motion` — se consolida en `trigger`
`Motion` ya era el animador de UN elemento: un preset, `data-animation-style` + `data-state`, entrada al montar. Lo único que faltaba era CUÁNDO dispara. Para el contenedor lo resolví bien —`trigger` en el `Cascade` existente— pero para el elemento suelto creé un componente paralelo, con su morfo, su recipe y sus tipos. Eso es forkear lo que ya existe, que es justo lo que el proyecto prohíbe. - `Motion` gana `trigger: 'mount' | 'viewport'` (+ `rootMargin`, `threshold`, `once`, `delay`). `viewport` solo mueve el MOMENTO: la entrada sigue siendo la del preset, estampada por `motionAttrs`, así que reduced-motion degrada dentro del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)` — sin JS la regla no aplica y el contenido se ve. - Atributos en la familia `data-animation-*` (`-trigger`, `-pending`), NO `data-motion*`: ese nombre ya tiene dos dueños (la pref de reduce y la dirección del NavMenu) — motion.md §17. - El ritmo por defecto del escalonado se acota a `[data-stagger] > [data-animation-trigger='viewport']`, para que un `Cascade` normal conserve su default documentado de 0 (paralelo). - Los 7 bloques migran a `<Motion trigger="viewport">`; `Reveal` queda huérfano y se retira (era mío, de esta misma sesión). Verificado: comportamiento idéntico tras consolidar — hero 0..3 con vídeo 470×264 e intent `fulfill`, feature-grid 0..5, testimonials 0..4, pricing 0..2 con `affirm`/`fulfill`, cero errores de página. `blocks:check` verde (7) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
164ca0183e |
feat(blocks): movimiento en TODOS los bloques, vídeo e intent semántico
El acabado deja de vivir solo en el hero. Los seis bloques restantes entran ya con movimiento, la media admite vídeo de verdad y los CTA declaran su intención semántica en vez de ser neutros. - **Movimiento en los 7**: cada rejilla marca `data-stagger` en SU contenedor y sus celdas SON un `Reveal` (feature-grid.Item, testimonials.Item, pricing.Plan) — el foundation les da el índice por `:nth-child`, así que las tarjetas se reparten una tras otra sin un solo milisegundo escrito a mano. Cabeceras, filas de feature-split (copy y media con un compás de diferencia) y el acordeón del FAQ entran también. - **Vídeo**: el hero acepta un `MediaPlayer` del canon dentro del mismo `Mockup` —nada a medida, el slot `media` toma lo que la app componga— y la demo lo expone como control («panel» / «vídeo»). La pregunta «¿y si quiero poner un vídeo?» ya tiene respuesta demostrada, no teórica. - **Semántica en la interacción**: los CTA llevan `intent`. El primario del hero es `fulfill` (esa pulsación consuma la página); en pricing, `fulfill` en el plan recomendado y `affirm` en el resto. Un CTA neutro no significa nada, y los 7 bloques lo eran. Nota arquitectónica: un block NO puede declarar eventos sema propios — no tiene morfo, es el contrato B. Su semántica entra por dos vías, y las dos estaban sin usar: el `intent` de los componentes que compone, y los eventos que esos componentes ya emiten (Accordion emerge, ToggleGroup, Drawer). Verificado con Playwright en los 6: reveals con índices estructurales (feature-grid 0..5, testimonials 0..4, pricing 0..2, hero 0..3), vídeo pintando 470×264, intents `fulfill`/`affirm` llegando al DOM, cero errores de página. `blocks:check` verde (7) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
31580f3b39 |
docs(spec): el contrato de delegación como especificación normativa (DRAFT)
Fase 1 del plan de corrección, y materialización de D-AG.12a firmada ayer: `docs/spec/delegation-contract.md`, versionada por fecha (2026-07-28). QUÉ ES 50 requisitos `AG-1…50` en RFC-2119, con escalera de estabilidad por sección (stable | provisional | reserved), identificadores estables que NUNCA se renumeran ni se reciclan (un requisito retirado conserva su número), y la exigencia de que ambos vehículos de conformance CITEN el identificador que incumplen — «una suite que no nombra lo que falla no acredita a nadie». La doctrina pasa a ser el PORQUÉ y lo declara explícitamente: si doctrina y spec discrepan sobre un requisito, manda la spec y la doctrina se corrige. Annex A lleva las dos reclamaciones de frontera con estado `claimed` (ninguna materializada) y su gate de promoción: ≥2 FORMAS DE DOMINIO con manifiesto conforme — y dice por qué dos participantes de texto no valen: un formato probado solo sobre texto secuencial asume texto secuencial en silencio. Annex B mapea el modelo de amenazas a requisitos concretos en vez de dejarlo como prosa. §0.6 declara la posición honesta (actos secuenciales, cliente- first, generalidad en prueba). LO QUE LA ESCRITURA SACÓ A LA LUZ — y no estaba en ninguna lista - **AG-35 (presencia)**: el invariante correcto NO es «Aura montada» sino «alguna superficie está expresando el ciclo». El playground de agnt ya expresa el ciclo SIN Aura, y exigir el componente lo rechazaría siendo correcto. Además resuelve el problema técnico: el motor es un arte sin DOM y no puede saber qué hay montado, pero sí puede contar adjuntos. - **AG-41 (elicitación)**: especificado y NO cumplido, con el porqué estructural — la escalada lleva razón tipada pero no la pregunta, y la resolución es binaria y no lleva respuesta. No era falta de superficie. - **AG-44**: «ningún estado se distingue solo por el color» sube de decisión de componente a requisito del contrato (ya lo fija `shapes.test.ts`). §D — DECISIONES ABIERTAS, VISIBLES Un DRAFT puede llevarlas; al promocionar, §D debe quedar VACÍA. Son dos y ninguna la firmo yo: `SHOULD` vs `MUST` en AG-35 (con recomendación razonada: SHOULD + política que lo eleve a MUST, porque rehusar por defecto rompería los 30 tests del motor y le quitaría al app el derecho a elegir su superficie) y la forma del canal de elicitación en AG-41. Registrada en el índice del corpus. `docs/README.md` lo edita otra sesión en paralelo: stageada SOLO mi fila (backup → HEAD → mi cambio → add → restaurar su versión + mi cambio), verificado que lo suyo queda sin stagear. Verificado: docs:check 542 docs, sin errores nuevos (el único es el ajeno preexistente de callout). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
f190a2266e |
feat(eidos): `Reveal` + `Backdrop` + cascada por viewport — la capa de acabado
Auditoría del tier (docs/process/PLAN-blocks-quality.md): los 7 blocks de F2 componían EXCLUSIVAMENTE la capa de layout (Box/Stack/Grid/Section/Text/Card). Uso de la capa expresiva del framework: cero. Ni un Motion, ni un Cascade, ni un Display, ni un TextGradient, ni un CountUp. La causa es estructural, no de gusto: el contrato B prohíbe que un block traiga `.css`, así que **un block solo puede ser tan expresivo como los componentes del canon que compone** — y faltaban dos primitivas. El arreglo no es maquillar 14 blocks uno a uno, es construir lo que falta en el canon y componerlo. - **`Reveal`** — reproduce la ENTRADA de un preset al entrar en viewport. No posee keyframes ni timing: estampa `data-animation-style` + `data-state='open'` cuando dispara el observador, así que reduced-motion degrada DENTRO del preset. Mientras espera, el recipe lo mantiene invisible bajo `@media (scripting: enabled)`: sin JS la regla no aplica y el contenido se ve siempre — el fallo de una animación de scroll debe ser «sin animación», nunca «página en blanco». - **Escalonado gratis**: el foundation ya escribe `--motion-stagger-index` en cada hijo de `[data-stagger]` (por `:nth-child`, sin JS) y los presets retrasan por `índice × --motion-stagger-each`. El recipe de Reveal le da ritmo por defecto, así que una lista se escalona poniendo `data-stagger` en SU contenedor: cero delays a mano, cero números en el consumidor. - **`Cascade trigger="viewport"`** — la cascada existente esperaba a `open` (disclosure); ahora puede esperar también a ser vista. - **`Backdrop`** — la capa de profundidad que no existía: `glow` · `mesh` · `grid` · `dots`, pintada en un `::before` desde tokens (acento por el forward de paleta, mesh desde `--gradient-aurora`), con `fade` para no cortar en seco. **El hero como prueba del nuevo listón**: `Display` (el primitivo de tipografía de hero que existía y no usaba — el titular pasa de `Heading` a 80px reales), entrada escalonada de eyebrow/título/subtítulo/acciones, media con `scale-fade` y `decor` (glow por defecto) expuesto como control vivo en la demo. Verificado con Playwright: glow pintado desde el token de paleta, 4 Reveal con índices estructurales 0·1·2·3 a 70ms, titular H1 a 80px, y el patrón `grid` con su máscara. `blocks:check` verde (7 blocks) · `vitest src/uix/eidos` 361/361 · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
79a90e6283 |
fix(eidos): las variables de `Flex` y `Grid` dejan de heredarse
Dentro de un `Mockup`, un `<Grid columns={7}>` colapsaba a columnas de `0px`: el
grid medía 72px, exactamente los 6 huecos de 12px, sin nada para las 7 pistas.
La §46 registró `inherits: false` para las 44 variables de `Box`, pero `--flex-*`
y `--grid-*` se quedaron fuera. Como una custom property hereda por defecto, el
`align="center"` de un `Stack` exterior escribía `--flex-align: center` y CADA
descendiente lo heredaba: un `Stack` anidado que no pide alineación ninguna
acababa con `align-items: center`, su hijo `Grid` encogía a contenido, el ancho
quedaba indefinido y `1fr` no tenía nada que repartir.
Las 7 variables de `flex.css` y las 13 de `grid.css` se registran ahora con
`@property { syntax: '*'; inherits: false }` y sin valor inicial, igual que Box.
Un layout anidado vuelve a decidir su propia alineación.
Verificado: `vitest src/uix/eidos` 361/361 · el grid del mockup pasa de `0px` a
`103.71px` por columna, que es exactamente `(798 − 6×12) / 7` —el reparto real—
· demos de hero, pricing, feature-split, testimonials, button, accordion y table
sin regresión. Doctrina en theming/changelog §48.
Lección registrada: al arreglar la herencia de un recipe hay que barrer TODOS los
hermanos que escriben variables para sí mismos. El «pon `justify` explícito en
los clusters anidados» que yo había anotado en el handoff era este bug sin
diagnosticar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
2 months ago |
|
|
620aef5c48 |
uix(aura): el anillo de custodia sale del `<script module>` y su afirmación queda fijada
Fase 0 de la corrección: higiene, sin decisiones pendientes de firma. EL MOTOR PURO La tabla de formas del anillo vivía dentro de un `<script module>` sin exportar — o sea, imposible de testear. Sale a `shapes.ts` siguiendo el precedente de la casa (OnionMenu ya tiene sus engines puros `geometry.ts` / `color.ts` con test al lado). No es abstracción nueva: es mover código que ya estaba separado a un sitio donde un test pueda alcanzarlo. EL TEST QUE FALTABA `RING_SHAPES` es LA afirmación del componente — «ningún estado se distingue solo por el color» — y no la sostenía nada: los 353 de eidos son de contrato y no la tocan. Ahora hay 8 casos, y el central compara SILUETAS (raíl · dash · loop · halo, con el color deliberadamente ausente) y exige que ningún par de estados comparta la suya. Los demás fijan las decisiones de diseño que se tomaron con motivo: el raíl permanece en reposo (la presencia recede, nunca desaparece), `reviewing` está QUIETO mientras `acting` gira (es lo que los separa cuando el tinte solo los aleja ~22° de hue), `escalated` es el único con anillo cerrado y halo, los loops son solo los registrados, y el halo arranca en el borde del orbe para que el `ping` (2×) aterrice en el anillo. Verificado en negativo, como el guard: igualando `reviewing` a `acting` el test falla con el mensaje exacto — «acting is indistinguishable from reviewing without colour». GAPS DESFASADOS El README de soma listaba dos gaps YA HECHOS (el wrapper de eidos y la comparativa, de fases cerradas) y los dos READMEs seguían diciendo que la elicitación «se materializa con la superficie de conversación» — que es justo lo que la firma de ayer (§5b) descartó. Corregido, y de paso queda escrito POR QUÉ nunca se materializó, que no era falta de superficie: la escalada lleva una razón tipada pero NO la pregunta, y `resolveEscalation(runId, 'continue'|'cancel')` no tiene por dónde llevar una RESPUESTA. Hasta que existan esas dos costuras, `escalated` se puede mostrar pero no responder — con el punto 4 del contrato a11y firmado (elicitaciones descartables/posponibles) incumplido. Verificado: eidos 360/361 · el único fallo es `backdrop`, componente SIN TRACKEAR de otra sesión en curso (dos `#000` crudos en su recipe), ajeno · eidos-lint aura 0 inválidos / 0 class-hooks · check en los 73 preexistentes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
2 months ago |
|
|
03d8b31711 |
fix(accordion): el Item no es una fila de lista — hover que llena toda la fila
El `Accordion.Item` (un `<div>` contenedor del header + contenido) llevaba `archetype: 'item'`, y `archetypes.css` estiliza `[data-archetype='item']` como FILA de lista/menú: le mete `padding-block: 6px` y su PROPIO fondo de `:hover`. En un acordeón eso peleaba con el hover del Trigger: el fondo del item (fila entera) y el del trigger (más corto, por el padding de 6px) se apilaban en un hover desigual y bandeado que no cubría toda la fila. Un item de acordeón es un CONTENEDOR de disclosure, no una fila clicable — la parte interactiva es el Trigger (que ya tiene `archetype: 'trigger'` y su hover). Quitado el archetype del Item (regla: «omit archetype for display parts»). Ahora el item no arrastra padding ni hover de fila: el Trigger llena el item y su hover cubre toda la fila, sin banda doble. El disclosure no cambia: los eventos open/close siguen apuntando al Item por su kebab, no por el archetype; el `data-state` sigue emitiéndose. Verificado: en el FAQ el item pasa de 57px (con 6px de padding) a 45px (trigger + 1px de separador), sin `data-archetype`; la demo canon del Accordion intacta (abre/cierra, eventos en la traza). `svelte-check` sin errores · `vitest src/uix/morfo/components` 53/53. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
54b8b838e7 |
feat(blocks): F2.6 `faq` — proxy fino del Accordion del canon
La sección de preguntas: una cabecera sobre un acordeón de P/R en columna estrecha. Compound (`.Item` repite) y —la regla al envolver un componente interactivo— un PROXY FINO del `Accordion` del canon: el block lee su API y la pasa tal cual, no reinventa el disclosure. `<Faq>` + `.Header` + `.List` + `.Item`. - `.List` **ES** el `Accordion`: toda su API pasa sin gate — `type` (single/multiple), `bind:value`, `collapsible`, `variant`, `size`. Defaults de FAQ: single, collapsible (el abierto se puede cerrar), outline. - `.Item` proxya `Accordion.Item > Header > Trigger`(snippet `question`) / `Content`(children) y autogenera el `value` (clave de estado) con `$props.id()` si no se pasa — la única conveniencia sobre el andamiaje. - El teclado (flechas, Home/End, Enter/Espacio), `aria-expanded` y `aria-controls` salen del `Accordion`; el block no toca la a11y del disclosure. `Container` estrecho (`md`) para una columna legible. Demo (`web/routes/blocks/faq/`): full-bleed + ruta `preview`, cinco preguntas en una columna, con la cola «¿aún tienes dudas?» que la app pone tras `.List`. Huecos a decisión del usuario (Gaps): la **lista estática 2/3 columnas** (6 de 7 en TW NO son acordeón, sino P/R siempre abiertas) — otra disposición, prop `layout` o hermano. Verificado en navegador: el acordeón abre/cierra, claro/oscuro × LTR/RTL. `blocks:check` verde (7 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
b59332132f |
feat(blocks): F2.5 `testimonials` — rejilla de citas con autor
Prueba social: una cabecera sobre una rejilla responsive de tarjetas de cita, cada una con su autor (avatar · nombre · cargo). Compound —la `.Item` se repite, como feature-grid— y SIN contexto (las partes no coordinan). `<Testimonials>` + `.Header` + `.Items` + `.Item` + `.Quote` + `.Author`(slot `avatar` + `.AuthorName`/`.AuthorRole`). - `AutoGrid` de tarjetas de igual alto; `.Author` fijada al borde inferior de la tarjeta (`margin-block-start: auto`) para que una fila de citas de distinto largo alinee las caras. - La cara es del app: un `<Avatar>` con imagen o un `Avatar.Fallback` de iniciales va en el slot `avatar` (B-7). - La tarjeta del quote usa `variant="outline"` — el `soft neutral` es casi invisible en claro. Encontrado al componer (en el README del block): el tema activa solo un SUBCONJUNTO de escalas donor (`green/indigo/orange/plum/teal`); una escala no activada (`cyan/ruby/amber/jade`) en `color` cae en SILENCIO a `primary` (la regla `[data-color]` hace `var(--scale-…, primary)`). Config del tema, no bug de componente; la demo usa escalas activadas. Hueco a decisión del usuario (Gaps): la **cita única en spotlight** (grande, centrada, logo+avatar+autor) — la variante modal del dossier, el grid es minoría. Otra disposición → candidato a hermano `testimonial-spotlight`. Demo (`web/routes/blocks/testimonials/`): full-bleed + ruta `preview`, cinco citas con avatares de iniciales en cinco colores distintos. Verificado en claro/oscuro × LTR/RTL, caras alineadas al fondo. `blocks:check` verde (6 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
594e5a5db4 |
feat(blocks): F2.4 `pricing` — el primer compound CON CONTEXTO del tier
La sección de planes: un toggle de periodo sobre una fila de planes, uno destacado. Es el primer block cuyas partes se COORDINAN de verdad (no solo se repiten como en feature-grid/split): el `Switch` escribe el periodo de facturación y cada `PlanPrice` lo lee. Esa coordinación es lo que gana un contexto compartido — la forma más fuerte de compound. `<Pricing bind:period>` + `.Header` + `.Switch` + `.Plans` + `.Plan`(featured, badge) + `.PlanName`/`.PlanDescription`/`.PlanPrice`/`.PlanFeatures`/ `.PlanFeature`/`.PlanAction`. - **Contexto reactivo** (`context.ts`): la raíz provee el periodo como getter sobre un `$bindable`; el `Switch` (un `ToggleGroup`) lo escribe, el `PlanPrice` lo lee y muestra el snippet `monthly` o `annual`. Mismo patrón que `CardGroup`. `period` es bindable por si la app quiere observarlo. - **El block NUNCA formatea moneda**: la app compone `FormatNumber` dentro de los snippets de precio (B-7). El block posee el switch, no el dinero. - `.PlanAction` fija el CTA al borde inferior de la tarjeta (`margin-block-start: auto`) para que una fila de planes alinee sus botones aunque tengan distinto nº de features; `.Plan` con `align="start"` deja los checks en columna limpia; `featured` da acento (borde primary) + elevación. Demo (`web/routes/blocks/pricing/`): full-bleed + ruta `preview`, tres planes (Pro destacado en el centro) con el toggle mensual/anual vivo. Hueco a decisión del usuario (Gaps del block): la **tabla de comparación** (features × planes) — la brecha recurrente del dossier en pricing. Es una tabla, no una fila de tarjetas: candidato a hermano `pricing-table`. Presentado, no resuelto. Verificado en navegador: el toggle cambia los TRES precios a la vez (0/29/99 → 0/23/79), tarjetas de igual alto con CTAs alineados, featured con acento, en claro/oscuro × LTR/RTL. `blocks:check` verde (5 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
cd399f7c7c |
fix(eidos): el `align`/`justify`/`alignContent` de `Grid` sí aplican
`<Grid align="center">` no tenía efecto: `align-items` computaba `normal`. Los atajos `place-items`/`place-content` del recipe se emiten DESPUÉS de los longhands, y su fallback `revert-layer` —cuando el prop `placeItems`/ `placeContent` no está puesto— revertía el longhand anterior a su valor inicial, pisando en silencio lo que `align`/`justify`/`alignContent` escribían (un atajo posterior gana sobre el longhand que expande). Ahora el fallback de los atajos COMPONE los vars de los longhands en vez de revertir; si el prop del atajo sí está puesto, sigue ganando (va el último). Los grids por defecto no cambian (para ítems de grid, `align-items: normal` ≡ `stretch`). Encontrado al alinear las filas de `feature-split`. Verificado: `vitest src/uix/eidos` 353/353 · en el navegador, feature-split (filas centradas), el chart del Mockup (`align="end"` = barras a la base) y feature-grid (AutoGrid, sin cambio). Doctrina en theming/changelog §47. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
327cd26876 |
fix(blocks): feature-split — la media reversa vuelve a la fila de su copy
En las filas `reversed`, la media caía a una fila de rejilla NUEVA en vez de quedar al lado de la copy: con solo `grid-column`, el auto-placement disperso no rellena la columna 1 después de que la copy tomara la columna 2, así que el screenshot bajaba ~420px. Ahora ambas columnas se fijan a `grid-row: 1` en `md` (auto en `base`, donde apilan copy-primero), y `align-self: center` las centra vertical en la fila. Verificado: las 3 filas con la media a su lado en claro/oscuro × LTR/RTL × reversed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |