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'
}
};
```
CSS generado:
```css
[data-toast] {
--toast-bg: var(--color-surface-raised);
}
[data-toast][data-event='announce'] {
--toast-bg-during-announce: var(--color-primary-element);
}
```
El recipe usa el token apropiado:
```css
[data-toast] {
background: var(--toast-bg);
}
[data-toast][data-event='announce'] {
background: var(--toast-bg-during-announce);
}
```
### 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;
}
```
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 |
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) }
})
```
`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 |
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.
#### 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
`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
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`
(`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
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
```
**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.
**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** |
**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.
**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 */
}
```
- **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; }
}
```
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
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
`--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`
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*,
> 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
> 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):
| Token | Light | Dark |
|---|---|---|
| `--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)` |
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.
**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 |
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`.
`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
> `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
> (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
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í
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
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).
- **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
> 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 |
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 |
| `--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 +
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
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
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 |
**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
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:
> **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`) |
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 |
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) |
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
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). |
**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
eidos(gradient-finish): v2.2 named finishes + on/data-on - EL PLAN COMPLETO
D11 - Named finishes (el diseno A rescatado como opt-in explicito):
- gradientFinish.named opta gradientes del open cage como acabado, cada uno
con su tinta AUTORADA obligatoria (--gradient-{name}-ink, override de la
convencion --_{c}-fg a especificidad (0,3,0) sobre el slice de variante).
- La base sigue siendo el solid de la identidad (fill por capas, D2) -> el
aurora shipped son blobs de rol con alfa SIN color base final:
background-image-valido POR ARQUITECTURA (el caveat del mesh disuelto, no
exceptuado), y re-tine por tema y modo via referencias de rol.
- Honestidad documentada: la validacion numerica al peor stop de un string
CSS arbitrario no es implementable (stops desconocidos); posible solo para
gradientes de modelo (buildGradient) - diferida. El guard clava la sanidad
del config (named subconjunto de gradients + ink presente).
D12 - on/data-on minimo (Surface on="light|dark"):
- La foundation re-vincula --color-content-*/--color-border-default para el
subarbol (dark -> tinta on-solid; light -> on-solid-contrast; mixes oklch
82/64/32%). Verificado EN VIVO: un parrafo muted dentro del aurora computa
la tinta del contexto al 64%.
- Limites POR CONSTRUCCION y documentados en cada consumidor: componentes
anidados con tokens propios y contenido portaleado NO se re-entintan (la
inversion completa sigue siendo iniciativa independiente). forced-colors:
el contexto resuelve a CanvasText/GrayText (bloque extendido). Sin
color-scheme a proposito (solo chrome UA).
Ademas: prop gradient ampliada a (string & {}) para named en Button/Badge/
Surface; poda del orphan-guard (border/text de Surface: 3 slots x 8 colores,
lo que las variantes consumen); guard 5/5; audit 145/145; lab CASO 07 con el
aurora REAL (<Surface gradient="aurora" on="dark">) verificado por valores.
Docs: capitulo D11+D12 + registro D1-D12 + roadmap COMPLETO; reference p39;
changelog p42; plan cerrado (quedan declaradas: inversion completa, demo
propia de Surface, validacion de modelo para tintas named).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
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 →
eidos(gradient-finish): auditoria final - tinta named determinista via --_{c}-finish-ink (D7)
Re-auditoria completa del ciclo (batteria entera de guards + bordes
discriminantes medidos en navegador). Dos hallazgos, dos cierres:
1. El override directo de --_{c}-fg del named finish EMPATABA (0,3,0) con el
slice solid de Surface y dejaba el ganador al orden de hojas de estilo -
no es un contrato (el caso discriminante aurora-sobre-amber lo destapo:
la primera verificacion usaba primary, cuya contrast blanca coincidia con
la tinta de aurora y enmascaraba el empate). Fix por la doctrina D7: el
generador emite la var --_{c}-finish-ink y el slice solid de cada recipe
la consume con su contrast de fallback - gana por existir, nunca por
especificidad; las variantes no-solid quedan inertes por no leerla.
Verificado por valores: aurora-amber blanco (autorada) en Button/Badge/
Surface; flat/rampa amber oscuros (heredada); outline inerte.
2. Nombre no declarado (gradient="foo") degrada a la rampa - medido y
documentado en D11 como fallback gracioso.
Atribucion corregida de paso: el crash de morfo:vocabulary/morfo:check es el
morfo de text-blur (deuda preexistente ajena, chip de tarea creado) - NO
palabras como se asumio antes; surface/box/palabras validan OK.
Bateria final: eidos entero 331/333 (2 = proof-of-human preexistentes),
config 72/72, guard 5/5, audit 145/145, eidos-lint invalid:0 en los tres
consumidores, typecheck limpio.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
`--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
eidos(gradient-finish): v2.2 named finishes + on/data-on - EL PLAN COMPLETO
D11 - Named finishes (el diseno A rescatado como opt-in explicito):
- gradientFinish.named opta gradientes del open cage como acabado, cada uno
con su tinta AUTORADA obligatoria (--gradient-{name}-ink, override de la
convencion --_{c}-fg a especificidad (0,3,0) sobre el slice de variante).
- La base sigue siendo el solid de la identidad (fill por capas, D2) -> el
aurora shipped son blobs de rol con alfa SIN color base final:
background-image-valido POR ARQUITECTURA (el caveat del mesh disuelto, no
exceptuado), y re-tine por tema y modo via referencias de rol.
- Honestidad documentada: la validacion numerica al peor stop de un string
CSS arbitrario no es implementable (stops desconocidos); posible solo para
gradientes de modelo (buildGradient) - diferida. El guard clava la sanidad
del config (named subconjunto de gradients + ink presente).
D12 - on/data-on minimo (Surface on="light|dark"):
- La foundation re-vincula --color-content-*/--color-border-default para el
subarbol (dark -> tinta on-solid; light -> on-solid-contrast; mixes oklch
82/64/32%). Verificado EN VIVO: un parrafo muted dentro del aurora computa
la tinta del contexto al 64%.
- Limites POR CONSTRUCCION y documentados en cada consumidor: componentes
anidados con tokens propios y contenido portaleado NO se re-entintan (la
inversion completa sigue siendo iniciativa independiente). forced-colors:
el contexto resuelve a CanvasText/GrayText (bloque extendido). Sin
color-scheme a proposito (solo chrome UA).
Ademas: prop gradient ampliada a (string & {}) para named en Button/Badge/
Surface; poda del orphan-guard (border/text de Surface: 3 slots x 8 colores,
lo que las variantes consumen); guard 5/5; audit 145/145; lab CASO 07 con el
aurora REAL (<Surface gradient="aurora" on="dark">) verificado por valores.
Docs: capitulo D11+D12 + registro D1-D12 + roadmap COMPLETO; reference p39;
changelog p42; plan cerrado (quedan declaradas: inversion completa, demo
propia de Surface, validacion de modelo para tintas named).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
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
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
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
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.
---
**Última revisión**: 2026-07-23 (§46 herencia de las variables de `Box` ). Si algo en
este doc no coincide con el código, el código gana — pero abre un issue para que
actualicemos el doc.