docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Eidos Theming — Changelog (dated sprint records)
type: notes
audience: human + agent
authority: chronicle — records of WHAT HAPPENED to the theming system, kept verbatim; the living reference is THEMING.md + the per-channel RFCs + the code
status: chronicle
source: extracted from THEMING.md ss13/ss20-ss38 (2026-07-02 docs reconciliation); relocated from src/uix/eidos/THEMING_CHANGELOG.md (docs-book F7.3)
---
# Eidos Theming — Changelog
Dated sprint records (corrections, incidents, commit references) extracted
verbatim from `THEMING.md` . Each section keeps its original `§N` number —
`THEMING.md` holds a numbered stub per section with the living decision and
the pointer here, so historical `§N` citations across the corpus resolve.
**Nothing here is the current API by itself**: where a section defined
doctrine that is still alive, the stub in THEMING.md says so and points at
the living source (RFC / config / generator).
---
## 13. Integración con Sema (`event:*` scope) — SUPERSEDED
> Original body of THEMING §13. Superseded by the two-moment motion model
docs(book): F7.3 (8/9) — eidos-motion translated to docs/theming/motion.md
src/uix/eidos/eidos-motion.md (698 L, Spanish) translated to English, same
s1-s19 numbering: the two-moment thesis, the closed cascade model
(D.11-D.13), motion across the 4 layers, the two-surface registry, types,
the EngineMotion API + cleanup policy, the 5 drivers, the DOM contract
(data-animation-style), Presence integration, reduced motion, primitives/
keyframes, built-in content, per-component defaults, code map, s15 (the
events.css -> signatures migration — cited from events.css), Chakra
comparison, naming, phases F1-F7, deferred. Stub with the full s-map at
the old path; corpus links swept (comparison, eidos chapter, changelog,
motion-guide). docs:check 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> ([`eidos-motion.md`](./motion.md)); kept as decision context.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Sema emite `data-event-*` durante hold windows perceptuales. Eidos
reacciona vía `events.css` (animations) o vía tokens scoped a `event:*` .
### Tokens scoped a `event:*`
Permite que un token cambie SU VALOR durante una señal:
```ts
recipes.toast = {
// Color base — scope 'host'
'bg': {
value: 'var(--color-surface-raised)',
scope: 'host'
},
// Override durante señal de announce — el toast cambia su bg
// mientras dura la señal perceptual
'bg-during-announce': {
value: 'var(--color-primary-element)',
scope: 'event:announce'
}
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
};
```
CSS generado:
```css
[data-toast] {
--toast-bg: var(--color-surface-raised);
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
[data-toast][data-event='announce'] {
--toast-bg-during-announce: var(--color-primary-element);
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
```
El recipe usa el token apropiado:
```css
[data-toast] {
background: var(--toast-bg);
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
[data-toast][data-event='announce'] {
background: var(--toast-bg-during-announce);
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
```
### Por qué NO usar `data-motion-ref`
`eidos-motion.md` propuso un atributo nuevo `data-motion-ref` y un
registry separado. **TSC absorbe esa necesidad** sin nueva superficie
DOM: el scope `event:*` se materializa contra `data-event='X'` que
sema ya emite.
### Reduced motion
Eidos lee `data-motion` (la pref global proyectada por `ActivePrefs` ):
```css
[data-motion='reduce'] [data-event][data-event-phase='active'] {
animation-duration: 1ms;
transition-duration: 1ms;
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
```
Cobertura per-event vive en `events.css` . Cobertura per-token
(durante señal) puede vivir como composite scope `[event:X, motion:reduce]`
si necesitas afinar.
---
## 20. Correcciones del engine de theming (2026-06-01)
Dos bugs del engine de theming detectados al construir el tema
`untitled-ui` (`web/routes/temas/untitled-ui`) y corregidos **a nivel
engine** (no parcheados en el theme), de modo que aplican a todos los
themes y consumidores.
### 20.1 — Densidad inerte (`data-density` no hacía nada)
**Síntoma**: cambiar `data-density` entre `compact` / `comfortable` /
`spacious` no movía nada en pantalla. El sistema de densidad parecía
muerto.
**Causa**: el generador emitía los escalares de densidad
(`--density-scale`, `--density-space-scale` , `--density-control-scale` ,
`--density-content-scale` ) y los redeclaraba por `[data-density='…']` ,
**pero las primitivas `--space-*` y `--control-height-*` eran px fijos
que nunca los consumían**. Los escalares existían y cambiaban, pero
ningún token los usaba → cero efecto visible.
**Fix** (`lib/render-css.ts`): nuevo helper
`appendDensityScaledDeclarations` que emite `--space-{n}` y
`--control-height-{k}` como `calc(<valor> * var(--density-{space|control}-scale))` .
El valor cero se emite tal cual (`0px`). A `comfortable` el escalar es
`1` , así que el resultado es idéntico al valor crudo — **cero regresión**
para quien nunca cambia de densidad. Las primitivas de tamaño
(`--size-{k}-*`) y el padding de los recipes heredan el escalado porque
referencian `var(--space-*)` / `var(--control-height-*)` .
Resultado (verificado): a `compact` el espaciado y las alturas se
reducen (× 0.84 / × 0.90), a `spacious` crecen (× 1.16 / × 1.12).
> **Nota**: solo se escalan `space` y `control-height` (los dos ejes
> con escalar dedicado y mapeo claro). La tipografía NO se escala con
> densidad — igual que Radix Themes / Untitled UI, la densidad afecta
> a ritmo y altura de controles, no al cuerpo de texto. **El zoom global
> que SÍ escala la tipografía es un eje aparte (`data-scaling`) — ver §23.**
>
> **Actualización (eje de scaling)**: los escalares `--density-scale` y
> `--density-content-scale` que el generador emitía originalmente fueron
> **eliminados** al introducir el eje `scaling` (§23). La densidad hoy
> emite solo `--density-space-scale` y `--density-control-scale`; el helper
> se generalizó a `appendScaledMetricDeclarations`, que compone
> `calc(<raw> * var(--density-…-scale) * var(--scaling))` — densidad y
> scaling se multiplican.
### 20.2 — `contrast` ilegible sobre sólidos
**Síntoma**: el texto de los botones / badges / banners / cards de
variante `solid` salía oscuro sobre un fondo saturado oscuro
(p. ej. botón primario del base: texto `purple-12` `#402060` sobre
`purple-9` `#8e4ec6` ≈ 2:1, ilegible).
**Causa**: el slot de color `contrast` mapeaba por defecto al **step 12**
("texto de alto contraste", pensado para fondos CLAROS), y los recipes
usan `--color-{role}-contrast` como **color de texto SOBRE el sólido**
(step 9). Step 12 sobre step 9 = oscuro-sobre-oscuro.
**Fix** (`lib/render-css.ts`, loop de slots en `renderThemeCss` ): el slot
`contrast` , **cuando usa el valor por defecto** , ahora resuelve a
`var(--color-content-on-solid, var(--primitive-{role}-12))` — el color
on-solid del theme (blanco), con el step 12 como fallback. Un **override
explícito** del slot (`roles: { x: { scale, slots: { contrast: '1' } } }`)
se respeta verbatim, así que roles monocromos que invierten su texto
(p. ej. un primario carbón que apunta `contrast` al step 1) siguen
funcionando.
`--color-{role}-contrast` se consume **exclusivamente** como fg sobre
sólidos (button / badge / banner / card / calendar-range / color-picker
ring) — verificado por grep — así que el cambio es seguro y no afecta a
ningún uso de "texto oscuro sobre fondo claro" (ese es el slot `text` ,
step 11).
### Verificación
- `npx vitest run src/uix/eidos` : sin regresión — las únicas fallas son
3 pre-existentes (`words` huérfanos + wrappers, track aparte),
confirmadas con baseline (`git stash` del cambio). El test
`active-eidos-config` se actualizó para asertar la nueva forma
density-aware de `--space-4` / `--control-height-xxs` .
- `npm run generate:eidos-css` regenerado (la densidad vive en el CSS
estático precompilado; el `contrast` vive en el bloque de tema runtime).
---
## 21. Modelo de color de dos niveles (RFC — RESUELTO en §25)
> **Resuelto (2026-06-02).** El modelo de color quedó decidido — ver **§25**.
> Se adoptó "paleta rica + capa semántica de alias / auto-derivación" y se
> **descartó** "intent = ancla de un solo color" (Radix no lo hace, y con una
> paleta rica el problema que motivaba el ancla desaparece). Lo de abajo se
> conserva como registro histórico de la propuesta original.
Tras el sprint de theming surgió una observación de fondo (comparando con
Radix Themes): hoy **cada rol de color exige una escala de 12 pasos** , incluidos
los 5 intents evaluativos (`affirm` / `fulfill` / `risk` / `threat` / `loss` ).
Eso obliga a autorar ramps a mano para hues fuera de la librería base (12
escalas) y es propenso a error — un intent es conceptualmente **un color** , no
un ramp interactivo.
La propuesta (dos niveles: accents/neutral ricos + intents de **un solo color
ancla** con slots derivados por `color-mix()` , más ampliar la librería hacia
paridad Radix) está documentada como RFC en
[`COLOR_MODEL_RFC.md` ](../../src/uix/eidos/COLOR_MODEL_RFC.md ). _(Estado original: propuesta.
**Resuelto en §25** — se adoptó paleta rica + alias / auto-derivación y se
descartó el ancla de un solo color.)_
---
## 22. Mejoras pendientes del theming
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Auditoría completa 2026-06-01**: `THEMING_AUDIT_2026-06-01.md` (informe
> retirado del árbol; su crónica sobrevive en este changelog)
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> — informe priorizado (P0– P3) en 6 frentes. Incluye defectos reales verificados
> (tokens de foundation inexistentes, `neutral` ilegible en dark, alpha scales
> fabricadas, tokens de densidad muertos, huecos de tests) más todo lo de abajo.
Backlog vivo de mejoras al sistema. Ordenado por impacto, no por prioridad.
1. ✅ **Modelo de color — paleta + roles/intents derivados** _(mayor · resuelto 2026-06-02)_
— adoptado el modelo Radix-style: **paleta** de 33 escalas (diseñable por el
tema) + **roles de jerarquía** como alias explícito + **intents auto-derivados**
por convención del libro (identidad = step 9). Se **descartó** el "intent =
ancla de un solo color". Modelo completo en §25 /
[`COLOR_MODEL_RFC.md` ](../../src/uix/eidos/COLOR_MODEL_RFC.md ).
2. ✅ **Variant `surface`/`soft` vía alpha en vez de tinte opaco** _(resuelto 2026-06-02)_
— el tinte `soft` por rol (Button + Badge `{role}-soft-bg` ) se computaba
**opaco** (step-1 `track` + `color-mix` opaco en hover) → no componía sobre
fondos no uniformes. **Resuelto** con tokens derivados `--color-{role}-surface`
(= `--primitive-{role}-a2` ) + `--color-{role}-surface-hover` (= `a3` ),
translúcidos por construcción. Ver §24.2.
---
## 23. Eje de `scaling` (zoom global) — 2026-06-02
Eje **independiente** de la densidad, en paridad con el `scaling` de
Radix Themes. Diseño completo en [`SCALING_RFC.md` ](../../src/uix/eidos/SCALING_RFC.md ).
### 23.1 — Qué es y en qué se diferencia de la densidad
Son **dos ejes ortogonales** que se multiplican:
| Eje | Atributo | Qué mueve | Tipografía |
| --- | --- | --- | --- |
| **Densidad** | `data-density` (`compact` / `comfortable` / `spacious` ) | ritmo de layout (`space`) + altura de controles (`control-height`) | **NO** — el cuerpo de texto queda fijo |
| **Scaling** | `data-scaling` (`90` / `95` / `100` / `105` / `110` ) | **zoom global** : `space` + `control-height` + `font-size` + `icon-size` | **SÍ** — escala el cuerpo de texto |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Densidad = "más/menos aire entre cosas, controles más bajos, mismo
texto". Scaling = "agranda/encoge **todo** proporcionalmente", igual que
el zoom del navegador pero acotado al subárbol del tema. Concep tualmente:
densidad es una decisión de **diseño** (compacto vs holgado); scaling es
una decisión de **accesibilidad / preferencia de tamaño** del usuario.
### 23.2 — Qué escala y qué NO
`--scaling` (default `var(--scaling-100)` = `1` ) multiplica **solo
métricas en px** cuyo crecimiento proporcional es correcto:
- ✅ `--space-{n}` , `--control-height-{k}` (también llevan el escalar de densidad)
- ✅ `--font-size-{name}` , `--icon-size-{k}`
**NO** escala (a propósito):
- ❌ `line-height` — es un **ratio sin unidad** ; escalar el `font-size`
ya escala el interlineado real.
- ❌ `--radius-*` , `--border-*` , sombras — un zoom de UI **no** engorda
bordes ni radios proporcionalmente (Radix tampoco lo hace); mantenerlos
fijos conserva la nitidez del chrome.
### 23.3 — Generación + proyección
`lib/render-css.ts` :
- `appendScalingDeclarations` emite las constantes `--scaling-{90..110}`
(`STATIC_SCALING` en `lib/primitives/static.ts` ) + `--scaling: var(--scaling-100)`
en `:root` .
- `appendScaledMetricDeclarations(declarations, prefix, record, densityScaleVar?)`
envuelve cada métrica en `calc(<raw>[ * var(--density-…-scale)] * var(--scaling))` .
El valor cero se emite tal cual. `space` y `control-height` pasan el
`densityScaleVar` ; `font-size` e `icon-size` no (no dependen de densidad).
- `renderScalingBlocks` emite `[data-scaling='90'] { --scaling: var(--scaling-90); }`
… para los niveles ≠ `100` . Como todas las métricas leen `var(--scaling)` ,
reescribir esa única variable reproyecta el subárbol entero — **cero
redeclaración por token**.
A `100` el escalar es `1` → idéntico al valor crudo, **cero regresión**
para quien no toca scaling.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización (2026-07-06) — el eje se define por participación
> declarada, elemento a elemento.** Decisión de diseño (usuario): qué
> familias multiplica el zoom deja de estar implícito en los emitters y pasa
> a ser **dato del modelo** — `ScalingParticipationMap` (`config-types.ts`)
> con defaults canónicos en `STATIC_SCALING_PARTICIPATION`
> (`primitives/static.ts`); el generador deriva TODA la emisión del mapa.
> Taxonomía canónica: **métricas escalan** (space · control-height ·
> font-size · icon-size · blur) y **chrome queda nítido** (radius ·
> border-width · shadow). Con esto se reconcilia una deriva sin registro: el
> commit `845d6579` (2026-06-22) hizo el radio "scaling-responsive" contra
> el RFC sin tocar ningún doc — el radio vuelve a NO escalar (conserva su
> knob propio de magnitud, `--radius-factor`). Se corrige además la cita
> falsa del RFC: **Radix Themes SÍ escala su radio**; la nitidez es decisión
> propia. Un config puede desviar la participación por definición del
> sistema (mapa parcial mergeado sobre el canon → regeneración), nunca como
> knob runtime por tema: la semántica de los ejes es canon; los temas
> retunean magnitudes, no significados.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### 23.4 — API (`ActiveEidos`)
Simétrica a `density` :
```ts
createActiveEidos({
scaling: '110', // estático
// o reactivo:
scalingSource: { get: () => prefs.scaling, onChange: (fn) => prefs.subscribe(fn) }
})
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
```
`ActiveEidos` escribe `data-scaling` en el target junto a `data-theme` /
`data-mode` / `data-density` , y lo limpia en `dispose()` . La preferencia
viaja por `ActiveEidosPreferenceSource.getScaling()` ; `DEFAULT_SCALING`
es `'100'` .
---
## 24. Correcciones P2 del engine (2026-06-02)
fix(cleanroom): F2+F3+F4-C+SEM-4s1 — lote mecánico, censos con guard, corpus documental y el close polimórfico de los pickers VIVO
F2 — lote mecánico (13 ítems):
- DEP-2 ogl eliminado (0 imports) · DEP-1 clsx inlineado como toClassString
propio + suite de contrato (props.test.ts; soma.md §12 cerrado).
- THM-7: los 5 selectores manuales de sema.md reescritos con semaSelector
(los ejemplos [data-toast-root] apuntaban a un part INEXISTENTE — la
deriva que el builder previene, demostrada en el propio doc).
- MOR-1 escape isomorfo + validación de attr-names en semaSelector + 9
tests (selectors.test.ts, matches() real con comillas/corchetes) ·
MOR-2 partMarkerAttr = única fuente compilador↔builder + test de paridad ·
MOR-3 _resetCompileCache borrado (0 usos).
- SOM-2 keydown continue en match sin handler + keyboardFixtureMorfo ·
SOM-1 no-await de handlers (censo async = 0; contrato V1 cumplido) + pin.
- SEM-2 trigger pre-attacha catch con logger (void trigger sin unhandled
rejection; throw intacto para awaiters) + pin · SEM-3 fallback muerto de
applyDominance → skip defensivo + timer tope de awaitExpression cancelado ·
SEC-1 adjudicado YA implementado (assertCssVariableValue desde 2026-05-11)
+ pin del path de VALOR.
- accordion → outline (§32; su outline:none dejaba CERO anillo en HCM) —
verificado en vivo · THM-6 radius-full 9999px · EID-4 recuentos 33.
F3 — censos con guard:
- SOM-3 cerrado: announcer + image-provider migrados a scheduler-preferred
(consumidores cableados: date/time-field vía soma.uix.timers; avatar/image
vía eidos.timers — verificado en vivo); guard de timers ENSANCHADO de
soma/components a TODO soma y pasado a EVIDENCIA (setTimeout exige
.schedule( en el fichero — layers/ y datetime/ escapaban del ámbito viejo).
- THM-5: R-4.7 nueva (válvula same-line /* important: <razón> */, escaneo
comment-blanked) + las 15 declaraciones anotadas con su razón + canon
recipe-contract §3/§4.
- SOM-4 adjudicado: el censo/guard YA existían (49 pins); knob/mask-field/
timeline pinneados (overrides documentados en call-site); media-player
Batch-4 (35 hits, cero renderProps) = único batch restante, registrado.
- THM-4 doctrinado en eidos.md §unused (comportamiento/composición =
legítimo; deuda = eje visual sin consumidor; hotspots por lotes).
F4-C — corpus documental (decisiones de usuario aplicadas):
- DOC-3: los 15 enlaces muertos resueltos (repoint a la edición FINAL
trackeada / des-link históricos) · docs:check I6-links WARN→ERROR.
- DOC-1: tabla «Build contract» MIGRADA a component-guide con estados
modernizados (A3–A5 → LIVE + guards de hoy); banners reapuntados; citas
de CANON/sema.md historificadas; lápida-redirect en el §13 del fósil.
- DOC-4: hold chain → holds.ts · FAQ event:* SUPERSEDED por signatures ·
gradient añadido a los DOS capstones (sextet real) · nota de paleta de
demo-authoring corregida (universalPaletteDecls + decisión THM-2 =
mecanismo universal como sucesor del tracker borrado).
- DOC-5/6: recuentos anti-frágiles datados · §4.11 dup → §4.12 · Known gaps
historificado · N-6/N-7 recuperadas de git (d68d2c45^) y canonizadas en
eidos.md §pickers · authoring E2 → canon/tsc.md · air-old des-linkado ·
EID-3 (placement) en la fila RTL · AUX-2 disabledDom documentado.
SEM-4 sesión 1 — el close polimórfico de los pickers, VIVO (D.11):
- Reconciliación: los morfos ya no declaran close (delegated al Popover,
de-dialoged 06-27); el agujero real era el cierre programático bypaseando
dismissWith → save/cancel/select eran perceptualmente SILENCIOSOS.
- Fix: PickerShellHandle.setPopoverDismiss + closeWith(cause) en los 5
providers (14 sitios; select/commit → 'save' = commit.save+fulfill,
cancel → 'cancel' = emerge; fallback raw para headless) + UN inyector en
el eidos PickerShell root (norma N-8). Picker genérico fuera a propósito
(ya suena commit-set/cancel por diseño S9).
- Verificado en vivo (date-picker): Done → close·commit·fulfill·active ·
Cancel → close·emerge · cierre real.
Gates: matriz 141/141 (los 6 morfos nuevos de la pista de texto paralela
también PASS) · contracts 38/38 · eidos 314 · sema 178 · morfo 94 ·
docs:check 0/0 con I6 en error · baseline propio 57.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Dos defectos de calidad de la auditoría (`THEMING_AUDIT_2026-06-01.md` P2-2,
P2-4 — informe retirado del árbol), corregidos **a nivel engine** para que
apliquen a todos los temas.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### 24.1 — Texto on-solid ilegible sobre sólidos claros (P2-2)
**Síntoma**: el texto de los botones / badges `solid` de roles con sólido
**claro** (amarillo, ámbar, `risk` =naranja) salía **blanco sobre claro** —
naranja-9 con blanco ≈ 2.3:1, sub-AA.
**Causa**: el slot `contrast` (color del texto SOBRE el sólido) resolvía por
defecto a `--color-content-on-solid` (blanco) para **todos** los roles. Correcto
para sólidos oscuros (purple, red), ilegible para sólidos claros.
**Fix** (`render-css.ts`): pick por **luminancia** . En generación, el engine
calcula la ratio de contraste WCAG (gamma-linealizada, `wcagContrastRatio` )
entre `onSolid` y el **step-9** del rol. Si `onSolid` falla (< 3:1 ), el slot
resuelve a `--color-content-on-solid-contrast` (un oscuro, nuevo semantic
**opcional** `content.onSolidContrast` , `#1c1917` en base) en vez de blanco.
```
risk (orange #f76b15 ) → texto #1c1917 = 5.89:1 ✓ (era ~2.3:1 con blanco)
primary (purple) → texto #fff = 5.18:1 ✓ (se mantiene)
threat (red) → texto #fff = 3.91:1 ✓ (convención, ≥3:1)
```
Solo `risk` volcó a oscuro en el tema base; el resto mantiene blanco. Un
override explícito `slots.contrast` se respeta verbatim (p. ej. `neutral`
sigue en step-12). El umbral 3:1 es el mínimo AA para UI / texto grande —
ancla principista, no número mágico.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización 2026-07-06 (auditoría A.7) — un criterio, dos caminos.**
> Este fix solo cubría el camino de ROLES; la cascada per-instance
> `palette-contrast` (`color="orange"` como escala cruda) usaba una **lista a
> mano** (`LIGHT_SOLID_SCALES`, 6 escalas) que había derivado del criterio:
> `orange` embarcaba tinta blanca a **2.97:1 (sub-AA)** y `cyan` a Lc 59.5,
> mientras el rol `risk` (el mismo hex naranja) computaba y volcaba. Decisión
> de usuario: el criterio se materializa como módulo puro
> `eidos/lib/on-solid.ts` (la `pickOnSolid` que rfc-color-engine §6.1
> nombraba) — **blanco-preferente salvo que falle AMBOS suelos** (APCA
> |Lc| ≥ 60 Y WCAG ≥ 3:1) — y lo consumen AMBOS caminos: el bucle de roles y
> la cascada, cuyo set de volcado ahora es `computeLightSolidScales(config)`
> (computado por tema; unánime → esa respuesta; discrepancia → gana el tema
> `*-light` y es el disparador documentado de emisión per-theme). Set medido
> del tema base: {cyan, yellow, amber, orange, sky, mint, lime, gold} — las
> instancias `orange`/`cyan` cambian a tinta oscura (`#1c1917`); fix de
> accesibilidad, no retune estético. Guardas: test de paridad rol↔instancia
> por escala donante + pin del set computado
> (`active-eidos-config.test.ts`). El criterio "elige el |Lc| mayor" del
> borrador del RFC quedó **rechazado** al canonizar (volcaría media paleta:
> teal 60.5, grass 60.2, jade/green/bronze 61.7, blue 62.6, grises 63– 64).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### 24.2 — Superficies tintadas opacas → translúcidas vía alpha (P2-4)
**Síntoma**: el fondo de la variante `soft` por rol (Button + Badge) era
**opaco** → al superponerse sobre fondos no uniformes (filas a rayas, imágenes,
gradientes) tapaba el fondo en vez de teñirlo.
**Causa**: `{role}-soft-bg` = `var(--color-{role}-track)` (step-1, opaco) y el
hover un `color-mix` opaco.
**Fix**: nuevos tokens de rol derivados, translúcidos por construcción (usan el
alpha compositing-inverse §P1-1, consistente con el sólido):
```
--color-{role}-surface = var(--primitive-{role}-a2) /* soft bg */
--color-{role}-surface-hover = var(--primitive-{role}-a3) /* soft bg hover */
```
Button y Badge `soft` consumen esos tokens. Sobre la superficie por defecto se
ven casi idénticos (a2 ≈ el step-1 anterior); sobre fondos no uniformes ahora
**componen** correctamente.
> **Toast y Tabs NO se tocaron** — aunque la auditoría los listó, son
> **tarjetas**: el toast tiene fondo neutral opaco y la tab-list un
> `surface-default` ya translúcido. La opacidad ahí es correcta por diseño (no
> quieres ver el contenido de la página a través de un toast). La fórmula opaca
> que §22 documentaba mal era la de `soft-bg-hover` de Button, ya migrada.
---
## 25. Modelo de color — paleta + roles/intents derivados (2026-06-02)
Decisiones **cerradas** sobre el modelo de color. Resuelve el RFC §21. Es, 1:1,
el modelo de **Radix Themes** : una **paleta** de escalas + una **capa semántica**
de alias + **override por componente** . Lo único propio es que los **intents**
(capa del libro) **auto-derivan** de la paleta por convención.
### 25.1 — Las tres capas
| Capa | Qué es | Cómo se define |
| --- | --- | --- |
| **Paleta** | librería de escalas de 12 pasos | `--scale-{name}-{step}` (+ alpha `--scale-{name}-a{step}` ) · directamente usable · **diseñable por el tema** |
| **Roles** (jerarquía) | `primary` · `secondary` · `tertiary` | **alias explícito** a una escala (decisión de marca · obligatorio) |
| **Intents** | `neutral` + `affirm` /`fulfill`/`risk`/`threat`/`loss` | **auto-derivados** de la paleta por convención del libro · identidad = step 9 · slots derivan normal · override opcional |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Los componentes consumen la capa semántica (`--color-{role}-{slot}`) y pueden
**override** su color a cualquier escala vía la prop `color` / `data-color` .
### 25.2 — Paleta (la fuente, diseñable)
- Escalas **funcionales** de 12 pasos: `1-2` fondos · `3-5` componente · `6-8`
bordes · ** `9` sólido** · `10` hover · `11-12` texto. El **representativo** de
una escala es el **step 9** (el sólido), NO el medio geométrico (step 6, que es
un tono de borde lavado). Cada rol expone **13 slots** desde esos 12 pasos —
`bg2·2` (2.º nivel de fondo), `separator·6` (divisor sutil), `border-hover·8` y
`text-strong·12` re-exponen steps que el contrato inicial de 9 había tirado;
eran necesidades reales de UI/a11y (ver §25.2 · "13 slots").
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización (retirada fechada ~2026-07-01; nota añadida 2026-07-07,
> auditoría C)**: `border-hover·8` se **retiró** con cero consumidores —
> el modelo vigente son **12 slots** (`COLOR_ROLE_SLOTS`,
> `lib/config-types.ts`; rfc-color-engine §3). Esta entrada queda como
> crónica del momento en que fueron 13.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **Directamente usable**: cualquier paso es `var(--scale-{name}-{step})`
(p. ej. `var(--scale-green-10)` ). **No** existe alias corto `--{name}-{step}` :
dos formas para el mismo valor crearían ambigüedad sobre cuál es la canónica.
- **Diseñable**: la paleta la trae el tema (dominio del diseñador). El framework
envía una paleta por defecto de **33 escalas** (en `lib/themes/color-scales.ts`
+ `base.ts` ). Muchas se sembraron desde Radix Colors —un buen punto de partida—
pero **la paleta es NUESTRA, sin perseguir paridad con nadie** . Un tema la
reemplaza/amplía; un color de marca se añade como **una escala** (autorada o
generada), nunca como un valor inline suelto.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
#### Regla de pertenencia — por qué 33 y no un número mágico
El tamaño de la paleta **no es un tope fijo** (el viejo "32 y punto" era un proxy
barato de "no metas relleno"). Una familia se gana su slot solo si cruza las
**tres puertas** — así el criterio escala sin depender de un número:
1. **Hueco perceptual real** — rellena un vacío en el plano **croma-hue**
(distancia ΔE Oklab entre familias vecinas en hue, **NO grados crudos** : un
hueco de 36° en la banda azul de bajo croma pesa perceptualmente **menos** que
uno de 24° entre magentas saturados, porque a bajo croma los puntos están más
cerca del origen a-b).
2. **Nombre + demanda** — es un color que la gente pide **con nombre propio**
(marca, gráficas), no una transición sin nombre.
3. **No confusable** — no está **más apretada que el suelo del set enviado**
(~0.023 ΔE Oklab en el sólido). El "≥ 0.04 absoluto" sería mentira: la propia
paleta enviada tiene **7 pares por debajo de 0.04** en el step 9 — pares
Radix-canónicos distintos-pero-cercanos que **aceptamos** (_grandfathered_):
`teal/jade` (0.024, el más ajustado), `green/grass` , `violet/iris` , `red/ruby` ,
`red/tomato` , `green/jade` , `ruby/crimson` . La puerta prohíbe **empeorar** ese
suelo, no alcanzar un ideal que el set nunca cumplió.
`fuchsia` (~H334, `#cf28bb` ) cruza las tres limpio: hueco real (plum→pink, el
3.er mayor del plano a-b), nombre fuerte + muy pedida, y ΔE **0.042** a su vecina
más cercana (plum) — holgado sobre el suelo. Por eso entra; el 33 es
**consecuencia** de la regla, no al revés. Descartadas por fallar alguna puerta:
`cerulean` /`azure` (hueco modesto una vez ponderado + nombre débil en la banda
azul, que "resiste nombres"), `chartreuse` (pega con `lime` ).
#### La invariante se verifica en la SALIDA, no en la entrada
La puerta 3 vale lo que el generador más flojo. Hay **tres** generadores —
autorado, `generatePalette` (paramétrico, `lib/generate-palette.ts` ) y
`deriveScheme` (M3 runtime, `$color` )— y cualquiera puede escupir dos familias
confusables sin que una puerta de *entrada* lo frene (medido: `generatePalette` a
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`tone +0.12` funde `yellow` = `lime` , ΔE **0.0** ). Por eso la garantía es un **test
sobre la salida** — [`lib/palette-invariant.test.ts` ](../../src/uix/eidos/lib/palette-invariant.test.ts ) — que:
- corre sobre **los tres** generadores;
- usa dos suelos honestos: **sólido** (step 9) ≥ 0.02 (el confusable que importa,
el acento que usan los componentes) e **idéntico** ≥ 0.002 en cualquier step;
- chequea **varios steps** (3 · 9 · 11): tints y texto colisionan *peor* que el
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
sólido (`green`↔`jade` cae a **0.003** en el step 3), así que medir solo el 9
subestima la confusabilidad;
- **exceptúa `monochrome` **: colapsar la jerarquía a una tinta es intencional
(diferencia por tono + énfasis, no por hue), no un defecto.
El builder clampa el `tono` a ** ≤ +0.08** justo para no entrar en la zona donde las
familias claras blanquean hacia el techo de gamut y se funden.
#### Jerarquía e intent nunca comparten escala (guard G1)
Un alias de jerarquía (`primary`/`secondary`/`tertiary`) y un intent
**valenciado** (`affirm`/`fulfill`/`risk`/`threat`/`loss`) no pueden resolver a la
**misma escala**: sería un hue con dos significados opuestos — "acento de marca" y,
p. ej., "pérdida". El modelo no lo impedía por sí solo, así que
`completeColorRoleMap` (`lib/config-types.ts`) lo **valida y lanza** — una config de
tema colisionante es un error y debe fallar alto, no recolorear semántica en
silencio. `neutral` está **exento** (es el intent no-valenciado; comparte el gris
legítimamente con el chrome neutro). En el builder, el picker de jerarquía
**excluye** las escalas que un intent ya ocupa — el mismo candado a nivel de UX.
### 25.3 — Roles de jerarquía (alias explícito)
`primary` / `secondary` / `tertiary` son decisiones de marca sin color canónico:
el tema **DEBE** mapearlos a una escala de la paleta. Pueden llevar override de
slots (p. ej. un primario monocromo con `slots: { contrast: '1' }` ).
### 25.4 — Intents auto-derivados (convención del libro)
- Los 6 intents tienen color canónico definido en el libro *Diseñando lo que
ocurre*. La convención `INTENT → escala` vive en `CANONICAL_INTENT_SCALES`
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
(`lib/config-types.ts`):
`neutral→gray · affirm→teal · fulfill→green · risk→amber · threat→red · loss→plum` .
- Un intent **omitido** del mapa de roles **auto-deriva** de la paleta por esa
convención (`completeColorRoleMap`, consumido por `render-css` y la validación).
Su **identidad es el sólido (step 9)** ; los 13 slots derivan normal. La paleta
debe proveer esas escalas (o el tema overridea el intent mapeándolo explícito).
- **Tipos**: en `ColorRoleMap` la jerarquía es **obligatoria** y los intents
**opcionales** — `Record<HierarchyColorRole, V> & Partial<Record<Intent, V>>` .
- **`neutral`** es el 6º intent pero **sin valencia** : funciona como gris de
superficies/bordes/texto, por eso auto-deriva a una escala gris (no es una
señal valenced). Las 5 valenced llevan la carga.
- Doctrina: **el color EXPRESA el intent, no lo define** — la valencia/activación
la lleva la capa **sema** (sonido/haptic/motion); el color solo aporta la
identidad de hue.
#### Intents nunca solo por color (CVD / WCAG 1.4.1 · guard G3)
El color es **un** canal, no el único. WCAG 1.4.1: el significado que comunica el
color debe comunicarse **también** por un canal **no cromático** — icono, forma,
texto/etiqueta o ARIA (`role`/live-region). No es opcional: los dos pares
valenciados **colapsan** en daltonismo — `affirm` (teal)/`fulfill`(green) son dos
verdes, y `risk` (amber)/`threat`(red) se confunden en protanopia/deuteranopia. Es
la cara concreta de "el color EXPRESA el intent, no lo define".
**`affirm` ≠ `fulfill` ** (no intercambiables): `affirm` = positivo de **baja
activación** (confirmación suave — checkbox marcado, toggle on, "guardado");
`fulfill` = positivo de **alta activación** (objetivo cumplido — proceso/tarea
completada). Por eso Toggle/Checkbox/Radio/Select solo exponen `affirm` ; Button y
las superficies de estado exponen ambos.
**Estado (auditado 2026-07-01)** — la mayoría cumple **por diseño** : Toast (icono
por intent), `Metrics.Delta` (flecha de tendencia), Field (`role=alert` +
live-region), Switch (posición del thumb), Checkbox/Radio (icono), Form
(ErrorSummary con texto), PasswordField (etiqueta de fuerza), Stepper
(forma + número). **Regla** : un componente que señaliza estado evaluativo **debe**
enviar su cue no-cromático **por defecto** , como éstos — no delegarlo al app.
- **Hueco a cerrar** — `Meter` : la zona (`below`/`optimum`/`above`) cambia **solo
por color** (`valueText` es opcional). Debe enviar por defecto un cue de
forma/icono por zona.
- **Acciones con intent** (Button · IconButton · SplitButton · Fab · ToggleGroup):
el significado lo lleva la **etiqueta** de la acción ("Borrar"); el intent tinta
como **refuerzo** — no es violación mientras haya etiqueta. La regla: un control
con intent evaluativo **nunca icon-only sin `aria-label`** , y su glifo/etiqueta
porta el significado, no solo el hue.
- **No confundir con afordancia**: el `color` de **foco/selección** (fields,
table, calendar, listbox…) es branding, **no** estado evaluativo — no cae bajo
esta regla (el foco ya lo marca el outline; la selección, `aria-selected` ).
**Enforcement**: la composición es runtime, así que un lint estático no prueba que
cada instancia lleve su cue. La garantía es doctrinal + el default de cada
componente de estado. Un dev-warning opt-in sobre `[data-intent]` sin cue
reconocible queda como trabajo futuro.
### 25.5 — Override por componente
Cualquier componente acepta `color="..."` (cualquier escala de la paleta) → la
cascada `_accent-*` del recipe remapea sus tokens a esa escala para esa
instancia. Equivalente a `<Button color="grass">` de Radix.
### 25.6 — Por qué se DESCARTÓ el "ancla por rol"
El RFC §21 proponía declarar un intent como un solo hex (`{ anchor }`) y derivar
los slots inline con `color-mix()` . Se **descartó** : Radix no lo hace (genera una
*escala* desde un hex y la aliasa), y con una **paleta rica** el problema que lo
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
motivaba (autorar 12 pasos a mano para `loss` → el bug de loss=azul) **desaparece
solo**: `loss` simplemente aliasa la escala `plum` , que ya existe en la paleta.
El modelo final es **paleta rica + alias / auto-derivación** , no ancla.
### 25.7 — Framework vs tema
- **Framework**: envía la paleta por defecto (33 escalas) — para el tema
base y para quien no traiga la suya.
- **Tema de marca** (p. ej. Grafito): trae **su propia paleta** + mapea la
jerarquía; los intents auto-derivan. El framework **no persigue paridad con
ninguna librería** — la paleta base es un punto de partida, no un contrato.
---
## 26. Theme builder en runtime — `eidos.applyColorScheme` (2026-06-04)
El RFC §6.2 (un seed → todo el sistema) está **implementado** como API de primera
clase. Un app re-tematiza desde UN color de marca con una llamada, sin tocar el CSS:
```ts
const result = eidos.applyColorScheme('#8e4ec6', {
variant: 'tonal', // 'tonal' | 'vibrant' | 'monochrome'
temper: 0.12, // cohesión de intents (mantiene hue)
overrides: { tertiary: '#3e63dd' } // fija un rol; el resto deriva del seed
})
eidos.clearColorScheme() // revierte a los primitives del tema
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
```
**Qué hace**: compone el motor `uix.color` — `deriveScheme` (Material 3 → jerarquía
+ neutral) → `generateScale` (12 pasos por rol) → APCA on-solid → alpha
compositing-inverse — en un override de la **capa de binding** `--primitive-{role}-*`
(+ `--color-{role}-contrast` ). Override del binding **reproyecta** cada
`--color-{role}-{slot}` y el chrome neutral (surface/content/border) aguas abajo. La
**paleta de 33 escalas** y los slots NO se tocan.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Capas** (matemática pura → composición pura → aplicación DOM):
| Pieza | Dónde | Qué |
| --- | --- | --- |
| matemática | `arts/color` (`$color`) | `deriveScheme` / `generateScale` / `temper` / APCA / alpha — pura, isomórfica |
| composición | `eidos/lib/build-scheme.ts` | `buildScheme(seed, opts)` → `{ variables, roles }` — pura, testeable |
| runtime | `ActiveEidos.applyColorScheme` | resuelve donantes + background del tema activo, escribe el bloque de estilo, **sigue light/dark** |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Sigue el modo**: las curvas-donantes + el background salen del tema activo, así que
el esquema se **re-deriva en cada `apply()`** (cambio de modo → ramp light vs dark). El
bloque `uix-eidos-scheme` se escribe **después** del de tema para ganar en orden de
cascada.
**Override por rol** + **temper** = doctrina de §25.4 / RFC §6.2: la jerarquía deriva
(override per-rol opcional), los intents **mantienen su hue** y solo afinan
temperatura. `applyColorScheme` devuelve `BuildSchemeResult` (steps hex + `stepsOklch`
+ solid / on-solid / pinned por rol) para introspección de UI.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Wide-gamut**: el bloque apila **hex fallback + `oklch()`** por paso (vía
`schemeDeclarations` ), y `generateScale` retiene el OKLCH raw sin clamp — un seed
vívido (croma > sRGB) sale wide-gamut en P3. Ver §27.
Demo en vivo: `/temas/color` (el builder usa el mismo `buildScheme` ). Tests:
`build-scheme.test.ts` + `active-eidos.test.ts` .
---
## 27. Salida wide-gamut OKLCH (default-on) (2026-06-04)
RFC §7 estrategia A, **implementada por defecto** . Cada paso de paleta se emite dos
veces: el **hex como fallback universal** + un hermano ** `oklch()` ** que gana donde el
navegador lo soporta (Chrome 111+ / Safari 15.4+ / Firefox 113+).
```css
:root {
--scale-purple-9: #8e4ec6 ; /* fallback sRGB */
--scale-purple-9: oklch(0.5556 0.1829 305.86); /* gana -> gamut del display */
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
```
- **Solo las hojas opacas** `--scale-{name}-{step}` ganan el hermano; las capas
`--primitive-*` / `--color-*` son `var()` (heredan) y las alpha siguen como
`color-mix` / rgba. Valores vacíos / no-color no reciben hermano.
- **sRGB idéntico**: el hex y el `oklch()` derivado de un sRGB pintan el mismo color
(verificado: `--scale-purple-9` → `oklch(...)` pinta `#8e4ec6` ). El wide-gamut REAL
aparece cuando el origen excede sRGB (tema OKLCH / esquema generado vívido). La
paleta Radix shipped es sRGB → idéntica hoy; wide-gamut **visible** de la paleta = Fase 3.
- **Default-on, sin flag**: es el comportamiento del framework.
`render-css.ts > appendColorScaleDeclarations` .
- **El generador SÍ produce wide-gamut REAL**: `buildScheme` / `applyColorScheme`
(§26) retienen el OKLCH raw de `generateScale` (sin clamp), así que un seed cuyo
croma excede sRGB renderiza más saturado en P3 que su hex fallback — el bloque apila
**hex + `oklch()`** por paso vía `schemeDeclarations(result, { fallback })` . El demo
`/temas/color` lo demuestra con el slider **vivacidad P3** (badge «fuera de sRGB → P3»
al cruzar el gamut; verificado: croma 0.18 → 0.31).
---
## 28. Accesibilidad forced-colors + ramp de bordes (2026-06-05)
**Forced-colors (Windows High Contrast)** — bajo `@media (forced-colors: active)` el
navegador auto-mapea bordes / texto / fondos a system colors (`forced-color-adjust:
auto`), PERO **elimina `box-shadow`** — y el focus ring de eidos (`--focus-ring`) es un
box-shadow, así que el foco **desaparecía** . Fix: la foundation emite siempre
```css
@media (forced-colors: active) {
:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
}
```
Los componentes que ya enfocan con `outline` (p. ej. Button) conservan el suyo por
especificidad; este es el fallback para los de box-shadow. `renderForcedColorsBlock`
en `render-css.ts` .
**`prefers-contrast: more`** (macOS "Aumentar contraste", etc.) — bloque aparte que
**refuerza el chrome neutral** para quien pide más contraste: bordes a pasos más
fuertes (`subtle/default/strong` → neutral 7/8/9) + texto de-enfatizado más legible
(`secondary` → 12, `muted` → 11). Sólidos + texto primario ya son alto-contraste. Usa
`:root:root` (especificidad 0,2,0) para ganar al `:root` del tema sin depender del orden;
referencia `--primitive-neutral-*` (resuelven del cascade; si un tema los omite, la
declaración se ignora — degrada con gracia). Estrictamente aditivo (gated por el media
query) y estrictamente MÁS fuerte, así que no puede regresar el look por defecto.
`renderPrefersContrastBlock` en `render-css.ts` .
**Ramp de bordes** — el slot de rol `border` pasó de **step 6 → step 7** . En la escala
funcional de Radix el 6 es un *separador sutil* y el 7 es el *UI element border* ; el 6
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
se leía lavado en bordes reales (outline / surface / controles). `element` / `hover` /
`active` (3 / 4 / 5) se mantienen (canónicos de Radix para component-bg).
`DEFAULT_COLOR_ROLE_SLOT_STEPS` . Verificado en navegador (checkbox + token
`--color-{role}-border` → step 7).
## 29. Profundidad (depth) — canal unificado + eventful (2026-06-05)
La profundidad es un **canal unificado y eventful** , no tres sistemas sueltos (sombra +
superficie + z). Guía canónica: `DEPTH_ENGINE_RFC.md` . **Dos momentos** :
- **Estado** — `data-depth='{plane}'` aplica un **plano en reposo** (`flush · raised ·
overlay · modal · recessed`) que cohere superficie + sombra + z. Los tokens
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`--depth-{plane}-{cue}` **componen los primitivos existentes** (`--color-surface-*`,
`--shadow-*` , `--z-index-*` ), así que la mezcla es mode-aware gratis. La regla
`[data-depth]` aplica las señales aditivas seguras (`box-shadow` = gota `shadow` + rim-light
`halo` , más `z-index` ); `surface` queda opt-in (no pisa fondos de componente). El ** `halo` **
es un rim de borde superior computado en oklab (`color-mix(in oklab, white N%, transparent)`):
invisible sobre superficies claras (manda la gota), señal de elevación sobre oscuras — la
respuesta mode-adaptive a "la sombra miente en dark".
- **Evento** — al **emerger** la sombra crece desde plano → la de reposo (sube); al
**presionar** se aplana (recede). Vive en la *firma* (`present-rise` / `press-squeeze`
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
sobre `data-event-*` ), coordinado con motion + sound + haptic desde **un solo evento** .
Generic: un elemento `flush` (sin sombra) = no-op. Degrada con `prefers-reduced-motion` .
**Jaula abierta**: el set de planos es config-driven (`EidosConfig.depth.planes` —
añade/renombra/retunea); los primitivos siguen accesibles (`box-shadow`/`z-index` crudo a un
paso); la capa eventful es aditiva y anulable (sobreescribe keyframes/signatures). Demo en
vivo: `/temas/profundidad` .
**Adopción** (hecha, 2026-06-05): los componentes elevados consumen el canal — sus tokens de
sombra (`--{c}-…-shadow` en `recipes/base.ts` , o el `box-shadow` directo) componen
`var(--depth-{plane}-shadow), var(--depth-{plane}-halo)` , así que **el halo llega a
popover · dialog · drawer · dropdown/context/navigation-menu · menubar · select · tooltip ·
card · combobox · command · link-preview · words**. La `z-index` la sigue gestionando cada
componente (las bandas z son más finas que los 5 planos) — la adopción es solo de la señal
sombra+halo, **cero riesgo de stacking** . La adopción plena vía atributo `data-depth` (que
unificaría también la z) queda como opción futura.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización (2026-07-06) — Adopción v2 canonizada (A1/"Decisión 8",
> 06-19→22).** La "opción futura" del párrafo anterior SE EJECUTÓ en la
> iniciativa A1 del audit de arquetipos (registrada entonces solo en
> `ARCHETYPE_COHERENCE_AUDIT`, hoy deprecated — este es su expediente
> canónico): los overlays **estampan el atributo** (`data-depth="overlay"` en
> select · dropdown · context-menu · popover · link-preview · nav-menu ·
> card-group…), y el plano pinta el **bundle de APARIENCIA**: `background`
> (si declara surface), **`border`** (cue nuevo — modelo *bordered elevation*,
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> el bando Radix/shadcn frente al tonal de M3: en light el borde separa
> planos con sombras sutiles; en dark, donde la sombra miente, borde+halo
> llevan el límite), `box-shadow` (gota+halo) y el **baseline tipográfico
> on-surface** (`--style-label-font-family`/`--leading-ui` — el equivalente funcional del
> re-wrap de Radix en portales; es baseline, no estilo de contenido: el
> adopter fija su radius/font/color encima y gana por orden de cascada, sin
> doble borde; ojo: el borde del plano es real y suma 1px a la caja).
> **z JAMÁS se pinta** — doctrina: *el plano pinta, el posicionador
> posiciona* (el positioner de soma espeja el z computado; los portalados
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> viven en la banda plana `--z-index-overlay-*` de §35 precisamente porque
> el ladder no puede ordenarlos). `--depth-{plane}-z` queda expuesto como
> introspección/escape (0 consumidores — jaula abierta). Guarda:
> `active-eidos-config.test.ts` prohíbe que el CSS generado pinte
> `z-index: var(--depth-…-z)`. Contrato de tokens actualizado en
> [`rfc-depth.md §5`](../rfcs/rfc-depth.md).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Atmósfera** (frost, hecha 2026-06-05): cue `blur` por plano + regla **opt-in**
`[data-depth='{plane}'][data-frost]` → superficie translúcida
(`color-mix(surface var(--depth-{plane}-translucency, 80%), transparent)`) +
`backdrop-filter: blur(var(--depth-{plane}-blur))` . Gated, nunca por defecto (un overlay opaco
sigue opaco salvo que pida `data-frost` ). El builder runtime `ActiveEidos.applyDepth(planes)` /
`clearDepth()` (+ `buildDepth` puro, exportado de `$uix/eidos` ) retune cualquier cue de plano
en vivo — hermano de `applyColorScheme` / `applyTypeScale` . Demo: `/temas/profundidad`
§Materiales.
**Opacidad = función de la elevación** (cue `translucency` , 2026-06-27): así como la
**sombra** crece con la elevación, la **opacidad** del frost también — es un cue de plano
(`--depth-{plane}-translucency`), no un valor fijo. Planos más altos = más opacos: un
`modal` lee como vidrio sólido y legible, un `raised` queda aéreo. Foundation:
`overlay 68% · modal 80%` . El tema cristal abre el rango (`raised 52% · overlay 66% ·
modal 80%`) para que el escalón sea claramente perceptible. El frost rule consume
`var(--depth-{plane}-translucency, 80%)` ; un plano sin declararlo cae al `80%` . (Antes
la translucidez era idéntica en toda elevación — un error: no acompañaba a la sombra.)
**Tier de sombra interior** (`--shadow-inset-*`, 2026-06-15): la escala de sombra
gana un tier **inset** mode-aware, distinto de las sombras de gota (exteriores) y
de los *inset-rings* (anillo nítido `inset 0 0 0 Npx` , otro eje):
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
| Token | Light | Dark |
|---|---|---|
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
| `--shadow-inset-subtle` | `inset 0 1px 2px rgb(15 23 42 / 0.08)` | `inset 0 1px 2px rgb(0 0 0 / 0.30)` |
| `--shadow-inset-deep` | `inset 0 2px 4px rgb(15 23 42 / 0.12)` | `inset 0 2px 4px rgb(0 0 0 / 0.45)` |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
El plano `recessed` lo consume (`--depth-recessed-shadow: var(--shadow-inset-subtle)`),
sustituyendo el `color-mix(neutral-contrast …)` inline previo — que en dark daba un
borde claro (embossado) en vez de un hundido; ahora es mode-correcto (inset oscuro en
ambos modos). Referencias: Tailwind `inset-shadow-{2xs,xs,sm}` , Bootstrap `shadow-inset` ,
Chakra `inner` . Disponible además para estados *pressed* / wells.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Inset-ring** (`--ring-inset-width`, 2026-06-15): eje hermano pero **distinto** —
un anillo interior **nítido** (no difuminado), como el `inset-ring` de Tailwind
(`inset 0 0 0 Npx < color > `). El ancho sale de la escala `--border-width-*`
(`--ring-inset-width` por defecto `thick` =2px, retunable por tema / override por uso);
el color va por el hook `--ring-inset-color` . **No** se puede hacer un token único
`--ring-inset` pre-resuelto: CSS hornea los `var()` anidados en el scope donde se
declara (`:root`), así que el color/ancho por-elemento no propagaría — la expresión
vive en el punto de uso: `box-shadow: inset 0 0 0 var(--ring-inset-width) var(--ring-inset-color, currentColor)` .
Consumidores: date-field (focus de segmento), drag-drop (accepting 1px / dragover 2px),
float-panel (focus + grabbed + resize-grip), select (item checked+highlighted). Las
marcas laterales de un solo lado (range-calendar `inset ±2px 0 0 0` ) **no** son anillos
→ se quedan. De paso, este eje da el primer uso real a los pasos `thin` /`thick` de
`--border-width-*` .
**Escala de blur canónica** (`--blur-*`, 2026-06-15): el desenfoque es un
**primitivo** (`STATIC_BLUR` → `lib/primitives/static.ts` ), no un px disperso.
Valores alineados a Tailwind, escalados por `--scaling` como `--icon-size-*` :
| Token | px | = Tailwind |
|---|---|---|
| `--blur-none` | 0 | — |
| `--blur-sm` | 4 | xs |
| `--blur-md` | 8 | sm |
| `--blur-lg` | 12 | md |
| `--blur-xl` | 16 | lg |
| `--blur-xxl` | 24 | xl |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Dos modelos de referencia: **Tailwind** (escala numérica cruda) y **Apple** (materiales
semánticos `ultraThin…thick` que acoplan blur+translucidez). Eidos toma el de Tailwind como
**eje crudo** y lo compone en la capa semántica de **profundidad** : los planos consumen
`--blur-*` (`--depth-overlay-blur: var(--blur-lg)`, `--depth-modal-blur: var(--blur-xl)` ),
igual que color separa `--scale-*` (crudo) de los roles. Un futuro tema "cristal" acopla
blur+alpha por plano (el modelo Apple) sobre esta escala. Consumidores ya migrados:
planos overlay/modal, `tooltip` (frost), `dialog` /`drawer` (`overlay-blur`). Nada
inventa px de blur a mano.
**Gradientes themeables** (`--gradient-angle-*` + `gradients` , 2026-06-15): eje de
dos capas, espejo de Tailwind (que declara **0 gradientes nombrados** — solo
maquinaria):
- **Direcciones** (`--gradient-angle-*`): las 8 brújulas de Tailwind como ángulos CSS
(`to-t 0deg · to-tr 45 · to-r 90 · to-br 135 · to-b 180 · to-bl 225 · to-l 270 · to-tl 315`).
- **Nombrados** (`gradients` config → `--gradient-*` ): **extensibles** (jaula abierta:
`extendEidosConfig({ primitives: { gradients: {…} } })` ), **role/surface-composed**
→ mode-aware vía los tokens que referencian. Default fuerte mínimo: **un solo**
nombrado, `--gradient-shimmer` (barrido de carga; lo consume `image` ). Un tema añade
sus gradientes de marca aquí.
Los gradientes **funcionales** (color-picker HSV/checkerboard, `conic` de progress/meter,
líneas 1px de cropper/tree-grid, máscara de scroll de tabs, split bicolor de
range-calendar, grip de float-panel, barra de carga de command) **no** son de tema y
siguen crudos — no son decorativos. `skeleton` tinta su shimmer por variante de color
(data-driven), así que conserva su gradiente local pero dogfoodea `var(--gradient-angle-to-r)` .
**Eje de degradados — el 6º builder** (`build-gradient` + `applyGradients` , 2026-06-27):
sobre la capa de tokens, un builder simétrico a color/depth/type/shape/space. Lo que **no
hace nadie**: los gradientes se **derivan de roles de color en OKLCH** , así que un
`--gradient-{name}` retinta con la semilla y flipea light/dark gratis (Tailwind/Open
Props/Panda mezclan dos extremos literales; Material 3 no tiene eje de gradientes).
- **Modelo compartido** en `$libs/gradient` (puro, zero-dep → [`README` ](../../src/libs/gradient/README.md )):
el MISMO `Gradient` (linear/radial/conic/mesh; stop = ref de rol | literal OKLCH | css)
que consumen el eje **y** el futuro `GradientBuilder` (soma) — un gradiente construido es
también un token. `gradientToCss` serializa los role-refs a `var(--color-…)` , default `in oklch` .
- **Factories derivadas de rol** (de `$uix/eidos` ): `deepen(role)` (rampa de un color, paso
9→11), `sheen(role)` (barrido highlight), `halo(role)` (glow radial), `aurora(roles)` (**mesh**
de N blobs radiales por rol, determinista, sobre `surface` — auto-retinta; nadie tiene un mesh
derivado de paleta, todos congelan hex literal).
- **Runtime**: `ActiveEidos.applyGradients({ brand: deepen('primary'), aurora: aurora([...]) })`
escribe un bloque gestionado de `--gradient-{name}` (re-derivado en cambio de modo), + `gradient`
en `ThemeSeed` /`applyTheme`. Gamut-safe por construcción (los role-refs ya pasaron por el motor
de color; un literal OKLCH lleva su fallback hex en la capa consumidora).
- **Interpolación** `in oklch` por defecto (no el `srgb` turbio de los midpoints grises); presets
de hue-path (`longer`/`shorter`) para auroras/iridiscencias desde 2 stops. `shimmer` migrado a `in oklch` .
- **`parseGradient`** (CSS → modelo, round-trip lossless) queda para la **Fase 2** — lo necesita el editor.
Dogfood: la demo `/demos/cristal` reemplazó sus ~12 gradientes hardcodeados por
`applyGradients({ aurora: aurora([...]), brand: {…} })` — gradiente = token themeable que retinta.
**Breakpoints — fuente única + container queries** (`EidosConfig.breakpoints`,
`--breakpoint-*` , 2026-06-15): la **fuente de verdad de los breakpoints es el servicio
runtime** `ActiveDom` (el dev los setea vía `createActiveUix({ dom: { breakpoints } })` ;
`BREAKPOINTS_DEFAULT` es solo el seed). `ActiveEidos` threadea `dom.breakpoints.current`
a `renderStaticCss` , que los emite como tokens `--breakpoint-{sm..xxl}` **y** los usa en
los `@media` de tipografía responsive — así el CSS generado deja de congelarse en un const
duplicado y sigue los breakpoints configurados. **Container queries** : una recipe declara
overrides por breakpoint en la key reservada `container` (hermana de `composition` ):
```ts
recipes: { card: { container: { md: { 'pad': 'var(--space-6)' } } } }
// → @container (min-width: 768px) { [data-card] { --card-pad: var(--space-6) } }
```
El generador (`emitContainerQueries`) usa los **mismos** breakpoints configurados (px
literal — CSS prohíbe `var()` en condiciones `@container` /`@media`, así que la sincronía
solo es posible generándolo). Opt-in: un ancestro con `data-container` activa
`container-type: inline-size` . Eje themeable, 0 consumidores hoy (jaula abierta).
**Opacidad — escala coordinada de dos capas** (`--opacity-*`, 2026-06-15): mismo
patrón dual que la sombra (numérico + semántico).
- **Numérico** (`--opacity-{0,5,…,100}`, Tailwind step-5): granularidad fina para
interfaces etéreas / cristal (capas translúcidas en el tramo bajo).
- **Semántico** (los roles que consumen los recipes): `ghost 0.3 · disabled 0.4 ·
scrim 0.45 · muted 0.65 · overlay 0.65 · subtle 0.8 · press 0.85 · hover 0.9 · full 1`.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`disabled = 0.4` (estándar moderno ≈ Material 38%).
**Unificación**: el estado `disabled` se renderizaba con ~10 valores distintos
(0.45– 0.72) en recipes (`disabled-opacity`) + CSS (`[data-disabled]`/`:disabled`).
Ahora TODOS consumen `var(--opacity-disabled)` . La deriva ad-hoc de CSS
(muted/ghost/subtle) migrada a sus roles. Quedan crudos solo los de animación
(`spinner` keyframe) y `scroll-frames` (rol no semántico). Retunable por tema, como
size/sombra/superficie (decisión del usuario).
**Border-width — escala lineal** (`--border-width-*`, 2026-06-15): adoptada la
**lineal de Bootstrap** (`none 0 · thin 1 · medium 2 · thick 3 · heavy 4`) — la única
escala de referencia que tiene el **3px** que los componentes usan (Tailwind salta
1/2/4/8). Podados los pasos muertos `hairline` (0.5) y el viejo `medium` (1.5) (0
consumidores); `medium` retuneado a 2, `thick` a 3, `heavy` (4) nuevo. Los 3
consumidores de `thick` (2px) — focus-ring de select, quote-border, separator — +
el default de `--ring-inset-width` movidos a `medium` (2px, sin cambio visual). **Todos
los anchos crudos tokenizados** (consumo completo de la escala — la tesis de la
auditoría): `3px→thick` , `2px→medium` , `1px→var(--border-width)` , `1.5px→medium`
(chevron de navigation-menu) en ~35 ficheros. Así un tema retunea el ancho de borde
de una vez (p. ej. `--border-width` denso) y todos los bordes lo siguen.
**Tracking — `caps` para mayúsculas** (`--tracking-*`, 2026-06-15): añadidos
`caps 0.04em` (micro-tracking canónico de etiquetas en MAYÚSCULAS — el patrón
dominante en menús/headings) y `widest 0.1em` . Los 11 `letter-spacing` crudos de
CSS migrados a sus roles (`0.04→caps · 0.05→wider · 0.02→wide · 0.1→widest`). El
`letterSpacing` óptico por-tamaño de la escala tipográfica (xxs/xs…) NO se migra:
es la corrección óptica intrínseca de cada paso.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización (2026-07-06) — los named styles HEREDAN la óptica de la
> escala.** Decisión de diseño (usuario): los 12 styles declaraban
> `letterSpacing: '0'`, anulando sin registro el tracking óptico por-tamaño
> de la Fase 3 en TODA la superficie de contenido (heading/display/text/
> caption…). Pauta vigente: *un named style solo declara
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> `lineHeight`/`letterSpacing` cuando DIVERGE deliberadamente de la métrica
> de su tamaño (anotado inline); coincidencia = omitir (hereda el token de
> la escala — fuente óptica única).* Aplicado: 12 trackings a herencia
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> (hero/h1/h2 aprietan − 0.02/− 0.015/− 0.01em; h6/caption aflojan +0.005em),
> 3 lineHeights redundantes fuera (hero, h1, code), 9 divergencias de
> leading anotadas. Guarda en `active-eidos-config.test.ts` (re-declarar la
> métrica idéntica a la escala falla). Nota causal: la anulación aguas
> arriba explica que el sweep C7 cableara 3 de las 4 coordenadas del bundle
> y saltara `--size-{k}-font-letter-spacing` (el eje parecía muerto) — la
> **adopción en controles queda para la auditoría de componentes**
> posterior, junto a los ~7 `letter-spacing` crudos que quedan en CSS de
> componentes.
refactor(eidos): prune the depth `scrim` cue (F4-D scrim decision)
`--depth-{plane}-scrim` was declared (DepthPlane field, DEPTH_CUES, validation,
applyDepth) but never seeded, emitted or read — a write-surface with no reader
(applyDepth({p:{scrim}}) silently no-op'd; never present in generated base.css).
Analyzed via workflow (state map + 4 dispositions + adversarial stress). The
four collapse to prune vs promote-to-z-parity: wire-and-unify SINKS (contradicts
the test-locked z open-cage precedent + the MD3/Radix/Vaul consensus that the
veil is component-owned, not an elevation cue), relocate flips its verb to
retain, and complete-to-z-parity would mint a third alias of the --color-overlay
backdrop role. Unlike z (kept as an exposed open cage — it varies per plane and
is unpaintable-forever), scrim has no per-plane variance and no value to seed.
Prune: remove scrim from DepthPlane / DEPTH_CUES / validation / emitter +
applyDepth doc. Amend rfc-depth.md (token contract + Phase-4 note + atmosphere
rows), reference.md cue list, changelog, pendiente. The modal veil stays where
MD3/Radix/Vaul + the standing decision put it: --{component}-overlay-* tinted by
--color-overlay.
Zero observable change (never emitted). Tests 71/71; npm run check src/ = 9
baseline, 0 new; base.css unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
**Resuelto 2026-07-12**: el cue `scrim` fue **podado** (era un token declarado sin sembrar/emitir/
leer — write-surface sin reader). El backdrop dim de los modales vive en `--{component}-overlay-*` /
`--color-overlay` (= MD3/Radix/Vaul); ninguna referencia pone el veil en un cue de elevación.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 30. Forma (shape) — continuidad + familias + anidado + eventful (2026-06-05)
La forma es un **canal** , no un número de `border-radius` . Guía canónica:
`SHAPE_ENGINE_RFC.md` . La **magnitud** sigue en `--radius-*` (intacta); shape añade los ejes
que todos dejan planos. **El primer squircle-como-token de la web** (el campo entero es
arco + estático; la continuidad solo existía en Apple, atada a plataforma).
- **Continuidad** — `--shape-smoothing` (exponente superelipse: 1 = arco, 2 = squircle) +
familias vía `data-shape='{family}'` → `corner-shape` : `rounded` (round) · `continuous`
(`superellipse(var(--shape-smoothing))`) · `cut` (bevel) · `scoop` . **Opt-in** (no pisa
círculos/píldoras) y **progresivo** : degrada al arco de `border-radius` donde no hay
`corner-shape` (Chromium 2025+).
- **Armonía anidada** — `[data-shape-nest]` deriva `border-radius: max(0px,
var(--shape-outer-radius) − var(--shape-nest-gap))`: el hijo queda concéntrico al padre (que
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
expone su radio en `--shape-outer-radius` ). **El concéntrico de 4 esquinas requiere radios
finitos**: a `full` (9999px) el radio se recorta a ½ de la dimensión menor *de cada elemento* , así
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
que un hijo de proporción distinta no puede serlo en las 4. Pero **sí en las superiores**
(`radio_card − gap`) si las inferiores quedan rectas — la geometría del reproductor iOS. El demo
`/temas/forma` lo mide (`ResizeObserver`, porque el cap es valor *usado* no legible en CSS) y lo
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
aplica al top de la carátula.
- **Eventful (dos momentos)** — la forma en reposo (`data-shape`) + el **morph** al pulsar: la
firma `press-squeeze` cuadra la esquina un instante (`--shape-smoothing` 2→3→2, registrado con
`@property` para que interpole). Cross-modal: un evento mueve escala + sombra + esquina.
Degrada con `prefers-reduced-motion` . No-op en familias no-`continuous`.
- **Jaula abierta** — escala + familias config-driven (`EidosConfig.primitives.shape`); el
`border-radius` crudo siempre a un paso; builder runtime `ActiveEidos.applyShape(seed)` /
`clearShape()` (+ `buildShape` puro, exportado de `$uix/eidos` ) para dialar continuidad /
nestGap / familias en vivo. Demo: `/temas/forma` .
- **Default por tier — la firma (2026-06-28)** — la continuidad dejó de ser opt-in inerte. Las
**SUPERFICIES** nacen **squircle** por default; los **controles** se quedan en **arco** . Razón
geométrica: a radio de superficie (≥~10px) arco y squircle divergen (premium); a radio de control
(~6px) son indistinguibles → el split no cuesta coherencia. Dos tiers, regla de foundation
**enumerada** (`renderShapeBlocks` → `:where(<superficies>) { corner-shape: var(--shape-surface-default, …) }` ):
- **Tier A** — paneles flotantes `[data-{c}-content]` (dialog/drawer/popover/dropdown/context/
menubar/navigation-menu/select/combobox/tooltip/link-preview + `[data-command]` ); los pickers
heredan vía Popover.
- **Tier B** — superficies no-flotantes (sin marcador compartido → enumeradas): `[data-card]` ,
`[data-banner]` , `[data-radio-cards-item]` .
- **NO** por `[data-archetype='content']` : ese arquetipo también marca tabs/accordion/collapsible/
table content (no-superficies) → un hook sobre-aplicaría. Enumerar es preciso.
- `:where()` (especificidad 0) deja ganar al prop ** `shape` ** → el prop pasa de **opt-IN** (activar
squircle) a **opt-OUT** (`shape='rounded'` escapa a arco) en superficies. Knob de tema
`--shape-surface-default` (unset → squircle; `round` revierte el tier entero). Degrada al arco
donde no hay `corner-shape` . Guard test (superficies sí, controles/filas/pills no) en
`active-eidos-config.test.ts` .
**Pendiente** (futuro): afinar el exponente por defecto si "2" canta en superficies grandes
(`--shape-smoothing` es dial de un token, A/B en dialog) y extender el prop `shape` de opt-out a las
superficies que aún no lo exponen (hoy lo tienen button/card/+pocos). Pills/avatares ya quedan fuera
por construcción (no entran en la enumeración).
## 31. Estructura (espacio · densidad · escala) — el espacio como ritmo (2026-06-05)
Los sistemas **estructurales** (a diferencia de los expresivos) son **solo-estado** — el
escenario, no el suceso. Guía canónica: `STRUCTURE_ENGINE_RFC.md` . Tres ejes ortogonales:
- **Densidad** — `[data-density='compact'|'comfortable'|'spacious']` reescala el layout (`space`
+ `control-height` ) sin tocar la legibilidad del texto. 3 niveles × 2 ejes (config-driven).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **Scaling** — `[data-scaling='90'..'110']` es el zoom global (incluye tipografía; paridad
Radix), compone con densidad.
- **Espacio (el ritmo)** — `--space-{key}` se emite como
`calc(value · var(--density-space-scale) · var(--scaling))` . El **value** ya no es solo px
plano: `buildSpaceScale(seed)` (puro) + `ActiveEidos.applySpacing(seed)` / `clearSpacing()`
lo regeneran desde **una unidad base** (`base × N`, modular) y opcionalmente **fluido**
(`growth > 1` → cada paso `clamp()` que respira entre 480 y 1280px, reusando el `fluidClamp`
del type scale). **Preserva** la composición density × scaling. Hermano de
`applyTypeScale` — opt-in sobre la escala authored (`STATIC_SPACE` intacta). Exportado de
`$uix/eidos` . Demo: `/temas/estructura` .
**Doctrina**: el espacio es **ritmo, no una tabla de píxeles** . Modular + fluido + compuesto con
densidad × zoom desde una semilla. El campo entero shippea una escala plana estática; el espacio
fluido (que casi nadie hace para el espacio, solo para el tipo) + los tres ejes integrados son el
diferencial. Estructural = solo-estado (sin dos momentos — el modelo eventful es de los canales
expresivos).
**Pendiente** (futuro): `applyTheme(seed)` — una semilla que componga tipo + espacio (ritmo
compartido), capstone del cuarteto→quinteto de builders.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización (2026-07-07, auditoría C)**: `applyTheme(seed)` /
> `clearTheme()` **existen** (`ActiveEidos`; `{ color?, type?, depth?,
> shape?, space? }` en un solo write gestionado y atómico — §29-gradientes
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> 06-27 ya lo citaba, y `channels.md` lo documenta como capstone). Este
> "pendiente" quedó stale al aterrizar el sexteto.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 32. Focus ring — modelo de dos anillos parametrizado (2026-06-11)
El foco de los inputs estaba implementado **distinto en cada componente** (el anillo del
`[data-archetype]:focus-visible` del foundation sobre el `<input>` , anillos ad-hoc
`[data-x-input]:focus-visible` , el `color-mix` propio del textarea…). Resultado: un **doble
marco** al editar (anillo interior + exterior), inconsistente entre componentes.
**Solución — un único modelo de dos anillos, parametrizado a nivel de tema:**
- **Token nuevo**: `--focus-ring-inner-width` (= `0` por defecto). Definido en
`primitives/static.ts > STATIC_FOCUS_RING.innerWidth` , tipado en `FocusRingPrimitiveSet`
(`config-types.ts`), emitido en `render-css.ts` .
- El **anillo canónico** (`--focus-ring` del foundation **y** todos los `*-focus-shadow` de
los campos en `recipes/base.ts` ) es ahora **dos anillos** :
```
inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), /* interior */
0 0 0 var(--focus-ring-offset) var(--color-surface-default), /* hueco */
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color) /* exterior */
```
Con `inner-width: 0` el anillo interior es invisible → **un solo marco exterior** .
- El anillo del foundation `[data-archetype]:focus-visible` **excluye los elementos internos
de campo** (`:not(input):not(textarea):not(select):not([data-archetype='segment'])`): su
foco lo muestra el control que los envuelve (`archetypes.css`).
- Quitados los anillos interiores ad-hoc: `[data-css-field-input]:focus-visible` ,
`[data-number-field-input]:focus-visible` ; el textarea pasa a `box-shadow: var(--focus-ring)` .
**Para encender el anillo interior** en un tema: subir `--focus-ring-inner-width` > 0 →
aparece la segunda línea en todos los inputs a la vez, sin tocar componentes.
**Doctrina**: el foco es **un concepto de tema, no de componente** . Dos anillos definidos una
sola vez y parametrizados; los componentes no reinventan su anillo.
### Backlog — tokens retirados en la unificación
Al unificar, la cascada per-`data-color` `--_{css-field,number-field}-accent-*` quedó **sin
uso** (solo la consumía el anillo interior ad-hoc) y se retiró. Quedan registrados aquí por si
se quiere reintroducir que `css-field` / `number-field` tiñan su foco por `data-color` (como
hacen date/time/color-field con sus segmentos):
| Componente | Tokens retirados | Cascada |
| --- | --- | --- |
| `css-field` | `--_css-field-accent-border` · `--_css-field-accent-track` · `--_css-field-accent-text` | `[data-css-field][data-color='…']` × 8 (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) |
| `number-field` | `--_number-field-accent-border` · `--_number-field-accent-track` · `--_number-field-accent-text` | `[data-number-field][data-color='…']` × 8 |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Para reinstaurarlos: re-declarar el trío en el bloque base + la cascada `data-color` , y
consumir `accent-border` en el anillo del campo. **Hoy** ambos usan el `--focus-ring-color`
genérico (consistente con el resto), así que `data-color` no tiñe su foco — decisión
deliberada de la unificación.
### Outline en superficies — `box-shadow` muere en HCM/overflow (2026-06-29)
El anillo `box-shadow` tiene tres fragilidades en **controles autónomos sobre una superficie**
(no campos): (1) `box-shadow` **desaparece bajo forced-colors / HCM** (§28); (2) lo **recorta**
un ancestro `overflow: hidden` ; (3) su capa de hueco hardcodea `var(--color-surface-default)`
(arriba), así que sobre un plano `raised` / `overlay` / relleno el hueco **no casa** con el
fondo → halo desalineado.
Por eso el tier de **controles de superficie** usa `outline` :
```
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset); /* o 0 flush para input / scrollbar */
```
`outline` sigue el `border-radius` en todo navegador evergreen, **no** lo recorta `overflow` ,
y el fallback de forced-colors ya mapea `outline` (§28). Es el patrón de `button` / `card` y
~40 componentes. Los ** últimos 5** en box-shadow se migraron en `ab62cca7` : `command-input` ,
`collapsible-trigger` , `scroll-area-scrollbar` , `toggle` , `splitter` .
**Los campos (input / textarea / segmentos) SÍ siguen en box-shadow** — necesitan el anillo
INTERIOR parametrizable (`--focus-ring-inner-width`) que `outline` , al ser una sola línea, no
puede dar. El modelo de dos anillos de arriba es para ellos.
**Orphan**: tras migrar los 5, el token `--focus-ring` (box-shadow) quedó **sin consumidores
en CSS** (solo lo citan docs). Se deja como token público de foundation; podarlo es decisión
de API aparte (junto con los `--{x}-bg-hover` huérfanos, mismo blocker de `base.css` ).
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Nota 2026-07-07 — outline CANONIZADO catálogo-completo (veredicto S5 del checkpoint de
auditoría de componentes,
[`audit/components/_veredictos.md` ](../audit/components/_veredictos.md ))**: la cláusula
"los campos SÍ siguen en box-shadow" de arriba queda **RETIRADA** . El censo de la
re-auditoría encontró outline como modelo de facto del catálogo entero (un solo remanente
box-shadow en `field.css` , que migra en la fase de fixes) y la comparativa de referencias
lo respalda (Radix Themes / MD3 `md-focus-ring` / Chakra v3 = outline; box-shadow era el
workaround pre-2021 de `outline` sin `border-radius` ). Razones canónicas: outline sobrevive
forced-colors (box-shadow se elimina ahí) + el flicker de segment-fields con anillos
transicionados (blur/refocus por increment). Standing actualizado en
[`reference.md §32` ](./reference.md ).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Nota 2026-07-11 — el anillo de la FUNDACIÓN migra a outline (EID-1, clean-room
2026-07-10)**: el último box-shadow del modelo era el fallback universal de
`archetypes.css` (`[data-archetype]:focus-visible`), justificado por un
`outline: none !important` global de la capa de aplicación que **ya no existe** (la única
mención en todo el árbol era el propio comentario). Migrado a `outline` + `--focus-ring-*`
y envuelto en `:where()` : al compartir ya la MISMA propiedad que las recetas, el fallback
debe perder ante cualquier regla de componente — sin envolver quedaba a (0,5,3) y las
machacaría; en la era box-shadow ambas reglas pintaban A LA VEZ (doble anillo enmascarado
por tokens idénticos, verificado en vivo). El hueco pasa de pintarse (`--focus-ring-bg`,
hook muerto sin consumidores — retirado) a mostrarse vía `outline-offset` . El bloque
forced-colors generado queda como suelo defensivo (comentario actualizado en
`render-css.ts` ). En el mismo pase (EID-2): las opacidades literales de la fundación
migran a tokens — disabled `0.5` → `var(--opacity-disabled)` (= 0.4, unificando con las
recetas que ya lo consumían: disabled se veía DISTINTO según qué capa lo pintara) y el
hover del close `0.85` → `var(--opacity-hover)` (= 0.9, token elegido por rol).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Los botones increment/decrement pintan su glifo desde un **token** , no desde markup
obligatorio. Un trigger sin children renderiza el glifo por defecto vía `:empty::before` ;
pasar children lo overridea por instancia. El glifo es **decorativo** — el botón se
etiqueta con su `aria-label` (morfo), así que `content` en un pseudo-elemento es seguro
(mismo patrón que `--date-range-field-separator-glyph` ).
Cuatro tokens, dos por layout (viven en el recipe **compartido** `spin-field` — ver §34):
| Token | Default | Layout |
| --- | --- | --- |
| `--spin-field-control-increment-glyph` | `'+'` | split |
| `--spin-field-control-decrement-glyph` | `'− '` (`\2212`) | split |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
| `--spin-field-control-increment-glyph-stacked` | `'▲'` (`\25B2`) | stacked |
| `--spin-field-control-decrement-glyph-stacked` | `'▼'` (`\25BC`) | stacked |
El CSS resuelve una variable interna `--_spin-field-increment-glyph` que apunta al token
split por defecto y se re-apunta al hermano `-stacked` bajo `[data-steppers='stacked']` ,
de modo que una sola regla `content` sirve ambos layouts. Un tema retinta/reforma
overrideando cualquiera de los cuatro (globalmente o scoped por componente con
`[data-number-field] { --spin-field-control-… }` ); el **color** del glifo ya viaja por
`--spin-field-control-color*` (no se duplica aquí).
Por qué cuatro y no dos: split usa el par horizontal `+` /`− `; la columna stacked usa
flechas verticales `▲` /`▼`. Un único par no puede tener ambos defaults a la vez, y forzar
`▲` /`▼` en split (o `+` /`− ` en stacked) rompe la convención. Cada par es independiente.
## 34. `spin-field` — visual compartido del stepper-field (`number-field` / `css-field`) — 2026-06-11
`number-field` y `css-field` son **el mismo visual** (campo con borde + input + botones
increment/decrement + scrubber, layouts split/stacked, sizes/variants/colors, glifos);
solo difieren en el *modelo de valor* (soma: número vs valor CSS). Tener dos recipes +
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
dos CSS clonados causaba **drift** : refinar uno (botones cuadrados, flush, divisor) dejaba
el otro con el look viejo. La respuesta canónica no es duplicar — es **compartir
estructuralmente**, el mismo patrón que `toggle-group` reusa `toggle` .
**Cómo**:
- **Recipe único** `spin-field` en `recipes/base.ts` → tokens `--spin-field-*` (geometría,
superficie, control, glifos). NO hay `--number-field-*` / `--css-field-*` .
- **CSS único** `components/spin-field/spin-field.css` con todas las reglas del
stepper-field, seleccionando `[data-spin-field*]` . Cargado por el `@import` de foundation
en `index.css` (no tiene `.svelte` propio que lo auto-importe).
- **Identidad estructural** en los morfos de `number-field` y `css-field` : cada part declara
`data-spin-field` / `-input` / `-increment-trigger` / `-decrement-trigger` / `-scrubber`
(presence attrs). El Provider los emite vía `syncAttrs` ; los sub-parts (cuyo soma
hardcodea sus attrs) los emiten en su getter `props` . `number-field.css` y `css-field.css`
quedan como stubs.
**Theming por componente**: aunque los tokens son compartidos, un tema puede tintar solo
uno scopeando el token al `data-` del componente — `[data-number-field] { --spin-field-bg:
… }` lo hereda el stepper porque vive dentro de ese elemento. El default es compartido.
**Resultado**: una sola fuente del visual del stepper-field. Un fix se aplica a los dos (y a
cualquier futuro spin-field) sin posibilidad de drift. `date` /`time`/`color-field` son
*segmentados* (sin steppers) — comparten solo la *superficie* del campo, lo que sería un
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
refactor aparte.
---
## 35. Canon de escalas — auditoría de theming (2026-06-15)
Sprint de auditoría que llevó las escalas del theming a paridad con las referencias
(Tailwind · Material 3 · Apple HIG · Bootstrap · Radix) y, sobre todo, **forzó su
consumo**: la tesis de la auditoría es que *una escala canónica que los componentes
no consumen (la bypassan con literales) deriva en N variantes del mismo valor*. Cada
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
eje es ahora **retunable por tema** (igual que size/sombra/superficie) y los recipes
**consumen el token, nunca un literal**. Detalle por-eje en el addendum de §29; tokens
en la tabla de §6.
| Eje | Token(s) | Canon | Decisión clave |
|---|---|---|---|
| **Blur** | `--blur-{none,sm,md,lg,xl,xxl}` | 0/4/8/12/16/24 (Tailwind) | numérico crudo; los planos de depth lo consumen (`--depth-*-blur`) |
| **Inner-shadow** | `--shadow-inset-{subtle,deep}` | mode-aware (light slate / dark negro) | lo usa el plano `recessed` ; ≠ inset-ring |
| **Inset-ring** | `--ring-inset-width` + `--ring-inset-color` | `inset 0 0 0 var(width) var(color)` **en el punto de uso** | un token único pre-resuelto es imposible (CSS hornea el `var()` anidado en `:root` ) |
| **Gradientes** | `--gradient-angle-*` + `gradients` →`--gradient-*` | 8 direcciones + nombrados role-composed | Tailwind ship 0 nombrados → solo `shimmer` ; los funcionales (HSV, conic, líneas) NO son de tema |
| **Breakpoints** | `--breakpoint-{sm..xxl}` , `EidosConfig.breakpoints` | **fuente = `ActiveDom`** (runtime, dev-settable) | `ActiveEidos` threadea `dom.breakpoints` al generador; los `@media` dejan de congelarse |
| **Container queries** | recipe key `container` → `@container` + `[data-container]` | px literal **generado** (CSS prohíbe `var()` en `@container` ) | jaula abierta, 0 consumidores hoy |
| **Opacidad** | `--opacity-{0..100}` + semánticos | dual numérico + semántico; `disabled 0.4` (≈ Material 38%) | unificado (~10 valores de disabled → 1); numérico fino = glass-friendly |
| **Border-width** | `--border-width-{none,thin,medium,thick,heavy}` | **lineal Bootstrap** 0/1/2/3/4 (única ref con el 3px real) | **todos** los anchos crudos tokenizados (~35 ficheros) |
| **Tracking** | `--tracking-{…,caps,widest}` | + `caps 0.04em` (MAYÚSCULAS) + `widest 0.1em` | 11 `letter-spacing` crudos migrados; el óptico por-tamaño NO |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Incidente registrado**: una reescritura masiva por PowerShell (`WriteAllText`)
corrompió 11 ficheros (`o→p`); recuperados con `git checkout` + rehechos con la
herramienta Edit. Regla: **modificar ficheros del repo SOLO con Edit/Write** , nunca
PowerShell en bloque.
**Fase 7 (size→fuente — parcial)**: documentados los **3 arquetipos** canónicos
(`control · compact · dense`, §5) + `--size-*` como referencia del `control` . **Guard
de coherencia** activo: ningún `font-size-*` /`icon-size-*` de recipe puede ser literal
px/rem (cierra el hueco del guard solo-CSS). Arreglados los últimos hardcodes
(`toggle`, `avatar` → `--font-size-*` , valores preservados). El `icon-size` de
`password-field` desde `--control-height-*` es **correcto** (es el tamaño del botón
reveal, no del glifo) — falso positivo de la auditoría. **Deferido** : el refactor a
*consumir* el bundle del arquetipo (en vez de re-declarar el mapeo) — grande, con
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
edge-cases (fuentes semánticas por-parte, sistema `--text-N` de accordion) + edición
en paralelo; el guard es lo que impide la deriva mientras tanto.
### Bloque C — números mágicos sueltos (z-index · duración · border/ring)
Cierre de los literales que bypasseaban una escala ya existente. **Regla** : un
literal que iguala un paso de escala DEBE consumir el token; nada de "intencionales".
- **z-index de overlays flotantes** — viven en una escala nombrada propia,
`--z-index-overlay-*` (`STATIC_Z_INDEX_OVERLAY` en `lib/primitives/static.ts` ),
**separada** del ladder global `--z-index-*` (que ordena los depth planes). Los
overlays se portalan al `<body>` como hermanos entre sí **y de los modales** , así
que comparten una **banda plana baja** donde cada peldaño queda justo por encima
del scrim modal: un menú / select / popover abierto DENTRO de un dialog debe
pintar por encima de él. La capa flotante de soma espeja el z **computado** del
content sobre su positioner (`soma/layers/floating/floating.svelte.ts`) — por eso
la banda NO puede ser el ladder 300– 900: `dropdown 300 < modal 700` ocultaría un
dropdown abierto dentro de un dialog. Peldaños (bottom→top):
`inline · backdrop · content · floating · tooltip · detached · toast` — `tooltip`
por encima de `floating` (un tooltip tapa al dropdown, no al revés), `toast` por
encima de la banda FloatPanel de soma (`layers/stacking.svelte.ts`). Cada recipe
consume su peldaño vía `var(--z-index-overlay-*)` : **cero enteros crudos** , y el
guard `contracts.test.ts` ("overlay z-index against raw integers") los prohíbe.
Los `z-index: 0..5` de apilado local (avatar, tabs, sticky cells) son ordenación
relativa intra-componente, NO esta banda — se quedan.
- **Duración** — los que igualaban un paso de la escala la consumen: `dialog` enter
`120ms` →`var(--duration-fast)`, exit `280ms` →`var(--duration-slow)` (asimetría
rápida-entra/lenta-sale preservada, ya 100% en escala); `card` emerge
`320ms` →`var(--duration-slow)`; banner/code-block/link `120ms` →`fast`. Los
fallbacks muertos `, 220ms` /`, 720ms` (checkbox/button, cuyo recipe ya declaraba
el token) se quitaron. **Excepción razonada** : `press-duration 80ms` ,
`spinner-duration 720ms` , `loading-indicator-duration 900ms` se quedan como token
de recipe — son **periodos de animación continua** (giro / shimmer) o un press
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
deliberadamente sub-`fast`, NO transiciones de interacción; la **banda de
interacción** de 5 pasos (`instant..slow`) no tiene sitio para ellos (la
escala completa de duraciones suma las 4 largas de F6 — `slower·deliberate·
emphatic·sustained`, 9 claves; `motion.md` ).
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **Border / ring width** — los anchos ** únicos** de borde/ring que igualaban un
paso (`2px`=`medium`, `3px` =`thick`, `1px` =`thin`) se tokenizaron a
`var(--border-width-*)` (avatar border + badge + carve, drawer drag-ring, slider
thumb, grid-list focus-ring, toast accent-stripe → `thick` , table/menu cell/content
border → `var(--border-width)` ). Valor-preservante, cero cambio visual. Lo que se
**queda como escala dimensional propia** (NO es el concepto border-width
re-derivado): el ring del avatar `sm/md/lg = 1.5/2/3px` (el `1.5` quedó fuera de la
escala global al podar el `0.5/1.5` ), `ring-thickness 3..10px` , `track-width` ,
`content-width` , offsets — escalas locales coherentes, no literales sueltos.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **Actualización (2026-07-06) — container ↔ breakpoints, una sola fuente.**
> Decisión de diseño (usuario): los anchos de contenedor se **interrelacionan
> con los breakpoints** — misma clave, mismo valor, una fuente. Cada
> `--container-width-{k}` referencia su `--breakpoint-{k}`
> (480 · 768 · 1024 · **1280** · 1536), así la geometría de página y las media
> queries no pueden derivar por separado; el **ancho de página canónico es
> `xl` = breakpoint xl = 1280px**. La escalera anterior importada de Radix
> (448/688/880/1136/1280) nunca se evaluó contra los breakpoints y queda
> retirada. En el mismo pass se reparó el defecto que la ocultaba: la recipe
> `container` re-emitía `--container-width-xl` en `:root` vía un token
> fantasma (`var(--layout-container-width-xl, 80rem)`), **sombreando la
> primitiva** para todos los consumidores (`maxWidth='xl'` ≡ `'xxl'`). Ahora
> la recipe posee su propio nombre — `--container-max-width:
> var(--container-width-xl)` — y `container.css` lo consume. Regla registrada:
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> **una clave de recipe jamás re-emite el nombre de un token de foundation**.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 36. Gap canónico trigger→panel — offset token-driven (2026-06-22)
El `sideOffset` de Floating UI es un **número JS** — no acepta `var(--token)` . Por
eso cada flotante hardcodeaba su gap (0/4/6/8, incoherente). Canon:
- **`@property --floating-gap`** `<length>` — registrada para que JS resuelva el px
vía `getComputedStyle` (las custom props **sin registrar** devuelven el `var(...)`
literal, no el valor). Tokens por arquetipo: `--floating-gap-menu: var(--space-1)` ·
`--floating-gap-panel: var(--space-1-5)` . Regla foundation
`[data-floating-gap='menu'|'panel'] { --floating-gap: … }` . Todo emitido en
`render-css.ts` .
- **El posicionador compartido** (`soma/layers/floating/floating.svelte.ts`) lee el
`--floating-gap` resuelto del content vía un ** `$derived` sobre `contentRef.current` **
(reactivo; **antes** era un rAF de una pasada que **NUNCA disparaba para menús
portalizados** → caían al `sideOffset` y salían pegados — fix 2026-06-28) y lo
usa como `offset` de Floating UI (fallback al `sideOffset` numérico). Es el
**OFFSET, no un margin CSS** : la flecha viaja con él (un margin la despegaría del
ancla en popover/tooltip). Por eso NO se reutilizó el `data-canonical-gap` de
split-button (margin — solo válido sin flecha).
- **Stamp condicional** en el content eidos:
`data-floating-gap={rest.sideOffset === undefined ? 'panel'|'menu' : undefined}`
(como split-button). Un `sideOffset` puesto por el consumidor **gana** ; solo el
default cae al token. Los 5 pickers default `sideOffset=undefined` para seguir el
token del panel (componen `PopoverContent` ).
| Arquetipo | Componentes | Gap |
|---|---|---|
| `menu` (gap pequeño) | dropdown · context · sub-menus · menubar · select · navigation-menu | `--floating-gap-menu` (`var(--space-1)`) |
| `panel` | popover · combobox · link-preview · 5 pickers (vía `PopoverContent` ) | `--floating-gap-panel` (`--space-1-5`) |
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Fuera: `command` (dialog/inline, no anclado a trigger), `onion-menu` (radial). Para
retunear el gap de un tema: override `--floating-gap-menu` / `--floating-gap-panel` .
> **Actualización 2026-06-28** — dos cosas: (1) `--floating-gap-menu` pasó de `0` a
> `var(--space-1)` (los menús dejan de salir pegados al trigger); (2) el read del canon
> en `FloatingContent` pasó de un **rAF** (que no entregaba el valor a los menús
> portalizados → solo nav-menu, CSS-posicionado, cogía el token) a un **`$derived`**
> reactivo sobre `contentRef`. Sin el (2), el (1) no llegaba a dropdown/menubar/select.
> En paralelo, en `archetypes.css` el **focus-ring universal** (`[data-archetype]:focus-visible`)
> ahora EXCLUYE `item`/`option`/`content`: las filas de menú/lista usan el highlight
> canónico también en `:focus-visible` (igual que el hover), y los paneles flotantes
> (`content`) ya no dibujan el anillo gordo alrededor de todo el float — su elevación
> (borde + sombra) es el límite.
## 37. Touch-target — 44px en táctil, gated por puntero (2026-06-28)
Eje de a11y que el `ARCHETYPE_COHERENCE_AUDIT` marcó 🔴 (0 componentes garantizaban
44/48px). Doctrina: agrandar el tap-target a 44px **solo en `@media (pointer: coarse)`**
(táctil) — el desktop (puntero fino) mantiene su densidad compacta, **cero regresión** , la
regla queda gated fuera. Validado reference-grade por React Spectrum (escala auto
`medium` /`large` por tipo de puntero); el resto del campo web (Radix/MUI/Chakra) no lo gatea.
- **Controles** (`button` + arquetipos `trigger` /`close`/`action`) — `archetypes.css` :
`@media (pointer: coarse) { :where(<arquetipos>, [data-button]):not([data-size='lg']):not([data-size='xl']) { min-block-size: 44px; min-inline-size: 44px } }` .
El `:not(lg)(xl)` hace doble función: salta los tamaños ya ≥44 (el `min-*` solo CRECE
xs/sm/md, nunca encoge) Y sube la especificidad a 0,2,0 para ganarle al `min-block-size`
del recipe (un `:where()` a 0,0,0 perdería).
- **Filas de lista** (menu/select/listbox/command) — `list-surface.css` : sube
`--list-item-height` (el suelo de `min-block-size` que cada lista puentea) a 44 para
xs/sm/md en coarse.
- **Marcadores** (checkbox/radio/switch) — **NO se agranda el visual** . Crecer la caja a 44px
es un mecanismo de layout de Compose (`sizeIn`); ningún referente web lo porta. El patrón
web (React Aria) es la **fila etiquetada como target** : `[data-radio-group-row]` (dot +
texto) → `min-block-size: 44px` en coarse. La caja desnuda agrupada ya cumple **WCAG 2.5.8
(24px, AA)** por la excepción **Spacing** (los círculos de 24px no se intersectan con gap
≥4px; checkbox 12px / radio 12-16px). AAA (44px) = la fila, por la excepción **Equivalent**
de WCAG 2.5.5. (Un pseudo de 44px que desborda al vecino NO vale: WCAG excluye el área
solapada de la medición.) Pendiente: fila etiquetada de checkbox/switch — son cajas desnudas
sin fila propia (label vía Field/consumidor) → tarea de Field.
Fuentes verificadas a mano: WCAG 2.5.5/2.5.8, Compose a11y, React Aria, React Spectrum.
Commits `a265d39a` (controles) · `c0904fc6` (filas) · `2750b9ce` (fila de radio).
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Revisión 2026-07-08 — checkbox/switch resueltos con `::before` , NO con fila-label.**
El paquete de fixes (touch-rows) cerró checkbox/switch con un pseudo transparente
`[data-checkbox]::before, [data-switch]::before { inline-size/block-size: max(100%, var(--touch-target)) }`
gated a coarse — **crece el ÁREA, no el visual** — más el token `--touch-target: 44px`
en `:root` de `archetypes.css` (compartido por controles + fila de radio, antes `44px` a pelo).
Comparativa hecha con 5 frameworks (React Aria, Radix, Material/MUI, Ark, Base/Bootstrap/shadcn):
solo React Aria y Material desacoplan área/visual; Material Web usa exactamente un pseudo-elemento
(`mdc-touch-target`). **Esto REVIERTE el "pseudo rechazado / tarea de Field" de arriba.**
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Justificación: checkbox/switch son `<button>` desnudos sin fila propia (el label es externo),
así que la fila-label no aplica sin reestructurar.
**Reconciliación CERRADA (decisión del usuario, 2026-07-10) — el pseudo queda CONFIRMADO**
como mecanismo para markers desnudos; supersede el rechazo del §37 original. Es el mecanismo
institucionalizado por las plataformas (Material `mdc-touch-target` · Android touch-delegates ·
iOS hit-test insets sobre los 44pt del HIG), no un atajo. El argumento WCAG-solape original se
resuelve en dos piezas:
1. **Requisito de spacing en listas táctiles densas** : WCAG excluye el área solapada de la
medición, así que una pila de checkboxes en coarse debe mantener pitch ≥ `--touch-target`
(o gap ≥ el excedente del pseudo) para que el AAA medible se conserve — mismo modelo con el
que Material gobierna la densidad. La caja desnuda cumple AA (2.5.8, excepción Spacing) por
sí sola en el peor caso.
2. **La fila-label de Field sigue viva como mejora ADITIVA** (la tarea que el §37 original ya
pedía): cuando el marker tenga fila etiquetada, la FILA es el target AAA real (2.5.5,
excepción Equivalent — patrón React Aria) y el pseudo queda como red inofensiva. Ambas
vías coexisten en los sistemas maduros; no compiten.
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 38. Capa de estado (state-layer) — feedback neutro unificado (2026-06-28)
El feedback neutro de interacción (hover / press / selected de un control **sin valencia** )
estaba implementado de ** ~5 maneras distintas** en ~60 archivos: surface-swap
(`background: var(--color-surface-raised)`), `color-mix(currentColor X%)` ad-hoc, opacity-dim,
tokens `--{x}-bg-hover` bespoke por componente… El `ARCHETYPE_COHERENCE_AUDIT` (Arq. 8) y el
plugin de diseño marcaron lo mismo 🔴: ~168 reglas `:hover` resolviendo el MISMO efecto de
cinco formas.
**Solución — un único state-layer (modelo MD3), parametrizado a nivel de tema**
(`archetypes.css > :root`):
```
--state-hover: color-mix(in srgb, currentColor 8%, transparent);
--state-press: color-mix(in srgb, currentColor 12%, transparent);
--state-selected: color-mix(in srgb, currentColor 12%, transparent);
```
Basado en `currentColor` → **theme-adaptive sin valores por-tema** : el mismo token tiñe
correcto en light y en dark (donde el surface-swap fijo perdía contraste — ganancia neta).
Es el state-layer de Material Design 3 (hover 8% · press/selected 12%).
### Doctrina — qué tier usa el state-layer
- **Tier neutro / ghost** (la mayoría: triggers, items, ghost, controles sin valencia) →
**state-layer** . Es el dueño del feedback neutro.
- **Tier valenced** (solid / soft por color) → **mantiene su hover de paleta** en el recipe
(sistema de Button: `solid→solid-hover` , `soft→element` ). NO se toca — es el patrón de
referencia (Radix/Material), no bespoke.
- El shift de **color de TEXTO** (`--{x}-color-hover`) **se conserva** por componente — el
state-layer solo reemplaza el idioma de **fondo** .
### Mecanismo — overlay, no replace
- **Base transparente** (la mayoría): `background: var(--state-hover)` .
- **Base rellena** (tiene `background-color` propio): overlay en capa →
`background-image: linear-gradient(var(--state-hover), var(--state-hover))` — pinta el tinte
SOBRE el fondo base sin perderlo (filled-safe). Es la forma usada en el rollout.
### Cobertura
- **Pilot**: `accordion-trigger` (`a13ca387`).
- **Rollout**: 19 componentes neutros (`7de6c76b`) — collapsible, breadcrumb, calendar,
pagination, toolbar, file-upload, tag-group, editable, stepper, spin-field, select,
radio-cards, … (cada `--{x}-bg-hover` neutro → `--state-hover` ).
- **Fold transversal** (`e7e4870d`) — las **dos reglas neutras canónicas de `archetypes.css`**
pasan al state-layer:
- `[data-archetype='trigger']:hover` — el opacity-dim (`0.85`) → tinte `--state-hover` (el
dim atenuaba también el texto; el tinte no).
- El highlight de `item` / `option` (hover/focus/highlighted/selected en dropdown · context ·
select · combobox · listbox · command) — `surface-raised` → `--state-hover` (conservando
`color: content-primary` ).
### Pendiente
- Poda de los `--{x}-bg-hover` huérfanos en `lib/recipes/base.ts` (sin uso tras el rollout) +
guard test (ningún recipe neutro declara su propio `--{x}-bg-hover` ). Diferido por un
entanglement de `base.css` con trabajo concurrente.
- `tabs` + `color-picker` (hover bespoke) — diferidos por el mismo motivo.
**Doctrina**: el feedback neutro es **un concepto de tema, no de componente** — paralelo
exacto al focus ring (§32). Un state-layer definido una vez y parametrizado; los componentes
no reinventan su hover.
---
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 39. Un nombre por concepto — muerte de los alias token-level + contrato derivado de la emisión (2026-07-06)
Auditoría de theming, ítem A.9. Dos decisiones de usuario ejecutadas juntas:
**(a) "No quiero alias."** Los puentes de la migración air→eidos
(`appendTransitionAliasDeclarations` + `appendFontFamilyAliases` +
`appendTypographyAliases` , nacidos en `6e8ced2e` 2026-05-14) emitían una
**segunda gramática** para conceptos que ya tenían nombre canónico. Mueren
todos, con migración value-preserving de sus consumidores:
| Alias muerto | Canónico | Refs migradas |
| --- | --- | --- |
| `--font-ui` (cadena de TRES nombres: ui → style-label → family-primary) | `var(--style-label-font-family)` — la capa de roles ES los named styles; nueva validación: `styles.label.family` es requisito | 94 |
| `--font-mono` / `--font-code` | `--font-family-mono` | 64 / 0 |
| `--font-sans` / `--font-serif` / `--font-prose` / `--font-heading` / `--font-display` | `--font-family-{primary·secondary·display}` | 9 |
| `--text-{1..6}-{size,lh,ls}` (escala numérica paralela) | `--font-size-{k}` etc. — y al migrar, los consumidores de control cayeron bajo la regla §5 y ahora consumen la **coordenada del bundle** `--size-{k}-font-size` (el alias EVADÍA ese guard, igual que `px/py` evadía R-4.4) | 7 |
| `--color-focus-ring` (par duplicado con ambos nombres vivos: 10 vs 36) | `--focus-ring-color` (contratado + mayoritario + familia cohesiva) | 13 |
| `--radius-xs` (¡step inexistente!), `--control-height-2xs` , `--font-size-{base,2xl,3xl}` , `--font-weight-normal` , `--motion-spin-duration` | `--radius-sm` · `xxs` · `md/xxl/xxxl` · `regular` · (0 consumidores) | 3 |
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Matiz web-app: `web/routes/layout.css` usaba `--font-sans` /`--font-mono`/
`--radius-xs` como **vocabulario local** (Geist para el chrome del dev-site);
esas declaraciones+consumos locales se conservan con su nombre local (auto-
contenidos tras la muerte del alias) — renombrarlos a canónicos habría
re-tipografiado los componentes de todo el site (no value-preserving).
Los 4 tokens compartidos `--field-segment-*` /`--field-control-trigger-*`
(bloque a mano en render-css) se resolvieron en dos mitades: el par ADOPTADO
(`segment-active-bg/-text`) vive en el **recipe `field`** (mismo nombre
emitido, TSC+contrato lo cubren; precedente list-surface); el par hover
(`segment-hover-bg` + `control-trigger-hover-bg` ) resultó **superseded por el
state-layer** (§38 — `field-segment-state.css` hace el hover con
`--state-hover` desde el rollout) y con **0 consumidores desde siempre** —
**eliminado** con veredicto explícito de usuario ("sí, mátalo. no tiene
sentido", 2026-07-07).
**(b) Contrato derivado de la emisión (census-as-data).** `getCssContract()`
era un SEGUNDO censo a mano (contract.ts, 580 líneas re-enumerando el config
en paralelo a los emisores) y había derivado **211 tokens** : familias enteras
nacidas después (depth, named styles, scaling, breakpoints, banda
`--z-index-overlay-*` , blur, gradient, shape, floating-gap…), claves de
config olvidadas (`content.muted`, `surface.muted` ,
`focusRing.innerWidth` ) y knobs cocidos en emisor (`--radius-factor`,
`--radius-default` , `--ring-inset-width` — su promoción a config queda
agrupada con el ítem state-layer/A.10). Consecuencia: `setCssVariables`
estricto LANZABA sobre vocabulario legítimo y el esqueleto
`renderContractCss` (temas CSS-only) lo omitía. Ahora `createEidosCssContract`
**parsea la misma emisión** que embarcan los generadores (static + todos los
temas con `scales:'all'` ; sintético si no hay temas) — un token está en el
contrato si y solo si se emite, con metadatos (`category`/`path`) resueltos
por tabla de reglas (una familia sin regla entra igual: solo degrada
metadatos, nunca cobertura). Guard bidireccional en suite
(`active-eidos-config.test.ts`): emisión-pública == contrato en AMBAS
direcciones + pins de familias nuevas + pins de alias-muertos. Patrón: la
lección A.7 (computado > lista) aplicada al censo entero; es el modelo
Panda/Style-Dictionary (una fuente, salidas derivadas) — con la diferencia de
que aquí la fuente es la emisión real, no un grafo paralelo.
---
## 40. Knobs cocidos en emisor → config — state-layer, radius factor/default, inset-ring, floating gaps (2026-07-07)
Auditoría de theming, ítem A.10 (+ lote V5 de A.9). Doctrina del usuario:
**"todo tiene que ser tematizable"** — un knob canónico del modelo no puede
vivir como constante de emisor ni como bloque a mano en un CSS estático,
porque queda fuera del config (un tema no puede expresarlo), fuera del
contrato (`setCssVariables` lo rechazaba) y fuera de la validación.
Promovidos a datos del config, con los valores embarcados **verbatim**
(cero cambio visual):
| Config nuevo | Token | Valor (antes cocido en) |
| --- | --- | --- |
| `primitives.state.{hover,press,selected}` | `--state-*` | `8%/12%/12%` de `currentColor` (bloque `:root` de archetypes.css — **eliminado** ; las reglas se quedan) |
| `primitives.radiusFactor` | `--radius-factor` | `1` (render-css) |
| `primitives.radiusDefault` | `--radius-default` | `'md'` (render-css; Decisión 2) |
| `primitives.border.insetRingWidth` | `--ring-inset-width` | `'medium'` (render-css; §29) |
| `primitives.floating.{gapMenu,gapPanel}` | `--floating-gap-*` | `var(--space-1)` / `var(--space-1-5)` (render-css; §36) |
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
El MECANISMO no se toca en ningún caso: el velo sigue siendo `currentColor`
(mode-correct gratis — misma razón por la que M3 no re-declara sus state
layers por scheme; solo la MAGNITUD es del tema), el factor sigue
multiplicando la escala, los gaps siguen referenciando la escala de espacio
(density × scaling fluyen). Validación nueva: porcentajes en `state` ,
`radiusDefault` ∈ steps de radius, `insetRingWidth` ∈ steps de border.width.
Contrato: entran solos por derivación (§39) con path real — el muro
bidireccional los pinnea como ciudadanos. Nivel de tematización: **config**
(como space/radius/typography); el nivel per-theme-id (`themes.{id}`) sigue
siendo color+shadow — si un tema-id concreto necesitara otra intensidad de
velo algún día, es una evolución del eje static/theme (registrada, no
construida). Con esto, M3 deja de ser el único con el state-layer como eje
tematizable — y aquí además es config→emisión→contrato→runtime de una pieza.
---
## 41. Fantasmas value-changing — el eager-freeze de `:root` y sus dos guards (2026-07-07)
Auditoría de theming, bloque B. **La mecánica** (la clase entera): un token de
recipe emitido en `:root` cuyo valor referencia un privado `--_*` declarado
solo en el CSS del componente se computa EN `:root` , donde el privado no
existe → congela *guaranteed-invalid* → los descendientes heredan el
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
congelado (re-declarar el privado más abajo no re-evalúa nada). El TSC no lo
cazaba: su inferencia de dependencias solo ve referencias públicas.
| Fantasma | Desde | Veredicto + fix |
| --- | --- | --- |
| `--calendar-day-holiday-shadow` + `--calendar-event-shadow` — las marcas de festivo (subrayado 2px en acento) y evento (anillo) **nunca pintaron** | su nacimiento | "Las features deben existir" → `scope: 'host'` (dato TSC puro): computan en `[data-calendar]` donde vive `--_calendar-accent-border` y sus 8 retunes por color. **Pintan por primera vez** (verificado en vivo). Los préstamos cross-component (range-calendar 79 tokens, month/year-grid 65, drp 38) siguen como estaban → veredicto de diseño registrado: **la familia calendar se formaliza como capa compartida/arquetipo** (chronos beberá de ella) — `docs/next-features.md` . |
| `--{date,time,color}-field-segment-height` — el alto encajado del segmento computó `auto` siempre | su nacimiento | Veredicto de diseño del usuario: la altura sale del **eje size a nivel familia field** (`--field-control-height-{k}`, que cada x-field hoy re-duplica) — spec registrada y **mandatada** : los componentes deben incorporar el wrapper Field (`[data-field][data-size]`; sondado en vivo: hoy los segmentos no viven dentro). Mientras: los 3 tokens rotos y sus 4 consumos **eliminados** (computaban `auto` ; cero cambio visual, verificado). |
| `image-adjustments` `value-color: var(--color-content-tertiary)` — slot **inexistente** (mezcló el namespace content con el nombre del ROL); el read-out heredó su color desde `7da7285d` (2026-06-11) | 2026-06-11 | Corrección del usuario: "tertiary era un color de ACENTO" → `var(--color-tertiary-text)` (slot de texto del rol jerárquico). El read-out pinta el acento terciario (verificado en vivo). |
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
**Guards que matan la clase** (`recipe-css-contract.test.ts`):
**G1** — un valor de recipe que referencia `--_*` debe declarar scope que lo
cubra (host/leaf), nunca `:root` . **G2** — toda referencia pública sin
fallback en un valor de recipe debe existir en el vocabulario del contrato
derivado (§39); `--color-content-tertiary` habría roto el build el día que se
escribió. Cero falsos positivos en el catálogo completo.
## 42. Gradient finish — el acabado derivado del fill (2026-07-15)
**Decisión de diseño** (tras tres análisis + revisión de coherencia + research
de frameworks de referencia — historia en
`docs/process/gradient-finish-plan-2026-07.md` ): el gradiente entra al sistema
como **ACABADO (material) del fill** , jamás como valor del eje `data-color` —
un `<image>` no puede cumplir el contrato del eje (10 slots derivados + toda
variante lo expresa). Doctrina y mecánica en `reference.md §39` .
Aterrizaje v1 (piloto Button): `primitives.gradientFinish.lift` (config data,
patrón §40) → token `--gradient-finish-lift` (`26%`, `0%` ≈ apagado);
`renderRecipeGradientFinish` emite la var `--_{c}-fill-finish` (rampa
`color-mix in oklch` desde `palette-solid` /`solid-hover` de la instancia —
re-tiñe con roles/33 escalas/modo gratis, tinta heredada); el recipe pinta en
`[data-variant='solid']` + re-assert en `:hover` (su hover usa el shorthand
`background:` ); `data-gradient` es attr **eidos-only de wrapper** (familia
`data-variant` — descubierto en vivo: declararlo en morfo hace que el runtime
lo resuelva desde el espacio de props de SOMA, emita `undefined` y
`mergeProps` clobberee el stamp del wrapper; `data-color-custom` se declara
porque SU prop sí cruza a soma). Inerte en soft/outline/ghost. `forced-colors` degrada
solo (la UA elimina el `background-image` → queda la base sólida). Lab vivo:
`web/routes/temas/gradientes` .
**Rectificación medida (mismo día): la rampa se ANCLA a la sombra de la
tinta.** La forma inicial (lift global hacia blanco) se midió antes de
commitear — 84 combos (9 roles + 33 escalas × claro/oscuro), floors
APCA≥60∧WCAG≥3 — y rompía la tinta heredada en 52/84 a 26% (techo global
seguro: 0%; teal 1%, azules/verdes de modo oscuro 0%): el paso 9 no tiene
margen hacia el blanco en media paleta. Forma final: ambos stops mezclan hacia
el lado de sombra de la tinta (blanca → `#000` , extremo fuerte abajo; oscura →
`#fff` , extremo fuerte arriba), con ancla + ángulo resueltos por color× modo
por el MISMO flip del slot `contrast` (`--color-{role}-finish-anchor/-angle`
+ líneas por escala en la capa THM-2) — el contraste solo puede MEJORAR sobre
el emparejamiento base: seguridad constructiva, dial sin topes (0 regresiones
hasta 40%). Guard ejecutable: `gradient-finish-guard.test.ts` (no-regresión +
set flat-fail clavado: cyan/orange, deuda preexistente del on-solid). **Badge**
se suma al gate v1 (paleta privada; sin `solid-hover` → el extremo profundo
cae a `solid` ). **Override por tema/modo del lift** (mismo día):
`ThemeDefinition.gradientFinish.lift` re-emite el dial en el bloque de tema
(misma especificidad, después en la cascada → el tema gana; claro y oscuro
pueden llevar intensidades distintas, `0%` lo apaga por tema) — test en
`active-eidos-config.test.ts` . v1.x COMPLETA. **v1.5 `spread` shipped (mismo
día, D10)**: `gradient="spread"` = rotación de matiz ±`--gradient-finish-spread`
(default `30` , SIN unidad — el canal `h` de relative color es `<number>` :
`30deg` computa `none` , medido en Chromium) a L/C constantes, con la mezcla
débil del ancla (lift/3) en ambos stops — medido: la rotación pura rompía
grass ±4°/gold ±27° (L de OKLCH ≠ luminancia); anclada: 0 regresiones ≤±45°,
clavado en el guard (4/4). ≈Plano en grises (C≈0), documentado. **v2.1
`Surface` shipped (mismo día)**: la primitiva del lienzo temable — Box +
tratamiento (Box sigue layout-only por doctrina; Surface compone `<Box>`
patrón Section y estampa `data-surface` + color/variant/gradient/rounded);
recipe palette-tint espejo de Card sin chrome (`_palette-*` 5× 8, THM-2:
roles + 33 escalas gratis), variantes `soft` /`solid`, ambos kinds del
acabado, morfo declarativo patrón Box (`scope:['eidos']`, 0 eventos
justificados), audit 145/145. El hero deja de ser escape-hatch. **v2.2 (mismo día) — EL
PLAN COMPLETO**: **named finishes** (D11: `gradientFinish.named` opta
gradientes del open cage como acabado CON tinta autorada obligatoria →
`--gradient-{name}-ink` , viajando como var `--_{c}-finish-ink` que el slice
solid de cada recipe consume con su contrast de fallback — auditoría
2026-07-15: el override directo de `--_{c}-fg` empataba (0,3,0) con el slice
de Surface y dejaba el ganador al orden de hojas, no-contrato → indirección
D7, gana por existir, nunca por especificidad; nombre no declarado degrada a
la rampa, medido; la base sigue siendo el solid de la identidad → `aurora` shipped como blobs de rol con
alfa SIN color base final, `background-image` -válido por arquitectura;
validación numérica solo posible para gradientes de modelo — diferida y
documentada) + ** `on` /`data-on` mínimo** (D12: re-binding de
`--color-content-*` /`--color-border-default` para el subárbol, verificado en
vivo — muted computa la tinta del contexto al 64%; límites por construcción:
anidados y portales excluidos; forced-colors → `CanvasText` ; sin
`color-scheme` a propósito). Guard 5/5. Capítulo del libro:
`docs/theming/gradient-finish.md` (registro de decisiones D1– D12).
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress)
Re-audit of the whole component catalog at pilot depth (91 fichas + the
checkpoint verdicts in docs/audit/components/) and the executed fix packages.
- P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 +
API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d,
component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts
(role=application removed ×4, aria-selected off the Day, drp translationRef,
field data-state prune, pin-input commit-set, media-player renames), 13 new
sema packs + 12 morfos family-default → pack.
- P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar);
onValueCommit terminal-callback norm (pin-input/search/password/textarea +
date/time/color-field add); typed validation reason + onInvalid (tags-input,
css-field); index/onIndexChange (carousel); deselectable; openDelay/
groupSkipDelay; allowCustomValue; defaultValue prune.
- P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across
cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in
field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel);
touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual,
44 AAA). S6 (Field composition) + S8 (calendar-surface) pending.
Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline);
morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched
components; per-component vitest suites green.
Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md
Excluded (broken by the N1 rename, left broken per user decision, not staged):
words/**, palabras/**, chronos, web/routes/alpha/**.
Reconciliation pending: the touch-rows ::before for checkbox/switch reverses
changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed
markers to a labeled-row/Field task — flagged for the user in the handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
uix(color): Fase 5 — guard estructural + docs (CIERRA la iniciativa)
Guard: test nuevo "keeps every component `*Color` prop open (THM-2 — the
cage stays open)" en recipe-css-contract.test.ts — escanea los alias
`export type XColor = …` de components/*/types.ts y FALLA si alguno se
estrecha por debajo de ComponentColorProp. Uniones aditivas ('muted',
'inherit', 'absent'…) pasan; alias resuelven transitivamente
(DatePickerColor = CalendarColor); WIP_TRACKS (chronos) re-entra con su
track; exento comentado: OnionColor = string (MAS ancho que la jaula,
normalizacion pendiente). Verde con el inventario real: 60+ alias abiertos.
Docs (todo lo aplazado del track):
- reference.md §25: reversion de los subconjuntos THM-2 registrada como
doctrina (color = sistema completo en TODOS; identidad ≠ evaluacion;
enforcement = el guard).
- Tracker clean-room §THM-2: nota de reversion (los narrows que THM-2 dejo
"por diseño" quedan abiertos, Avatar.Badge incluido).
- changelog.md §43: cronica de la iniciativa completa (mecanica, fases,
huecos señalados).
- Handoff open-color-cage-2026-07.md → CERRADO con el resumen de la tercera
sesion (cola completa, F4, F5, commits) y los 2 huecos pendientes de
decision: chart (11 props ColorRole) y qr-code (color inline semi-abierto,
invisible para el guard). El cuerpo queda como referencia de patrones A–E.
- 13 READMEs con afirmaciones ya falsas corregidos (roles-only, subsets,
"Locking to ColorRole", tablas de props): badge, css-field, field-langs,
float-panel, link, listbox, mark, metrics (solo el icon; Chart delega en
chart y queda como esta), password-field, proof-of-human (incl. la fila de
decision "descartar" → "hecho (reversion)"), range-calendar, textarea,
timeline. banner NO se toca (su ColorRole es del eje intent, otra cosa).
Contract test: 29/29 verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 43. Abrir la jaula del color — `color` = sistema completo en TODOS los componentes (2026-07-18)
Decisión de diseño del usuario que REVIERTE los subconjuntos que THM-2 dejó
por componente (`AffirmativeColorRole`, `ProgressiveColorRole` , narrows por
`Extract<>` ): el prop `color` acepta el sistema completo —
`ComponentColorProp` = rol / intent / 33 escalas donantes / valor CSS crudo —
en todos los componentes, sin excepciones. Mecánica: el helper compartido
`resolveComponentColor` (wrapper: canónico → `data-color` ; crudo →
`data-color-custom` + seed `--color-custom` ) + el forward per-recipe de THM-2
(§25 de `reference.md` ). Patrones del rollout (A eidos-wrapper · B soma-routed
· C mini-recipe · D tinta de contenido · E delegante) + trampas cazadas:
[`process/open-color-cage-2026-07.md` ](../process/open-color-cage-2026-07.md ).
Cierres de esta fase final (2026-07-18, sesión de continuación): los ~11
standalone (skeleton · spinner · textarea · metrics · form · field-langs ·
radio-cards · image · color-picker con su cross-portal · float-panel ·
proof-of-human con exención fixed-tone formal), la Fase 4 de tinta de
contenido (text · heading · display · code · label — unión aditiva que
conserva el eje `muted` /`disabled`/`on-solid` con rama eje-primero en el
wrapper) y el **guard estructural** : el test "keeps every component `*Color`
prop open" (`recipe-css-contract.test.ts`) rompe el build si un tipo `*Color`
uix(color): abre `color` en la familia chart (resolver SVG rol/escala/crudo)
Chart era el último hueco de la iniciativa. Su `color` NO tinta labels/ejes:
tinta la MARCA de datos (stroke de línea, fill de área/barra/burbuja, hue de
heatmap, arco de gauge, porción de pie/funnel/bar-list/polar, punto de smith).
Es SVG con resolución JS, así que NO usa el `data-color` + capa de paleta
compartida (eso es para cascadas CSS en HTML) — el mecanismo es propio.
Resolver central en `chart/context.ts`:
- `seriesColor` / `seriesSurface` reescritos + `seriesContrast` nuevo, todos
`number | ComponentColorProp`. Un índice cicla `SERIES_ROLES` (multi-serie,
sin cambios). Un valor explícito pasa por `resolveChartColor`:
· rol/intent → `var(--color-{role}-{solid,surface,contrast})`
· una de las 33 escalas → `var(--scale-{name}-{9,a2})` (steps solid /
surface-alpha del `PALETTE_SLOT_STEP`); contrast → white (no hay token)
· valor CSS crudo → verbatim (surface = `color-mix 15%`, contrast = white;
sin contraste garantizado, el trade-off de cualquier custom).
- `ChartContext.color` widenado a `number | ComponentColorProp`.
Todos los props `color?: ColorRole` (~14: ChartSeriesProps, Bubble, Sparkline,
ChartCategory, BarList, Funnel, Calendar/Heatmap, Smith×2, Gauge) →
`ComponentColorProp`. Los 3 builders que armaban el fill inline (funnel:
solid+contrast, heatmap:56, calendar-heatmap:118) refactorizados para pasar
por el helper — funnel pierde su `roleOf` local.
qr-code queda FUERA por decisión de diseño (color = tinta de módulos del QR,
contraste con el fondo, no encaja escalas).
Verificado en Chrome: las 3 ramas del resolver resuelven a color real
(--scale-teal-9→oklch, --scale-teal-a2→rgba translúcido, #3b82f6→verbatim,
mix translúcido ok); marcas existentes = primary-solid (roles behavior-
preserving). check chart limpio; contract 30/30. Docs: changelog §43 +
handoff actualizados (hueco chart cerrado, qr-code excluido por diseño).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
se estrecha por debajo de `ComponentColorProp` . `chronos` re-entra con su
track (WIP_TRACKS).
Verificación adversarial multi-agente (2026-07-18/19) + cierre de chart: la
revisión de 23 agentes cazó 2 fugas TRANSITIVAS (componentes que heredan un
alias abierto pero no enrutaban el runtime) — **card-group-item** (estampaba
`data-color` crudo sin par custom) y **s-text** (quedó en el puente pre-Fase-4)
— arregladas, más un **guard runtime** nuevo (`data-color={…}` dinámico exige
`data-color-custom` en el mismo `.svelte` ). Y la familia **chart** (`color` de
series / gauge / heatmap-hue / per-categoría) se abrió con su propio mecanismo:
al ser SVG con resolución JS, no usa el `data-color` + capa compartida sino un
resolver central en `chart/context.ts` (`seriesColor`/`seriesSurface`/
`seriesContrast` ) que mapea rol → `--color-{role}-*` , escala → `--scale-{name}-
{9,a2}`, valor crudo → verbatim. **qr-code** queda deliberadamente FUERA
(decisión de diseño: `color` es la tinta de los módulos del QR, no encaja el
sistema de escalas). **Bug pre-existente descubierto + ARREGLADO** (`b93cc6c5c`):
el custom-color del FILL de Card + Avatar (su split bespoke `--{c}-color-custom`
quedó ENSOMBRECIDO por el forward de THM-2 — misma especificidad, carga después,
gana; para un valor crudo `--palette-*` sin definir porque el shared layer lo
deriva de `--color-custom` ) → caía a neutral. Migrados a `resolveComponentColor`
(genérico) + borrados los bloques bespoke y los 4 tokens de recipe huérfanos; de
paso Card **pisaba su propio seed** (`style=` antes de `{...rest}` ) → ambos
componen con `composeInlineStyle` . Avatar RING + BADGE = canales separados
(namespace propio, no ensombrecidos) → intactos. Lección: **la verificación de
cascada CSS exige navegador** (el workflow lo dio por bueno leyendo solo código).
uix(color): Fase 5 — guard estructural + docs (CIERRA la iniciativa)
Guard: test nuevo "keeps every component `*Color` prop open (THM-2 — the
cage stays open)" en recipe-css-contract.test.ts — escanea los alias
`export type XColor = …` de components/*/types.ts y FALLA si alguno se
estrecha por debajo de ComponentColorProp. Uniones aditivas ('muted',
'inherit', 'absent'…) pasan; alias resuelven transitivamente
(DatePickerColor = CalendarColor); WIP_TRACKS (chronos) re-entra con su
track; exento comentado: OnionColor = string (MAS ancho que la jaula,
normalizacion pendiente). Verde con el inventario real: 60+ alias abiertos.
Docs (todo lo aplazado del track):
- reference.md §25: reversion de los subconjuntos THM-2 registrada como
doctrina (color = sistema completo en TODOS; identidad ≠ evaluacion;
enforcement = el guard).
- Tracker clean-room §THM-2: nota de reversion (los narrows que THM-2 dejo
"por diseño" quedan abiertos, Avatar.Badge incluido).
- changelog.md §43: cronica de la iniciativa completa (mecanica, fases,
huecos señalados).
- Handoff open-color-cage-2026-07.md → CERRADO con el resumen de la tercera
sesion (cola completa, F4, F5, commits) y los 2 huecos pendientes de
decision: chart (11 props ColorRole) y qr-code (color inline semi-abierto,
invisible para el guard). El cuerpo queda como referencia de patrones A–E.
- 13 READMEs con afirmaciones ya falsas corregidos (roles-only, subsets,
"Locking to ColorRole", tablas de props): badge, css-field, field-langs,
float-panel, link, listbox, mark, metrics (solo el icon; Chart delega en
chart y queda como esta), password-field, proof-of-human (incl. la fila de
decision "descartar" → "hecho (reversion)"), range-calendar, textarea,
timeline. banner NO se toca (su ColorRole es del eje intent, otra cosa).
Contract test: 29/29 verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 44. Paridad de contraste del tonal-ramp — Stage 1 (medir) (2026-07-19)
Ejecución de la iniciativa [`next-features.md §1` ](../next-features.md ). Se
generalizó el criterio on-solid de UN par (§8 del RFC color-engine, `lib/on-solid.ts` )
a una TABLA de pares de slots, y se midió si las promesas de contraste declaradas
se cumplen en las 33 escalas × 2 modos. Herramienta reutilizable:
`scripts/contrast-audit.ts` (WCAG 2 = gate normativo; APCA Lc reportado al lado;
reutiliza la matemática de `$color` ).
**Drift medido + veredictos del usuario:**
- **Texto sobre fondos**: `text·11` (Radix "low-contrast text") queda marginal
sub-4.5:1 en ~11 escalas turbias en modo claro (bronze/orange/teal/gold/…,
peor sobre `element·3` ); `textStrong·12` pasa 4.5 en TODAS. **Veredicto:
`text·11` es el tier SECUNDARIO (contrato Radix, ≈APCA 60), `textStrong·12`
(`text-strong`) es el texto AA garantizado. Para texto AA-crítico se usa
`text-strong` .** Cero cambio de color.
- **Bordes**: TODO el vocabulario de border vive en el rango sutil (steps 4-8,
contrato Radix); el único que cruza 3:1 es el `solid·9` (checked/selected). Se
ratifica el modelo de DOS tiers: **decorativo** (acento `border·7` por-escala
§28, semantic `subtle·4` /`default·6`, border en reposo sobre superficie con
fondo) = indicador NO único → WCAG 1.4.11 EXENTO, sutil por diseño; **portador**
(checked/selected `solid·9` , error, foco) = 3:1 objetivo. El error (`risk·7`)
y el hover/ghost (`neutral·8`) miden sub-3:1 pero van acompañados de señal
redundante (texto de error / fondo) → exentos donde la hay.
- **Foco**: es el único indicador SIEMPRE único. El anillo por defecto
(`color.focus.ring` = `primary·8 @ ~50%` translúcido, `innerWidth: 0` ) mide
sub-3:1 (1.4– 1.9:1 vs superficies; ~1.1 sobre un control solid del mismo hue).
PERO es un **eje de config** (`primitives.focusRing` {offset,width,innerWidth}
+ `color.focus.ring` , emitido una vez, §32): endurecerlo (color opaco,
`innerWidth>0` , offset) es una decisión de valores-por-defecto por config, NO
código. El modelo sigue siendo UN `outline` (§32 lo canonizó; el box-shadow
doble-anillo se retiró — sobrevive a HCM, sin flicker de segment-fields). Se
mantienen los defaults por decisión del usuario; queda registrado.
Stage 2 (generador by-construction que resuelve la luminancia de cada step para
satisfacer la tabla) sigue registrado en `next-features.md §1` , gated en la
migración base→seeds (`rfc-color-engine §9`). Doctrina standing:
[`reference.md §40` ](./reference.md ).
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
## 45. Contraste tonal — Stage 2 CERRADO: el solver era innecesario (2026-07-20)
Ejecución del plan [`process/contrast-stage2-plan-2026-07.md` ](../process/contrast-stage2-plan-2026-07.md ).
Se cerraron las 6 decisiones del usuario (D1 banda `text·11` blanda ≈APCA 60 ·
D2 *medido-pasa* · D3 flip-only · D4 post-pass opt-in · D5 runtime-first · D6
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
tabla-como-datos ya) y luego la **ejecución refutó la premisa** del solver.
**Hallazgo (medido, no teórico):** el morph de plantilla **no COMPUTA** el
contraste del texto — lo **HEREDA** . Los steps 11/12 copian la curva-L del
donante *verbatim* y el contraste está dominado por L (el gamut-mapping baja
color: contraste Stage 2 — contrato §40 como datos + guard CI; solver descartado
La ejecución refutó la premisa del plan: el morph HEREDA el contraste del texto
(steps 11/12 = curva-L del donante verbatim; ~invariante al croma bajo
gamut-mapping), así que toda escala desde donantes §40-compliant cumple por
construcción. Medido en 3 bancos (base, leave-one-out, 45 semillas OOD): 0 fallos
del gate duro text-strong·12, min WCAG 9.7:1. El solver de luminancia no tenía
nada que resolver → descartado por especulativo. D2 = base verbatim (sin
migración base→seeds).
- $color: CONTRAST_PAIRS (arts/color/contrast-contract.ts) — tabla §40 como datos
compartidos: floor duro text-strong·12, banda text·11 (tope relacional, sin nº
mágico), border·7 exento
- scripts/contrast-audit.ts: muere la PAIRS pre-veredicto (4.5 duro contra §40),
consume CONTRAST_PAIRS + banco de regresión morph-generado
- eidos/lib/contrast-invariant.test.ts: guard de CI (4 tests) que bloquea la herencia
- docs: reference.md §40, next-features.md §1, changelog.md §45, plan (OUTCOME)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
croma a L fija ⇒ el contraste es casi **invariante al croma** ); el anclaje exacto
solo toca el step 9. Por tanto, TODA escala generada desde una librería de
donantes §40-compliant hereda los suelos de texto ratificados **por
construcción**. Verificado en tres bancos — base autorado, regeneración
leave-one-out, y 45 semillas fuera-de-distribución (L 0.30– 0.88, croma ≤ 0.24):
**0 fallos del gate duro `text-strong·12` , min WCAG 9.7:1.** Confirmado en el
camino de producción (`buildScheme`/`applyColorScheme` morphan desde las escalas
del tema activo). El solver de luminancia no tenía nada que resolver para
entradas realistas → **descartado por especulativo** (CLAUDE.md: nada
especulativo).
**Lo que sí entró:**
- La tabla de pares ratificada (§40) como **datos compartidos** —
`$color` → `CONTRAST_PAIRS` (`arts/color/contrast-contract.ts`): floor duro
`text-strong·12` , banda blanda `text·11` (tope relacional a text-strong, sin
número mágico), `border·7` exento. Una sola fuente para auditoría + cualquier
consumidor futuro.
- `scripts/contrast-audit.ts` refactorizado: **muere** su `PAIRS` pre-veredicto
(codificaba 4.5 duro en `text·11` , contradiciendo §40) → consume
`CONTRAST_PAIRS` , y gana un **banco de regresión sobre output morph-generado** .
- **Guard de CI** que bloquea la herencia: `eidos/lib/contrast-invariant.test.ts`
(patrón de `palette-invariant.test.ts` ) — falla si un donante autorado o un
cambio del generador rompe la propiedad. La garantía "by construction" ya no es
una afirmación: es un test.
Con D2, el BASE queda **verbatim ground-truth** (la auditoría lo vigila); **sin
migración base→seeds**. Doctrina standing actualizada: [`reference.md §40` ](./reference.md ).
---
docs(blocks): registro de F2.1, doctrina de demo del tier y handoff
Documentación al día de lo que se cerró hoy y handoff para retomar mañana.
- `PLAN-blocks.md` §7: **F2.1 `site-header` HECHA** con sus siete commits, la
fase 0 contra el dossier §P1, el landmark que le faltaba a `NavigationMenu`
(arreglado en el canon, no parcheado en el block), el hueco del CTA que
navega y parece botón (registrado, no falseado) y el defecto de framework
que destapó la demo.
- `theming/changelog.md` §46: las 44 variables de cascada de `Box` dejan de
heredarse. Un `Section` regalaba su padding a cada descendiente —la galería
arrastraba ~300px de aire desde F0— y los hijos heredaban anchos y `display`
ajenos. `@property { inherits: false }`, radio verificado sin regresiones.
Lección: una variable que un componente escribe para SÍ MISMO debe declararse
`inherits: false`; si no, deja de ser un prop y se vuelve un contagio.
- `architecture/blocks.md` B-9: la demo de un block se construye sobre el
harness compartido y el block se enseña A SANGRE — nunca dentro de un marco
con relleno ni de una caja con scroll, porque eso cambia lo que el block
hace.
- `src/uix/blocks/README.md`: anatomía de la demo (harness, `{Name}Site`, ruta
`preview`, `DocRow`, catálogo único, ejes en el shell).
- `CONTINUE-blocks.md` (nuevo): handoff — qué toca (F2.2 `hero`), la plantilla
de ficheros para copiar, las reglas que ya costaron sangre (a sangre, iframe
solo para anchos de dispositivo, cada prop un control, nada de backticks en
`<Text>`, ojo con las variables que heredan), la deuda declarada que es
decisión del usuario y el estado exacto de los gates.
Gates al parar: `blocks:check` verde · `svelte-check` 73 errores, todos deuda
ajena (0 propios) · `vitest src/uix/eidos` 353/353 · `contracts.test` con los
3 fallos ajenos conocidos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
## 46. Las variables de cascada de `Box` dejan de heredarse (2026-07-23)
**Incidente**: cualquier página construida con las primitivas de layout crecía
cientos de píxeles de aire muerto, y los hijos heredaban anchos y `display`
ajenos (en la demo de blocks, un `<header>` acabó midiendo 32× 435 px).
**Causa**: el recipe de `Box` resuelve cada propiedad del modelo de caja con
`var(--box-…, revert-layer)` , y **una custom property hereda por defecto** . Un
`Section` —que es un Box con 64px de padding de bloque— se lo regalaba a TODOS
sus descendientes: `Container` , `Stack` , `Group` , `Card` … Cinco componentes de
layout anidados = cinco veces el padding.
**Corrección**: las 44 variables se registran en `box.css` con
`@property { syntax: '*'; inherits: false }` y sin valor inicial. Así cada una
queda *garantizada-inválida* salvo que la ponga el propio elemento, y las
docs(blocks): registro de F2.1, doctrina de demo del tier y handoff
Documentación al día de lo que se cerró hoy y handoff para retomar mañana.
- `PLAN-blocks.md` §7: **F2.1 `site-header` HECHA** con sus siete commits, la
fase 0 contra el dossier §P1, el landmark que le faltaba a `NavigationMenu`
(arreglado en el canon, no parcheado en el block), el hueco del CTA que
navega y parece botón (registrado, no falseado) y el defecto de framework
que destapó la demo.
- `theming/changelog.md` §46: las 44 variables de cascada de `Box` dejan de
heredarse. Un `Section` regalaba su padding a cada descendiente —la galería
arrastraba ~300px de aire desde F0— y los hijos heredaban anchos y `display`
ajenos. `@property { inherits: false }`, radio verificado sin regresiones.
Lección: una variable que un componente escribe para SÍ MISMO debe declararse
`inherits: false`; si no, deja de ser un prop y se vuelve un contagio.
- `architecture/blocks.md` B-9: la demo de un block se construye sobre el
harness compartido y el block se enseña A SANGRE — nunca dentro de un marco
con relleno ni de una caja con scroll, porque eso cambia lo que el block
hace.
- `src/uix/blocks/README.md`: anatomía de la demo (harness, `{Name}Site`, ruta
`preview`, `DocRow`, catálogo único, ejes en el shell).
- `CONTINUE-blocks.md` (nuevo): handoff — qué toca (F2.2 `hero`), la plantilla
de ficheros para copiar, las reglas que ya costaron sangre (a sangre, iframe
solo para anchos de dispositivo, cada prop un control, nada de backticks en
`<Text>`, ojo con las variables que heredan), la deuda declarada que es
decisión del usuario y el estado exacto de los gates.
Gates al parar: `blocks:check` verde · `svelte-check` 73 errores, todos deuda
ajena (0 propios) · `vitest src/uix/eidos` 353/353 · `contracts.test` con los
3 fallos ajenos conocidos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
cadenas `var(--box-…, revert-layer)` resuelven lo que su autor escribió: los
props de ESE Box y, si no, el cascade normal.
**Radio verificado**: `vitest src/uix/eidos` 353/353 · barrido por la galería de
blocks y las demos de button, card, sidebar, nav-tree y table (cero errores de
consola, cero desbordes, alturas sanas) · el CSS generado no cambia — la
corrección vive en el recipe.
**Lección**: una variable de cascada que un componente escribe para SÍ MISMO
tiene que declararse `inherits: false` . Si no, deja de ser un prop y se
convierte en un contagio.
## 47. El `align` / `justify` / `alignContent` de `Grid` no aplicaban (2026-07-24)
**Incidente**: `<Grid align="center">` (y `justify` , `alignContent` ) no tenía
efecto — `align-items` computaba `normal` . Encontrado al alinear las filas de
`feature-split` : la media y la copy no se centraban.
**Causa**: en `grid.css` , los atajos `place-items` / `place-content` se emiten
DESPUÉS de los longhands (`align-items` / `justify-content` / `align-content` ),
y su fallback era `revert-layer` . Cuando el prop `placeItems` /`placeContent` no
está puesto, `place-items: revert-layer` **revierte** el longhand anterior a su
valor inicial — pisando en silencio lo que `align` /`justify`/`alignContent`
acababan de escribir (un atajo posterior gana sobre el longhand que expande).
**Corrección**: el fallback de los atajos COMPONE los vars de los longhands en
vez de revertir: `place-items: var(--grid-place-items, var(--grid-align, stretch)
normal)` y `place-content: var(--grid-place-content, var(--grid-align-content,
normal) var(--grid-justify, start))`. Si el prop del atajo SÍ está puesto, sigue
ganando (va el último); si no, respeta los longhands. Los grids por defecto no
cambian (para ítems de grid, `align-items: normal` ≡ `stretch` ).
**Radio verificado**: `vitest src/uix/eidos` 353/353 · `feature-split` (filas
centradas), el chart del `Mockup` (`align="end"` = barras a la base) y
`feature-grid` (AutoGrid, sin cambio) en el navegador.
**Lección**: un atajo (`place-*`) con fallback `revert-layer` emitido junto a sus
longhands los clobbe rea cuando el atajo no se usa. El fallback debe componer los
longhands, no revertir.
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
## 48. Las variables de `Flex` y `Grid` tampoco heredaban… pero sí (2026-07-24)
**Incidente**: dentro de un `Mockup` en el hero centrado, 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. Las barras del gráfico eran invisibles.
**Causa**: 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"` del `Stack` exterior del hero escribía
`--flex-align: center` y CADA descendiente lo heredaba: el `Stack` interior del
mockup —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 → `0px` .
**Corrección**: las 7 variables de `flex.css` y las 13 de `grid.css` se
registran con `@property { syntax: '*'; inherits: false }` y sin valor inicial,
igual que `Box` . Un layout anidado vuelve a decidir su propia alineación.
**Radio 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, no un número puesto a mano— y las demos de blocks (hero, pricing,
feature-split, testimonials) + button/accordion/table responden sin regresión.
**Lección** (la misma de §46, y por eso duele): cuando se arregla la herencia de
un recipe hay que **barrer TODOS los recipes hermanos que escriben variables
para sí mismos**, no solo el que dio el síntoma. Y un síntoma tratado con un
workaround —«pon `justify` explícito en los clusters anidados», que es lo que yo
había anotado en el handoff de blocks— es una causa raíz sin buscar.
docs(blocks): registro de F2.1, doctrina de demo del tier y handoff
Documentación al día de lo que se cerró hoy y handoff para retomar mañana.
- `PLAN-blocks.md` §7: **F2.1 `site-header` HECHA** con sus siete commits, la
fase 0 contra el dossier §P1, el landmark que le faltaba a `NavigationMenu`
(arreglado en el canon, no parcheado en el block), el hueco del CTA que
navega y parece botón (registrado, no falseado) y el defecto de framework
que destapó la demo.
- `theming/changelog.md` §46: las 44 variables de cascada de `Box` dejan de
heredarse. Un `Section` regalaba su padding a cada descendiente —la galería
arrastraba ~300px de aire desde F0— y los hijos heredaban anchos y `display`
ajenos. `@property { inherits: false }`, radio verificado sin regresiones.
Lección: una variable que un componente escribe para SÍ MISMO debe declararse
`inherits: false`; si no, deja de ser un prop y se vuelve un contagio.
- `architecture/blocks.md` B-9: la demo de un block se construye sobre el
harness compartido y el block se enseña A SANGRE — nunca dentro de un marco
con relleno ni de una caja con scroll, porque eso cambia lo que el block
hace.
- `src/uix/blocks/README.md`: anatomía de la demo (harness, `{Name}Site`, ruta
`preview`, `DocRow`, catálogo único, ejes en el shell).
- `CONTINUE-blocks.md` (nuevo): handoff — qué toca (F2.2 `hero`), la plantilla
de ficheros para copiar, las reglas que ya costaron sangre (a sangre, iframe
solo para anchos de dispositivo, cada prop un control, nada de backticks en
`<Text>`, ojo con las variables que heredan), la deuda declarada que es
decisión del usuario y el estado exacto de los gates.
Gates al parar: `blocks:check` verde · `svelte-check` 73 errores, todos deuda
ajena (0 propios) · `vitest src/uix/eidos` 353/353 · `contracts.test` con los
3 fallos ajenos conocidos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
---
uix(card): el realce al puntero deja de ser rehén de `interactive`
El usuario preguntó por qué una tarjeta de plan de precios no responde al
puntero. Le dije que Card no exponía estado de puntero y era falso: el realce
existe desde siempre —los dos tokens están en la fundación, con reglas para
ghost, outline y solid, composición con el anillo de selección para que no
desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo
colgaba de un solo attr.
Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el
cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que
ya contiene su propia llamada a la acción no puede tomar ese paquete: un button
dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo
gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de
precio, teasers de artículo, tarjetas de testimonio.
La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta
clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de
puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa
falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le
debe al visitante.
Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio
visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de
presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo
que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho.
El eidos-lint lo clasifica como eidos-only, 0 inválidos.
Medido en navegador sobre estilos computados y posición real: apagado, el hover no
mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra
de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un
solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la
sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad;
y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button,
con cursor de puntero y los mismos dos píxeles de elevación.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 49. El realce al puntero de `Card` deja de ser rehén de `interactive` (2026-08-17)
**Incidente**: una tarjeta de plan de `pricing` no respondía al puntero, y no
había forma de que lo hiciera. El realce existía —`--card-hover-lift` y
`--card-hover-shadow` están en la fundación, con reglas para `ghost` / `outline` /
`solid` , composición con `data-selected` y anulación bajo
uix(card): el realce al puntero deja de ser rehén de `interactive`
El usuario preguntó por qué una tarjeta de plan de precios no responde al
puntero. Le dije que Card no exponía estado de puntero y era falso: el realce
existe desde siempre —los dos tokens están en la fundación, con reglas para
ghost, outline y solid, composición con el anillo de selección para que no
desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo
colgaba de un solo attr.
Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el
cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que
ya contiene su propia llamada a la acción no puede tomar ese paquete: un button
dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo
gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de
precio, teasers de artículo, tarjetas de testimonio.
La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta
clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de
puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa
falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le
debe al visitante.
Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio
visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de
presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo
que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho.
El eidos-lint lo clasifica como eidos-only, 0 inválidos.
Medido en navegador sobre estilos computados y posición real: apagado, el hover no
mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra
de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un
solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la
sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad;
y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button,
con cursor de puntero y los mismos dos píxeles de elevación.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`prefers-reduced-motion` — pero **todo colgaba de `[data-interactive]`** .
**Causa**: `interactive` empaqueta cinco cosas: el elemento `<button>` , el evento
`commit-select` , el `cursor: pointer` , la escala de pulsación, el anillo de foco
**y** la elevación. Una tarjeta que ya contiene su propia llamada a la acción no
puede tomar ese paquete: un `<button>` dentro de otro es marcado inválido y las
dos activaciones se pelean por el mismo gesto. Así que la elevación era
inalcanzable justo donde más se pide — planes de precio, teasers de artículo,
tarjetas de testimonio.
**Corrección**: la elevación pasa a su propio eje, `data-lift` , y las reglas de
hover (más las dos de `selected` + hover y la de reduced-motion) cuelgan de él.
uix(card): el realce al puntero deja de ser rehén de `interactive`
El usuario preguntó por qué una tarjeta de plan de precios no responde al
puntero. Le dije que Card no exponía estado de puntero y era falso: el realce
existe desde siempre —los dos tokens están en la fundación, con reglas para
ghost, outline y solid, composición con el anillo de selección para que no
desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo
colgaba de un solo attr.
Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el
cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que
ya contiene su propia llamada a la acción no puede tomar ese paquete: un button
dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo
gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de
precio, teasers de artículo, tarjetas de testimonio.
La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta
clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de
puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa
falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le
debe al visitante.
Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio
visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de
presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo
que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho.
El eidos-lint lo clasifica como eidos-only, 0 inválidos.
Medido en navegador sobre estilos computados y posición real: apagado, el hover no
mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra
de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un
solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la
sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad;
y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button,
con cursor de puntero y los mismos dos píxeles de elevación.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`interactive` estampa `data-lift` también, así que una tarjeta clicable no cambia
en nada. Lo que el eje NO trae, a propósito: ni `cursor: pointer` —un puntero
sobre algo que al pulsarlo no hace nada es una promesa falsa—, ni escala de
pulsación, ni anillo de foco. Eso es lo que un CONTROL le debe al visitante.
**Sin tokens nuevos**: los dos que hacían falta ya existían. El attr es
visual-wrapper, no contrato: lo estampa el componente de eidos y lo guarda la
mitad de PRESENCIA (`component-visual-attrs.test.ts`, que gana su primera fila de
`card` ), no el morfo — declarar ahí lo que sólo lee una capa es lo que la doctrina
del 2026-08-15 dejó dicho. `eidos-lint card` : 0 inválidos, `data-lift` clasificado
como eidos-only.
**Radio verificado** (Playwright, estilos computados y posición real): `lift`
apagado → el hover no mueve nada ni pinta sombra, cero regresión · `lift` puesto →
`translateY(-2px)` + `0 18px 48px` , con la tarjeta todavía `<div>` , `cursor: auto`
y su CTA intacto dentro (cero botones anidados en botón) · bajo
`prefers-reduced-motion` el desplazamiento se anula y la sombra se queda —se
suprime el movimiento, no la señal de profundidad— · una `Card interactive` real
(cinco en la demo del canon) sigue siendo `<button>` , con `cursor: pointer` y los
mismos 2px. `vitest src/uix/eidos` 420/421, con el único rojo (`skin-media-player`)
ajeno y anterior.
uix(card): el realce al puntero deja de ser rehén de `interactive`
El usuario preguntó por qué una tarjeta de plan de precios no responde al
puntero. Le dije que Card no exponía estado de puntero y era falso: el realce
existe desde siempre —los dos tokens están en la fundación, con reglas para
ghost, outline y solid, composición con el anillo de selección para que no
desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo
colgaba de un solo attr.
Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el
cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que
ya contiene su propia llamada a la acción no puede tomar ese paquete: un button
dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo
gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de
precio, teasers de artículo, tarjetas de testimonio.
La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta
clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de
puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa
falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le
debe al visitante.
Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio
visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de
presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo
que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho.
El eidos-lint lo clasifica como eidos-only, 0 inválidos.
Medido en navegador sobre estilos computados y posición real: apagado, el hover no
mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra
de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un
solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la
sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad;
y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button,
con cursor de puntero y los mismos dos píxeles de elevación.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
---
uix(card): el estado deshabilitado deja de pelearse con la animación por la opacidad
Salió midiendo el plan actual de pricing: la tarjeta no se atenuaba. La regla
estaba escrita, el token resolvía a 0.4 sobre la propia tarjeta y el cursor de esa
misma regla sí aplicaba — pero la opacidad computada seguía en uno.
La culpable es su propia animación de entrada. Corre con relleno en los dos
extremos y su último fotograma es opacidad uno; una animación gana a una
declaración normal en la cascada, así que fijaba la propiedad para siempre.
Aislado en el mismo nodo desactivando la entrada, atenúa correctamente. Llevaba
ahí desde que el componente existe, invisible porque el cursor hacía parecer que
el estado estaba cableado, y alcanzaba a toda Card deshabilitada del ecosistema.
El arreglo es dejar de compartir la propiedad: el estado atenúa por filter, que la
animación no posee. Mismo token, misma escala, ninguno nuevo, y el bloqueo del
puntero en la tarjeta interactiva no se toca.
El guard afirma la FORMA con su porqué escrito —que la regla usa filter y no
opacity, más el bloqueo del puntero y la presencia de la animación que obliga a
esa forma— y está probado por mutación: devolviendo la regla a opacity, falla;
restaurada, verde.
Verificado con la animación presente: la deshabilitada computa filter opacity a
0.4 y sus hermanas ninguno; en píxeles, su tinta más oscura sube a 164 frente a
139 de la normal sobre el mismo lienzo, que es justo lo que da un 0.4 sobre el
fondo de página.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 50. `Card disabled` no atenuaba: la animación de montaje se comía la opacidad (2026-08-17)
**Incidente**: marcando el plan actual de `pricing` como no elegible, la tarjeta
no se atenuaba. La regla existía —`[data-card][data-disabled] { opacity:
var(--card-disabled-opacity) }`—, el token resolvía a **0.4** sobre la propia
tarjeta y el ** `cursor: not-allowed` de esa MISMA regla sí aplicaba**. La opacidad
computada seguía en `1` .
**Causa**: la animación de entrada de la tarjeta. `card-emerge` corre
`slide-from-bottom, fade-in` con `animation-fill-mode: both` , y una animación
**gana a una declaración normal en la cascada**; el fotograma final de `fade-in`
es `opacity: 1` , así que fija la propiedad para siempre. Aislado en el mismo nodo
con `data-no-emerge` : atenúa a 0.4. Es el patrón que la doctrina de motion llama
KNOWN-FRAGILE — dos dueños peleándose por una propiedad sobre un nodo — y llevaba
ahí desde que el componente existe, invisible porque el cursor hacía parecer que
el estado estaba cableado.
**Corrección**: dejar de compartir la propiedad. El estado atenúa por
`filter: opacity(var(--card-disabled-opacity))` , que la animación no posee. Mismo
token, misma escala perceptual, cero tokens nuevos. `pointer-events: none` de la
tarjeta interactiva deshabilitada no se toca.
**Guard**: `components/card/disabled-attenuation.test.ts` afirma la FORMA —que la
regla usa `filter` y **no** `opacity` , con el porqué escrito— más el
`pointer-events` y la presencia de la animación que obliga a esta forma (si algún
día desaparece, `opacity` vuelve a ser viable y el guard se revisa). **Probado por
mutación**: devolviendo la regla a `opacity` , falla; restaurada, verde.
**Radio verificado**: con la animación presente, la tarjeta deshabilitada computa
`filter: opacity(0.4)` y las hermanas `none` ; en píxeles, la tinta más oscura de
la deshabilitada sube a **164** frente a **139** de la normal sobre el mismo
lienzo — que es exactamente lo que da 0.4 sobre el fondo de página. Una `Card
interactive` deshabilitada conserva `pointer-events: none` y `cursor:
not-allowed`.
---
uix(table): una tabla ancha vuelve a tener por dónde salir, y expone los tipos que exige
Dos cosas que salieron al componer la tabla de comparación de pricing, y que no
son del block.
La primera se ve y se mide: a 375 la tabla ocupaba 420 dentro de un contenedor de
327, y esos 93 píxeles eran inalcanzables. Sin barra, sin indicio, columnas
desaparecidas. Fijar la primera columna —que funciona— no sirve de nada si no hay
nada que desplazar. El envoltorio recortaba en los dos ejes: el recorte existe por
el radio de las esquinas y el eje de bloque lo necesita, pero el eje en línea no,
y ahí se comía contenido. Ahora el eje en línea desplaza y el de bloque sigue
recortando, así que el radio se conserva y nada scrollea en vertical salvo que se
pida con maxHeight, que es otra cosa.
Medido después: cuatrocientos veinte sobre trescientos veinticinco, desplazable;
al mover noventa y cinco la columna fijada se queda donde estaba y la última
columna entra en pantalla. La demo del propio componente no cambia.
La segunda es de tipos: el componente exige una instancia del motor y no la
exportaba, así que cualquiera fuera de libs tenía que cruzar una frontera para
tipar su propia envoltura. El guard del tier blocks lo rechazó por nombre y tenía
razón: lo que se corrige es que el componente exponga lo que demanda, no que la
frontera se ensanche.
El guard afirma la forma con su porqué escrito y está probado por mutación:
devolviendo el recorte de siempre, falla. Que sea guard y no nota importa porque
el fallo era silencioso — nada peta, el contenido desaparece por el borde.
Quedan registradas dos filas abiertas que no toco: la columna fijada se ancla con
una propiedad física, así que en dirección derecha-izquierda se mueve con el
contenido en vez de quedarse —medido, su borde se sale de la pantalla—, y eso es
del eje de dirección; y el prop de la tabla no lleva parámetro de tipo, así que
todo consumidor tipado castea, empezando por la demo de este mismo componente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
## 51. Una tabla ancha no tenía por dónde salir (2026-08-17)
**Incidente**: la tabla de comparación de `pricing` a 375px medía **420px dentro
de un contenedor de 327**, y los 93 restantes eran **inalcanzables** : sin barra,
sin indicio, columnas desaparecidas. Fijar la primera columna —que funciona— no
sirve de nada si no hay nada que desplazar.
**Causa**: `[data-table-root]` era `overflow: hidden` en los DOS ejes. El recorte
existe por el radio de las esquinas (corta la cabecera y la última fila), y el eje
de bloque lo necesita; el eje en línea no, y ahí el recorte se come contenido. El
`overflow: auto` sólo aparecía con `maxHeight` (`[data-scrollable]`), que es otra
cosa: pedir una región de scroll VERTICAL.
**Corrección**: `overflow-x: auto` + `overflow-y: hidden` . El radio sigue
cortando, nada scrollea en vertical salvo que el consumidor lo pida, y una tabla
ancha vuelve a leerse en una pantalla estrecha. Medido después: `scrollWidth` 420
sobre `clientWidth` 325 → desplazable; al desplazar 95px la columna fijada NO se
mueve (x=25 antes y después) y la última columna entra en pantalla.
**Guard**: `components/table/horizontal-escape.test.ts` afirma la forma (auto en
línea, hidden en bloque, el radio, y `auto` en ambos ejes bajo
`[data-scrollable]` ) con el porqué escrito, **probado por mutación** : devolviendo
el `overflow: hidden` de siempre, falla. Importa que sea guard y no nota, porque
el fallo era SILENCIOSO — nada peta, el contenido desaparece por el borde.
**Radio verificado**: la demo del propio canon no cambia (su tabla cabe y no
recorta). ⚠️ Y en RTL apareció otro defecto, que NO se toca aquí: la columna
fijada se ancla con `left: 0` FÍSICO, así que al desplazar en RTL —donde
`scrollLeft` va negativo— se mueve con el contenido en vez de quedarse (medido: su
borde pasa de 350 a 444 y sale de una pantalla de 375). Es del eje de dirección,
con doctrina propia; queda registrado en el README de `Table` .
⚠️ **Nota de proceso, segunda vez** : este fichero NO se pasa por prettier. Tiene
ejemplos de código con su propio formato y reformatearlo produce un diff de 400
líneas que sepulta la entrada. Se edita a mano y se deja como está.
---
**Última revisión**: 2026-08-17 (§51 el escape horizontal de `Table` ). Si algo en
este doc no coincide con el código, el código gana — pero abre un issue para que
actualicemos el doc.