feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
# Eidos Theming — Architecture Reference
> **Audiencia**: cualquier dev que abra el repo y necesite entender cómo
> se hace el theming en UIX. Cubre el modelo mental, los contratos,
> las herramientas y las trampas. Si después de leerlo todavía no sabes
> dónde poner un token nuevo, falló este doc — abre un issue.
**TL;DR**:
- **9 roles canónicos** de color (`primary`, `secondary` , `tertiary` ,
`neutral` , `affirm` , `fulfill` , `risk` , `threat` , `loss` ).
- **6 sizes canónicos** + `full` (`xxs..xxl`).
- **3 niveles de tokens** públicos: foundation (estable), per-component
recipe (overrideable), private (`--_*`, no contrato externo).
- **Token Scope Contract (TSC)** decide DÓNDE se emite cada token
(`:root` / `[data-{c}]` / `[data-{c}][data-color='X']` / etc.) y
valida transitividad al generar.
- **226 KB raw / 25 KB gzip** de CSS foundation por defecto. Usa
`npm run eidos:purge` para apps en producción → − 46 a − 55%.
- **Modelo de color**: paleta de 31 escalas (diseñable) → roles de
jerarquía (alias explícito) → intents (auto-derivados por convención
del libro, identidad = step 9). Ver §25.
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
- **Compatible con** persistencia versionada, themes CSS-only,
runtime overrides, dark/light, density (compact/comfortable/spacious),
scaling (zoom 90– 110, eje aparte), reduced motion, multi-axis breakpoints.
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
---
## Tabla de contenidos
1. [Mental model ](#1-mental-model )
1bis. [Theming vive en Eidos, no en Morfo (por diseño) ](#1bis-theming-vive-en-eidos-no-en-morfo-por-diseño )
2. [Las 6 capas del CSS de Eidos ](#2-las-6-capas-del-css-de-eidos )
3. [Las 7 capas de tokens ](#3-las-7-capas-de-tokens )
4. [Los 9 roles canónicos de color ](#4-los-9-roles-canónicos-de-color )
5. [El canon de sizes ](#5-el-canon-de-sizes )
6. [Convenciones de naming ](#6-convenciones-de-naming )
7. [Token Scope Contract (TSC) ](#7-token-scope-contract-tsc )
8. [Cómo añadir un componente nuevo ](#8-cómo-añadir-un-componente-nuevo )
9. [Cómo definir un theme ](#9-cómo-definir-un-theme )
10. [Cómo overridear tokens en runtime ](#10-cómo-overridear-tokens-en-runtime )
11. [Bundle strategy + `eidos:purge` ](#11-bundle-strategy--eidospurge )
12. [Herramientas de validación ](#12-herramientas-de-validación )
13. [Integración con Sema (`event:*` scope) ](#13-integración-con-sema-event-scope )
14. [Motion (estado actual) ](#14-motion-estado-actual )
15. [Comparación con librerías de referencia ](#15-comparación-con-librerías-de-referencia )
16. [Anti-patterns que NO debes cometer ](#16-anti-patterns-que-no-debes-cometer )
17. [FAQ — decisiones polémicas ](#17-faq--decisiones-polémicas )
18. [Cobertura universal de TSC ](#18-cobertura-universal-de-tsc )
feat(eidos): variants canon — EIDOS_VARIANTS + per-component lint
Variants son canon del eidos, NO del theme. Decisión arquitectónica
firmemente sostenida: el vocabulario de variants (solid/outline/ghost/
soft/surface/line/pills) está fijo a nivel del framework — paralelo
a las 8 sema families del libro. Theme = retintar lo perceptualmente
fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`.
Cambios:
- lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes
canónicos (control / selection / chip / marker / tabs). Los 5 union
types se derivan via `[number]` indexed access — valor y tipo no
pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con
todos los valores canónicos + utilidades cross-component (`plain`,
`subtle`).
- recipe-css-contract.test.ts: nuevo test "variant CSS selectors per
component match the declared type union". Por cada componente:
extrae el union type de `components/{c}/types.ts` (soporta literal
unions + archetype aliases; cae a advisory mode en Extract<> y
conditional types); compara con `[data-{c}][data-variant='X']`
selectores en `{c}.css`; reporta typos y unauthorized extensions
bidireccionalmente.
- THEMING.md §19: nueva sección "Variants son canon del eidos, NO
del theme" con argumentación (portabilidad, type safety, archetypes
perceptuales paralelos a sema families), tabla de las 3 capas de
la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix
Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado.
- eidos/README.md: tabla de referencia ampliada con §19.
- CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)".
- CONTINUE.md: nota de la decisión arquitectónica.
Variants component-specific permitidos (Banner inline/overlay/
persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada
`components/{c}/types.ts` y el lint los valida contra la CSS del
componente.
Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 6
errores pre-existentes (lib/_demo, soma/components/internal, web/
routes/active) — no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
19. [Variants son canon del eidos, NO del theme ](#19-variants-son-canon-del-eidos-no-del-theme )
20. [Correcciones del engine de theming (2026-06-01) ](#20-correcciones-del-engine-de-theming-2026-06-01 )
21. [Modelo de color de dos niveles (RFC — RESUELTO en §25) ](#21-modelo-de-color-de-dos-niveles-rfc--resuelto-en-25 )
22. [Mejoras pendientes del theming ](#22-mejoras-pendientes-del-theming )
23. [Eje de `scaling` (zoom global) ](#23-eje-de-scaling-zoom-global--2026-06-02 )
24. [Correcciones P2 del engine (2026-06-02) ](#24-correcciones-p2-del-engine-2026-06-02 )
25. [Modelo de color — paleta + roles/intents derivados ](#25-modelo-de-color--paleta--rolesintents-derivados-2026-06-02 )
feat(eidos): runtime theme builder API — eidos.applyColorScheme(seed)
RFC Phase 4: derive a whole-system color scheme from ONE brand seed at runtime.
Packages the demo-only builder into a first-class, tested API.
- build-scheme.ts (pure): buildScheme(seed, opts) composes the uix.color engine
(deriveScheme -> generateScale -> APCA on-solid -> compositing-inverse alpha)
into the `--primitive-{role}-*` (+ `--color-{role}-contrast`) override map.
seed -> { variables, roles }. No DOM. 6 tests.
- ActiveEidos.applyColorScheme(seed, opts) / clearColorScheme(): resolves donor
scales + background from the active theme, writes a managed `uix-eidos-scheme`
style block AFTER the theme block (wins the cascade), and RE-DERIVES on mode
change (follows light/dark). Returns BuildSchemeResult for introspection. opts:
variant (tonal|vibrant|monochrome) + temper (intent coherence, keeps hue) +
per-role overrides + selector. 4 tests (return value, intents, DOM block
ordering + clear, mode re-derivation).
- index.ts: export buildScheme + ApplyColorSchemeOptions + BuildScheme* types.
- temas/color demo: themeOverride now dogfoods buildScheme (drops the duplicated
emitRole/rgbaStr; identical output verified in-browser).
- generated/base.css: regenerated for the loss->plum role fix (binding layer
--primitive-loss-* now points at --scale-plum-*; keeps the contract test green).
- docs: THEMING.md SS26 + COLOR_ENGINE_RFC SS6.2 (status: landed) + README ref row.
Overriding the binding layer reprojects every --color-{role}-{slot} + the neutral
chrome downstream; the 31-scale palette stays put. Math in $color, composition in
eidos/lib (pure), DOM application in ActiveEidos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
26. [Theme builder en runtime — `eidos.applyColorScheme` ](#26-theme-builder-en-runtime--eidosapplycolorscheme-2026-06-04 )
27. [Salida wide-gamut OKLCH (default-on) ](#27-salida-wide-gamut-oklch-default-on-2026-06-04 )
28. [Accesibilidad forced-colors + ramp de bordes ](#28-accesibilidad-forced-colors--ramp-de-bordes-2026-06-05 )
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
---
## 1. Mental model
Eidos es **la capa visual** de UIX. NO posee comportamiento ni estado.
Lee del DOM lo que las capas anteriores escribieron y aplica estilos.
```
Morfo declara la genética (qué attrs / events / partes existen)
↓
Soma transcribe behavior (data-state, data-color, aria-*, focus, …)
↓
Sema emite señales (data-event-* durante el hold perceptual)
↓
Eidos aplica visual (tokens, themes, recipes, archetypes, motion)
```
**Lo que Eidos posee**:
- El namespace `--*` de custom properties.
- 5 layers de CSS (archetypes, events, foundation generado, recipes,
themes).
- El runtime `ActiveEidos` que inyecta foundation + theme CSS.
- Tooling: generación, validación, purge, lint.
**Lo que Eidos NO posee**:
- Estado lógico de componentes (eso es soma).
- Definición de qué events existen (eso es morfo).
- Disparar señales perceptivas (eso es sema).
**La regla del 2-de-3**: una extensión al sistema (atributo, token,
convención) sólo se justifica si **al menos dos de las tres capas**
(soma, sema, eidos) la consumen. Las extensiones que entraron por voto
de eidos: `archetype` , `events[].semantic.{family,intent}` ,
`events[].prewrite[]` , `data-starting-style` / `data-ending-style` .
---
## 1.bis Theming vive en Eidos, no en Morfo (por diseño)
> **Esta es la pregunta arquitectónica más frecuente — y la respuesta más
> importante para no romper el sistema.**
La intuición razonable de un dev nuevo es: *"si morfo es la fuente de
verdad cross-layer, los tokens visuales deberían vivir en morfo también"*.
**NO.** El diseño explícito de UIX dice lo contrario. Esta sección existe
para cerrar el caso con citas, antes de que la confusión arrastre a un
PR que viole la arquitectura.
### Las dos citas canónicas del repo
**`src/uix/active_architecture.md` §9 (Lo que NO es esta arquitectura)**:
> **No es un design system clásico.** Tokens, themes y recipes pertenecen
> a Eidos, no al núcleo.
**`src/uix/README.md` §2 (Eidos)**:
> Capa visual: **tokens, themes, recipes CSS por componente**, archetype
> rules, event reactions y wrappers Svelte sobre los providers headless de
> soma…
>
> Eidos lee del DOM lo que las otras capas escriben — **nunca importa
> internals de soma ni de sema**.
Estas dos frases, por sí solas, cierran cualquier debate sobre dónde
vive el theming. Si una propuesta futura las contradice, la propuesta
debe rechazarse o el doc canónico debe actualizarse antes — no después.
### La regla 2-de-3 lo deriva mecánicamente
**`active_architecture.md` §7 #12 ** y **README.md §5** dicen lo mismo:
> Una extensión a morfo solo se justifica si **al menos dos de las tres
> capas** (soma, sema, eidos) la consumen.
Aplicado al theming:
| ¿Quién consume los tokens visuales? | |
|---|---|
| Soma (behavior runtime) | ❌ no |
| Sema (perceptual signals) | ❌ no |
| Eidos (visual layer) | ✅ sí |
| **Cuenta** | **1-de-3** |
**1-de-3 ≠ 2-de-3 → tokens NO van en morfo, por regla**. La integración
visual cae automáticamente en eidos por la disciplina del 2-de-3, sin
que nadie tenga que decidirlo per-caso.
### ¿Cuál es entonces la relación entre morfo y theming?
Morfo es source-of-truth del **contrato cross-layer** :
- Parts (qué partes existen)
- Events (qué eventos puede disparar)
- Attrs y sus valores enumerados (qué attrs aparecen en DOM con qué values)
- Archetypes (clasificación transversal)
- States declarativos
**El theming se integra con morfo en UN SENTIDO PRECISO**: las recipes de
eidos targetean DOM attrs que morfo declara. Sin morfo, los attrs no
existirían en el DOM y los selectores eidos estarían muertos.
```
MORFO declara data-color.values = ['primary', 'affirm', 'threat', ...]
↓
SOMA emite < button data-color = "affirm" > (al DOM)
↓
EIDOS recipe targetea scope: 'color:affirm' → [data-toggle][data-color='affirm']
↓
BROWSER cascade resuelve la regla CSS
```
El canal entre morfo y eidos es **el DOM** , no objetos TypeScript. Esto
es crítico y está blindado por la regla #6 de las reglas duras:
**`active_architecture.md` §7 #6 **:
> **Eidos consume DOM y data-*, no internals de Soma ni Sema.** Si lo
> necesita, debe estar declarado en morfo o emitido en una señal de sema.
**README.md §5** lo repite literalmente.
### Lo que NO debe hacer eidos jamás
```ts
// ❌ VIOLACIÓN ARQUITECTÓNICA — Eidos importando morfo en runtime
import { toggleMorfo } from '$uix/morfo/components/toggle';
const validValues = toggleMorfo.parts
.find((p) => p.kebab === 'provider')!
.data.find((d) => d.attr === 'data-color')!.values;
// usar validValues para validar TSC scope:'color:X'
```
Aunque la intención sea buena (validar que `scope: 'color:affirm'` matchee
un value morfo-declarado), **este import viola la regla #6** y rompe el
boundary morfo↔eidos. Si quieres esa validación, la defensa correcta es
eidos-lint al nivel del DOM/CSS, no acoplamiento TS.
### La defensa correcta: eidos-lint al nivel del DOM/CSS
La validación de que las recipes eidos targetean valores que morfo declara
SE HACE, pero al nivel DOM/CSS:
```bash
node scripts/eidos-lint.ts toggle
```
Clasifica cada selector `[data-*]` como:
- **morfo-backed** — declarado en morfo, soma lo emite con value válido
- **eidos-only** — attr añadido por wrapper (data-variant, data-size)
- **invalid** — referencia un attr morfo-backed con value fuera del enum → bug
Esto cierra el loop arquitectónicamente sin requerir imports cross-layer.
### Recapitulación de la división canónica
| Concepto | Source-of-truth | Justificación |
|---|---|---|
| Parts (qué partes existen) | **Morfo** | Cross-layer: soma emite, eidos selecciona, sema referencia |
| Events + semantic | **Morfo** | Cross-layer: soma trigger, sema dispatch, eidos reaction |
| Archetypes | **Morfo** | Cross-layer: soma emite, sema cascade, eidos selectores |
| Attr values (`data-color.values`) | **Morfo** | Cross-layer: soma valida, eidos targetea, sema referencia |
| States declarativos | **Morfo** | Cross-layer: soma emite data-state, eidos selecciona |
| **9 roles canónicos sistémicos** | **Eidos** (`lib/themes/base.ts`) | Solo eidos los materializa |
| **6 sizes canónicos** | **Eidos** (`lib/config-types.ts`) | Solo eidos los coordina |
| **Variants visuales** (solid/outline/ghost) | **Eidos wrapper** | Solo eidos las renderiza |
| **Tokens** (`--toggle-solid-on-bg`) | **Eidos** (`lib/recipes/base.ts`) | Solo eidos los consume |
| **Themes** (light/dark/custom) | **Eidos** (`lib/themes/`) | Solo eidos los compone |
| **Persistencia del theme** | **Eidos** (`toDocument()`) | Solo eidos lo serializa |
| **TSC scope axes** (color, state, variant, size, event) | **Eidos** (refieren a attrs morfo-emitted) | Hardcoded en TSC porque son axes del DOM contract |
### La frase canónica
> **Morfo declara el contrato. Eidos declara el theming. El DOM los conecta.**
Esto NO es un compromiso. ES el diseño. La pureza de morfo (TS
declarativo, sin runtime, sin imports de capas visuales) DEPENDE de que
el theming viva fuera.
### Qué propuestas futuras DEBEN rechazarse citando esta sección
1. ** "Vamos a poner los tokens visuales del Toggle en su morfo así
morfo es source of truth de todo"** — viola §9 y la regla 2-de-3.
2. ** "Vamos a hacer que TSC valide `color:affirm` importando
`toggleMorfo.data['data-color'].values` "** — viola regla #6 (eidos
no importa internals de morfo en TS).
3. ** "Vamos a meter `variant: 'solid' | 'outline'` en el morfo del
Toggle"** — viola 2-de-3 (variants solo las consume eidos).
4. ** "Vamos a definir `size` en el morfo con sus 6 valores
canónicos"** — viola 2-de-3 (los 6 sizes son sistema visual, solo
eidos los materializa con tokens coordinados).
Si la propuesta tiene mérito, lo correcto es **actualizar el doc
canónico (`active_architecture.md` §9) ANTES** de aplicar el cambio.
No al revés.
### Lo que SÍ debería entrar en morfo respecto al theming
- Un componente que expone `color` como prop → debe declarar
`data-color.values: ['primary', 'affirm', ...]` en su morfo. Esos
values son cross-layer (eidos targetea, soma emite, sema podría
referenciar).
- Un componente que expone `state` (open/closed) → declara
`data-state.values: ['open', 'closed']` . Igual.
- Un componente que añade attrs visualmente puros (`data-variant`,
`data-size` ) que NADIE más necesita → **NO van en morfo** , los añade
el wrapper eidos directamente.
### Consistencia con los docs canónicos
Esta sección **no introduce doctrina nueva** . Recoge y consolida lo que
ya estaba disperso en:
- [`src/uix/active_architecture.md` ](../active_architecture.md ) §3 (Morfo
= único punto de articulación cross-layer), §7 #6 (Eidos no importa
internals), §7 #12 (regla 2-de-3), §9 (tokens pertenecen a Eidos).
- [`src/uix/README.md` ](../README.md ) §2 (Eidos = capa visual con tokens),
§4 (no es design system clásico), §5 (Eidos consume DOM y data-\*).
- Esta misma `THEMING.md` §1 (Mental model) y §7 (TSC).
Si alguno de esos docs canónicos contradice esta sección, **el doc
canónico gana**. Esta sección consolida; no decide.
---
## 2. Las 6 capas del CSS de Eidos
`src/uix/eidos/index.css` es el entrypoint. Importa, en orden:
```
1. themes/fonts.css ← font faces
2. generated/base.css ← foundation + recipes tokens (226 KB)
3. archetypes.css ← reglas transversales por data-archetype
4. events.css ← reacciones a data-event-* (sema)
5. lib/menu-indicator.css ← partial compartido
6. components/{c}/{c}.css × ~95 ← recipes per-component
```
### Por qué este orden importa
- `generated/base.css` declara tokens (no estiliza). Si los recipes se
cargan antes, no tienen los tokens disponibles.
- `archetypes.css` setea baseline interactiva (cursor, hover, focus
ring). Recipes específicos sobrescriben.
- `events.css` reacciona a `data-event-*` con `animation: @keyframes`
(no `transition` ) porque las señales son transitorias y la animación
debe completar independiente del lifetime de la señal.
- Recipes específicos vienen al final → mayor cascade priority en
conflictos de igual specificity.
### Qué hace cada layer concretamente
| Layer | Tipo | Cuántas reglas | Propósito |
|---|---|---|---|
| `themes/fonts.css` | `@font-face` | 4-12 | Cargar fuentes |
| `generated/base.css` | `:root` + algunos `[data-{c}]` blocks | 4400+ declaraciones | Foundation tokens (scale, primitive, color, size, density, typography, recipe tokens) |
| `archetypes.css` | `[data-archetype='X']` selectors | 11 archetypes | Estilo baseline transversal (trigger, overlay, content, indicator, thumb, track, close, action, item, option, focus) |
| `events.css` | `[data-event-*]` selectors + @keyframes | 9 keyframes + 15 rules | Reactions perceptivas (announce pulse, dismiss fade, commit settle, etc.) |
| `lib/menu-indicator.css` | Partial | 1 selector | Indicator alignment compartido entre menus |
| `components/{c}/{c}.css` | `[data-{c}-*]` selectors | varía | Recipe específico del componente |
### Reglas para tocar cada layer
- **`fonts.css`**: añade `@font-face` . No declara tokens, no estiliza.
- **`generated/base.css`**: **NUNCA editar a mano** . Es output del
generador. Para cambiarlo, edita `lib/recipes/base.ts` o
`lib/themes/base.ts` y corre `npm run generate:eidos-css` .
- **`archetypes.css`**: añade entradas sólo si el archetype está
declarado en algún morfo. Reglas de specificity baja (un atributo).
- **`events.css`**: añade reactions sólo para señales que sema emite.
Usa `animation: @keyframes` , NO `transition` . Lee `data-event-intent`
(signal-bound), NUNCA `data-intent` (state-bound).
- **`components/{c}/{c}.css`**: el dueño es la persona que mantiene
el componente. Sigue la convención de naming (sección 6).
---
## 3. Las 7 capas de tokens
Eidos compone el color final de un elemento atravesando 7 niveles de
indirección. Cada nivel sirve un propósito distinto:
```
┌─ Capa 1: --scale-{name}-{step} :root (estable)
│ Escalas físicas Radix (12 steps + alpha): --scale-teal-9 = #12a594
│
├─ Capa 2: --primitive-{role}-{step} :root (estable)
│ Mapeo role → scale: --primitive-affirm-9 = var(--scale-teal-9)
│
├─ Capa 3: --color-{role}-{slot} :root (estable)
│ Slot semántico: --color-affirm-solid = var(--primitive-affirm-9)
│
├─ Capa 4: --{component}-{role}-{slot} :root (estable)
│ Per-component alias: --button-affirm-solid = var(--color-affirm-solid)
│ (NOTA: drop del segmento "color-" en 2026-05-27)
│
├─ Capa 5: --{component}-palette-{slot} [data-{c}] (DINÁMICA)
│ Palette dinámica por instancia: cambia con data-color
│
├─ Capa 6: --{component}-{variant}-{slot} [data-{c}] (host) (DINÁMICA)
│ Combinación de variante × palette
│
└─ Capa 7: --_{component}-{slot} [data-{c}] (privado)
Token privado consumido por el recipe CSS directamente
```
**Reglas de scope**:
- Capas 1-4 son constantes → `:root` .
- Capa 5 cambia por instancia → `[data-{c}]` y `[data-{c}][data-color='X']` .
- Capa 6 depende de la 5 → DEBE estar en `[data-{c}]` (TSC lo enfuerza).
- Capa 7 es privada → siempre en `[data-{c}]` .
### Por qué tantas capas
**No es accidental**. Cada salto sirve un punto de extensión:
| Capa | Override permite | Ejemplo de uso |
|---|---|---|
| 1 | Cambiar la escala física Radix | Brand quiere su propio teal |
| 2 | Cambiar qué escala mapea un role | "Affirm" usa green en vez de teal |
| 3 | Cambiar slot mapping per role | "Solid" del affirm usa step 10 en vez de 9 |
| 4 | Cambiar token component-specific | Toggle quiere su affirm distinto del global |
| 5 | El runtime per-instancia | `<Toggle color="affirm" />` cambia el palette |
| 6 | Combinar variant × color | Solid variant del toggle con affirm color |
| 7 | Recipe-internal | El recipe decide qué token interno usa para qué |
En la práctica, **la mayoría de las apps SÓLO tocan las capas 1-3**
(brand customization). Las capas 4-7 son del catálogo de componentes.
### Cuándo crear un token nuevo en cada capa
- **Capa 1** (scale): una app rara vez; un **tema de marca** SÍ trae o
amplía su propia paleta (§25.7). Las **31 escalas** por defecto cubren
el caso general.
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
- **Capa 2** (primitive): rara vez. Sólo si añades un role canónico
nuevo (lo cual cambiaría el book canon — no lo hagas).
- **Capa 3** (color): si añades un nuevo `{slot}` (raro). Hoy hay 9
slots (track, element, hover, active, border, solid, solid-hover,
text, contrast).
- **Capa 4** (component-color): añadiendo color support a un componente
nuevo. Se genera automáticamente por `lib/recipes/base.ts` .
- **Capa 5** (palette): cuando el componente acepta `data-color` prop
y necesita un palette dinámico. TSC `scope: 'host'` + overrides
`scope: 'color:X'` .
- **Capa 6** (variant): cuando una variant (`solid`, `outline` , etc.)
combina palette + algo específico. TSC `scope: 'host'` .
- **Capa 7** (private): el recipe lo consume. Convención: prefijo `_` .
---
## 4. Los 9 roles canónicos de color
Los roles vienen del libro *Diseñando lo que ocurre* . SON CANON. NO
inventes nuevos.
```
HIERARCHY (no evaluative) INTENT (evaluative)
───────────────────────────── ───────────────────────────
primary — brand main affirm — turning ON something positive
secondary — brand support fulfill — completion / success
tertiary — brand tertiary risk — moderate negative consequence
neutral — gray default threat — active negative consequence
loss — irreversible negative outcome
```
**Reglas estrictas**:
- Los 9 nombres son los únicos válidos. NO uses `success` , `warning` ,
`danger` , `info` — esos pertenecen a otros modelos (Bootstrap, etc.).
- **`primary`/`secondary`/`tertiary`** son **jerárquicos** : usar cuando
la diferencia es "más vs menos prominente". Sin carga evaluativa.
- **`neutral`** es el default. Sin carga semántica.
- **`affirm`/`fulfill`/`risk`/`threat`/`loss`** son **evaluativos** :
comunican qué pasa con la acción.
- Si `intent === 'neutral'` , `color` (hierarchy override) puede aplicar.
Si `intent` es evaluativo, el `intent` GANA y `color` se ignora.
**Mapping a escalas físicas** (en theme base):
| Role | Escala Radix | Reasoning |
|---|---|---|
| primary | indigo | Brand default, neutral-positive |
| secondary | slate | Support hierarchy, slate-blue |
| tertiary | (lo elige el theme) | — |
| neutral | gray | Sin carga |
| affirm | teal | Light positive, mint-fresh |
| fulfill | green | Completion, classic success |
| risk | amber | Caution, warning |
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
| threat | red | Active danger |
| loss | plum | Posterior gravity, deep |
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
> **Los intents auto-derivan de la paleta** (`CANONICAL_INTENT_SCALES`,
> identidad = step 9): `neutral→gray · affirm→teal · fulfill→green ·
> risk→amber · threat→red · loss→plum`. La jerarquía (`primary` /
> `secondary` / `tertiary`) la elige el tema. Modelo completo en **§25**.
La **paleta** son **31 escalas** de 12 steps + 12 alpha = 24 tokens c/u
(**744 tokens `--scale-*` ** — el grueso del bloat del foundation). Sobre
ella, los 9 roles aliasan vía `--primitive-{role}-{step}` (9 × 24 =
**216 primitives**).
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
### Por qué 9 roles y no 4 (como shadcn) o 14 (como Mantine)
Los 9 son el resultado del análisis perceptivo del libro:
- 3 hierarchy roles cubren la dimensión "prominencia visual".
- 1 neutral cubre el default sin carga.
- 5 intent roles cubren las cinco valencias evaluativas distintas.
Cualquier sistema con menos pierde resolución perceptiva. Cualquier
sistema con más cae en redundancia (success vs fulfill, danger vs
threat — no son lo mismo).
### Subset por componente
Cada componente expone su propio subset de los 9. Ejemplos:
| Componente | Subset | Excluye |
|---|---|---|
| Toggle | primary, secondary, neutral, affirm, risk, threat | fulfill, loss (no aplica) |
| Button | los 9 | — |
| Badge | primary, secondary, neutral, affirm, fulfill, risk, threat, loss | tertiary (no canónico) |
Por qué subsets: un toggle no es completion ni irreversible loss.
Exponer fulfill/loss en su API sería semánticamente incorrecto.
---
## 5. El canon de sizes
```
xxs · xs · sm · md · lg · xl · xxl | full
───────────────────────────────────── ───────
6 sizes canónicos (físicos) 1 size de layout
```
`md` es el default. `full` no es físico — es semántica de layout
(`100%` / `100vw` / `100dvh` según contexto). No genera tokens fijos.
Cada size canónico genera tokens coordinados:
```
--size-md-control-height: 36px
--size-md-font-size: 14px
--size-md-font-line-height: 1.5
--size-md-icon-size: 16px
--size-md-padding-inline: 12px
--size-md-padding-block: 8px
--size-md-gap: 8px
--size-md-radius: 6px
```
### Regla clave: `md` NO cambia por viewport
Lo responsive decide **qué size activo se usa** , NO redefine los
tokens. Si tu Toggle en mobile usa `sm` y en desktop `md` , ambos
tokens están disponibles y el wrapper elige cuál.
```svelte
<!-- Correcto -->
< Toggle size = {{ base: ' sm ' , md: ' md ' } } / >
<!-- Incorrecto -->
@media (max-width: 768px) {
:root { --toggle-size-md-control-height: 32px; }
}
```
### Subset por componente
Como con color, cada componente expone su subset de sizes que su
recipe soporta. Categorías:
| Categoría | Subset | Ejemplos |
|---|---|---|
| Form controls + text inputs | `xs..xl` | input, select, switch, slider, checkbox |
| Nav controls | `xs..lg` | breadcrumb, pagination, tag-group, toolbar |
| Composed panels | `sm..lg` | calendar, date-picker, file-upload, stepper, tooltip |
La categorización vive en
[`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md §12.8` ](../../../web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md ).
---
## 6. Convenciones de naming
### Tokens públicos (consumibles)
```
--{prefix}-{slot}
```
Donde `{prefix}` es uno de:
| Prefix | Significado | Ejemplo |
|---|---|---|
| `--scale-{name}-{step}` | Escala física Radix | `--scale-teal-9` |
| `--primitive-{role}-{step}` | Role → step | `--primitive-affirm-9` |
| `--color-{role}-{slot}` | Color role × slot | `--color-affirm-solid` |
| `--font-{kind}-{key}` | Tipografía | `--font-family-primary` |
| `--size-{key}-{slot}` | Size primitive | `--size-md-control-height` |
| `--space-{n}` | Spacing scale | `--space-3` |
| `--radius-{key}` | Radius scale | `--radius-md` |
| `--shadow-{n}` | Shadow scale | `--shadow-3` |
| `--z-index-{key}` | Z-index layer | `--z-index-modal` |
| `--opacity-{key}` | Opacity | `--opacity-disabled` |
| `--duration-{key}` | Motion duration | `--duration-fast` |
| `--ease-{key}` | Motion ease | `--ease-out` |
| `--style-{name}-*` | Typography named style | `--style-h1-font-size` |
| `--{c}-{slot}` | Component recipe token | `--toggle-radius-md` |
| `--{c}-{role}-{slot}` | Component color | `--toggle-affirm-solid` |
| `--{c}-palette-{slot}` | Component palette runtime | `--toggle-palette-solid` |
### Tokens privados (componente-internal)
```
--_{c}-{slot}
```
El prefijo `_` significa: NO consumes esto desde fuera del recipe del
componente. Es interno. Ejemplo:
```css
[data-toggle] {
--_toggle-bg: var(--toggle-solid-bg); /* privado */
--_toggle-on-bg: var(--toggle-palette-solid); /* privado */
}
```
### Reglas estrictas
1. **Todos los public tokens del Eidos llevan el prefijo `--`** sin
sub-prefijo de capa. Razón: clarity en debug. Ver `--toggle-bg` y
sabes que es Eidos. Ver `--bg` y no sabes de dónde viene.
2. **NUNCA usar `--eidos-`** como prefijo. La capa ya está implícita
en el path `$uix/eidos/components/{c}` .
3. **NUNCA usar `--soma-` ni `--air-` ni `--terra-`** . Esas capas son
muertas o no poseen tokens.
4. **Los component tokens siguen el patrón** `--{component-kebab}-...` .
El componente kebab es el nombre del directorio.
5. **No abreviar nombres de componente** . `dropdown-menu` no se vuelve
`ddmenu` . La authorship clarity vale 6 chars.
6. **NO incluir el segmento "color-"** intermedio en tokens de color.
`--toggle-affirm-solid` (correcto), `--toggle-color-affirm-solid`
(deprecado 2026-05-27).
7. **Slots siguen vocabulario fijo** : `track, element, hover, active,
border, solid, solid-hover, text, contrast` (de capas 3-5),
`bg, fg, border, on-bg, on-fg, on-border, hover-bg, on-hover-bg`
(de capas 6-7).
### Tokens generados vs autoría
Tokens en `generated/base.css` son **output** . Para añadir uno nuevo,
editas:
- `lib/themes/base.ts` para primitives, scales, theme variants.
- `lib/recipes/base.ts` para tokens de componente.
Y corres `npm run generate:eidos-css` .
---
## 7. Token Scope Contract (TSC)
> Eidos does not infer token scope from emitted CSS. Token scope is
> part of the source contract. The generator emits CSS from scoped
> declarations and validates that every token dependency is available
> in the consumer scope.
TSC es la pieza arquitectónica que distingue a Eidos de Tailwind /
Radix / Chakra / Mantine / shadcn. Resuelve un problema sutil pero
crítico que ningún otro sistema cierra estructuralmente.
### El problema que resuelve
CSS custom property substitution es **eager** , no lazy:
```css
:root {
--base: black;
--derived: var(--base);
}
.x { --base: red; }
.y { background: var(--derived); }
```
¿Qué color tiene `.x.y` ? **NEGRO** , no rojo. `--derived` se computa
en `:root` con `--base=black` y se hereda como `black` . El override
de `.x` sobre `--base` no afecta a `--derived` ya congelado.
Aplicado al Toggle pre-TSC:
```css
:root {
--toggle-palette-solid: var(--toggle-color-neutral-solid);
--toggle-solid-on-bg: var(--toggle-palette-solid); /* CONGELADO */
}
[data-toggle][data-color='affirm'] {
--toggle-palette-solid: var(--toggle-color-affirm-solid); /* INÚTIL */
}
```
`--toggle-solid-on-bg` quedaba congelado al neutral. El toggle con
`data-color='affirm'` mostraba gris en vez de teal. **Bug
arquitectónico** que ningún linter detectaría.
### Cómo TSC lo cierra
El config del recipe declara **dónde** se emite cada token:
```ts
recipes.toggle = {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-color-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-color-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-color-threat-solid)', scope: 'color:threat' }
]
},
'solid-on-bg': {
value: 'var(--toggle-palette-solid)',
scope: 'host' // ← obligatorio: dep está en 'host', no en 'root'
}
}
```
El generador:
1. **Infiere `depends`** parseando `var(--{c}-XXX)` del value.
2. **Valida transitivamente** : `solid-on-bg` (scope `host` ) depende
de `palette-solid` (scope `host` o más específico) — OK.
3. **Emite cada declaración bajo su selector** : `host` → `[data-toggle]` ,
`color:affirm` → `[data-toggle][data-color='affirm']` , etc.
4. **Falla el build** si el scope del consumer no cubre el del dep.
### Scopes disponibles
| Scope | Selector generado | Cuándo usar |
|---|---|---|
| `'root'` | `:root` | Token estable. Default para bare-string. |
| `'host'` | `[data-{c}]` | Token referencia `var(--{c}-palette-*)` u otro `host` token. |
| `color:${v}` | `[data-{c}][data-color='${v}']` | Override del palette por color value. |
| `variant:${v}` | `[data-{c}][data-variant='${v}']` | Cascada de variante. |
| `state:${v}` | `[data-{c}][data-state='${v}']` | Cascada de estado. |
| `size:${v}` | `[data-{c}][data-size='${v}']` | Cascada de tamaño. |
| `event:${v}` | `[data-{c}][data-event='${v}']` | Token de motion ligado a señal perceptual. |
| `[axis:v, …]` | `[data-{c}][data-X='v'][data-Y='w']` | **Composite** — múltiples condiciones ANDed. |
### Tres formas de declarar un token
```ts
recipes.toggle = {
// (1) Forma corta — scope 'root' implícito (token estable)
'height-md': '32px',
// (2) Forma simple — una declaración con scope explícito
// depends se infiere automáticamente de var() en el value
'solid-on-bg': {
value: 'var(--toggle-palette-solid)',
scope: 'host'
},
// (3) Forma multi-declaración — el MISMO token bajo distintos scopes
// (la realidad CSS de un custom property redeclarado por cascada)
'palette-solid': {
declarations: [
{ value: 'var(--toggle-color-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-color-affirm-solid)', scope: 'color:affirm' }
]
}
};
```
### Álgebra de scope (`scopeCovers`)
No es un orden total simple. La regla es:
> `consumer scopeCovers dep` ⇔ todo elemento que matchea el consumer's
> scope también matchea el dep's scope.
Equivalentemente: las constraints del dep deben ser un **subconjunto**
de las constraints del consumer.
| consumer | dep | covers? | Razón |
|---|---|---|---|
| `host` | `root` | ✓ | host es más específico, root siempre aplica |
| `host` | `host` | ✓ | mismo scope |
| `host` | `color:affirm` | ✗ | consumer no constraint el color |
| `color:affirm` | `host` | ✓ | host cubre todo el host scope |
| `color:affirm` | `color:affirm` | ✓ | mismo scope |
| `color:affirm` | `color:loss` | ✗ | scopes incompatibles (diferentes values del mismo axis) |
| `color:affirm` | `size:lg` | ✗ | consumer no constraint el size |
| `[color:affirm, size:lg]` | `color:affirm` | ✓ | composite cubre cada componente |
| `[color:affirm, size:lg]` | `size:lg` | ✓ | igual |
### Cross-axis collision detection
Si un token tiene declaraciones en axes incomparables (e.g.
`color:affirm` y `state:on` ), un elemento con ambos atributos matchea
ambos bloques. El cascade winner depende de orden de declaración —
silent correctness bug.
El generador detecta esto y **exige una declaración composite** que
desambigüe:
```ts
'bg': {
declarations: [
{ value: 'red', scope: 'color:affirm' },
{ value: 'blue', scope: 'state:on' },
{ value: 'purple', scope: ['color:affirm', 'state:on'] } // ← obligatorio
]
}
```
Sin la composite, build falla:
```
Eidos recipe scope contract violations:
- synth.bg: declarations at scopes color:affirm and state:on can both
apply to the same element. Add an explicit composite declaration
[color:affirm, state:on] to disambiguate cascade order.
```
### Multi-part scope — `parts: [...]` (TSC v2.2)
Cuando `data-color` (u otro axis TSC) NO vive en el root del componente
sino en parts específicos, el generador emite una regla con selector
comma-separado:
```ts
// recipes.select._accent-track
{
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-track)', scope: 'host' },
{ value: 'var(--select-affirm-track)', scope: 'color:affirm' }
]
}
```
Genera:
```css
[data-select-trigger], [data-select-content] {
--_select-accent-track: var(--select-primary-track);
}
[data-select-trigger][data-color='affirm'], [data-select-content][data-color='affirm'] {
--_select-accent-track: var(--select-affirm-track);
}
```
**Cuándo usarlo**: el componente porta `data-color` per-part (típicamente
porque un part viaja por portal y se renderiza fuera del árbol DOM del
otro). Single-part components siguen sin necesitar `parts` — el default
`[data-{c}]` es lo correcto.
**Quién lo usa hoy**: `select` (trigger + content) — único caso real
en el catálogo. Los demás componentes con `data-color` lo declaran en
el root.
### Cross-recipe composition — `composition: { ... }` (TSC v2.2)
Cuando un recipe necesita modificar tokens de OTRO recipe scoped a su
propio cascade, declara un bloque `composition` sibling de los tokens
regulares:
```ts
// recipes.toggle-group
{
gap: 'var(--space-1)',
composition: {
toggle: { // foreign recipe name
targetSelector: '[data-toggle-group-item]', // descendant selector
tokens: {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-solid)', scope: 'color:risk' }
]
}
}
}
}
}
```
Genera:
```css
[data-toggle-group][data-color='affirm'] [data-toggle-group-item] {
--toggle-palette-solid: var(--toggle-affirm-solid);
}
[data-toggle-group][data-color='risk'] [data-toggle-group-item] {
--toggle-palette-solid: var(--toggle-risk-solid);
}
```
Reglas:
- El CSS variable name se deriva del recipe FORÁNEO
(`--toggle-palette-solid`), no del host. Para tokens privados del
foreign use `_palette-solid` → `--_toggle-palette-solid` .
- El selector es `{host's scope-rule} {targetSelector}` — combinación
ancestor + descendant.
- Las composition declarations DEBEN tener scope ≠ `'root'` . Un
override no-scoped pertenece al foreign recipe, no al composition
block. El validador rechaza root-scoped composition entries.
- Composition NO se valida con el algebra de scope del host (las
composition entries modifican TOKENS del foreign, no del host), pero
sí pasa por el mismo pipeline de validación general
(`validateRecipeComposition`).
**Quién lo usa hoy**: `toggle-group` (modifica `--toggle-palette-*` en
sus items). Pattern reutilizable para futuros wrappers compositivos
(button-group, nav-menu).
### Pipeline de defensas (5 capas)
```
1. tsc --noEmit ← TS bien tipado
2. TSC scope algebra ← ningún token depende de scope más dinámico
3. TSC cross-axis check ← composites obligatorios donde hay collision
4. eidos-lint ← defensa secundaria del CSS generado
5. runtime probe ← confirma comportamiento real en browser
```
---
## 8. Cómo añadir un componente nuevo
Asumiendo que ya tienes el morfo, soma y el scaffolding del wrapper
eidos (`src/uix/eidos/components/{name}/`):
### Paso 1 — Decide qué tokens necesitas
Mira componentes similares (`button`, `toggle` , `switch` ). Identifica
qué dimensiones tu componente expone:
- ¿Tiene `data-color` ? → palette tokens.
- ¿Tiene `data-variant` ? → variant tokens.
- ¿Tiene `data-size` ? → size tokens.
- ¿Cuántas partes tiene? → tokens por part.
### Paso 2 — Añade el recipe en `lib/recipes/base.ts`
```ts
// Within THEME_BASE_RECIPE_TOKENS:
'my-component': {
// size tokens — scope 'root' (estables)
'height-md': '36px',
'padding-inline-md': 'var(--space-3)',
'gap': 'var(--space-2)',
'radius': 'var(--radius-md)',
// per-color literal definitions — scope 'root'
'primary-solid': 'var(--color-primary-solid)',
'affirm-solid': 'var(--color-affirm-solid)',
'threat-solid': 'var(--color-threat-solid)',
// palette dinámica — scope 'host' default + overrides por color
'palette-solid': {
declarations: [
{ value: 'var(--my-component-primary-solid)', scope: 'host' },
{ value: 'var(--my-component-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--my-component-threat-solid)', scope: 'color:threat' }
]
},
// tokens derivados — scope 'host' (deps inferidas)
'solid-bg': {
value: 'var(--my-component-palette-solid)',
scope: 'host'
}
}
```
### Paso 3 — Regenera
```bash
npm run generate:eidos-css
```
Si tu config viola TSC, te avisa al regenerar:
```
Eidos recipe scope contract violations:
- my-component.solid-bg (scope root): dependency 'palette-solid' is only
declared at scopes [host], none of which is reachable from the
consumer's scope.
```
### Paso 4 — Escribe el recipe CSS
`src/uix/eidos/components/my-component/my-component.css` :
```css
[data-my-component] {
/* Private tokens — sólo este recipe los lee */
--_my-component-bg: var(--my-component-solid-bg);
--_my-component-radius: var(--my-component-radius);
display: inline-flex;
align-items: center;
padding-inline: var(--my-component-padding-inline-md);
height: var(--my-component-height-md);
border-radius: var(--_my-component-radius);
background: var(--_my-component-bg);
gap: var(--my-component-gap);
}
[data-my-component][data-disabled] {
opacity: var(--opacity-disabled);
pointer-events: none;
}
```
### Paso 5 — Importa en `index.css`
```css
@import './components/my-component/my-component.css';
```
### Paso 6 — Verifica
```bash
npm run generate:eidos-css
npm test -- src/uix/eidos
npm run morfo:check
node --import tsx/esm scripts/eidos-lint.ts my-component
```
### Anti-pattern común: declarar tokens compuestos en `:root`
```ts
// ❌ INCORRECTO — TSC fallará
'my-component': {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-bg': 'var(--my-component-palette-solid)' // ← scope 'root' implícito, deps en 'host'
}
// ✓ CORRECTO
'my-component': {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-bg': {
value: 'var(--my-component-palette-solid)',
scope: 'host'
}
}
```
---
## 9. Cómo definir un theme
Eidos soporta **3 modos** de definir un theme:
### Modo 1 — Patch del theme base (recomendado)
Cambia sólo lo que necesitas; el resto sigue el base:
```ts
import { ActiveEidos } from '$uix/eidos';
ActiveEidos.create({
themeBase: {
semantics: {
color: {
roles: {
primary: 'blue', // primary usa la escala blue Radix
secondary: 'plum'
}
}
},
primitives: {
typography: {
families: {
primary: { family: 'Inter' }
}
}
}
},
applyDom: true
});
```
### Modo 2 — Config completa
Si quieres autoría desde cero:
```ts
import { ActiveEidos, defineEidosConfig } from '$uix/eidos';
const config = defineEidosConfig({
primitives: { /* … */ },
semantics: { color: { /* … */ } },
themes: { /* … */ }
});
ActiveEidos.create({ config, applyDom: true });
```
### Modo 3 — Theme CSS-only (sin TypeScript)
Eidos publica el contrato como CSS vacío para que externals lo
sobrescriban:
```ts
const contract = activeEidos.renderContractCss({
themeSelector: "[data-theme='acme-light']"
});
// Output:
// [data-theme='acme-light'] {
// --scale-blue-9: ;
// --color-primary-solid: ;
// --size-md-control-height: ;
// ...
// }
```
El consumer rellena los valores:
```css
[data-theme='acme-light'] {
--scale-blue-9: #006adc ;
--color-primary-solid: var(--scale-blue-9);
--size-md-control-height: 38px;
}
```
Y carga ese CSS junto con el de Eidos. Con `themeSource: 'css'` ,
`ActiveEidos` no genera theme propio.
### Persistencia versionada
```ts
const document = activeEidos.toDocument();
// → { kind: 'uix.eidos-config', version: 1, options: {...} }
const json = activeEidos.serialize();
localStorage.setItem('user-theme', json);
// Más tarde
const hydrated = createActiveEidos({
config: JSON.parse(localStorage.getItem('user-theme')!),
prefs, dom
});
```
El document envelope tiene `version` para migraciones futuras.
---
## 10. Cómo overridear tokens en runtime
`ActiveEidos.setCssVariables()` permite override runtime contract-aware:
```ts
activeEidos.setCssVariables({
'--color-primary-solid': 'rebeccapurple',
'size-md-control-height': '40px', // sin -- también vale
'shadow-3': '0 10px 28px rgb(20 20 20 / 0.16)'
});
```
Eidos:
1. **Valida** cada nombre contra `getCssContract()` . Tokens fuera del
contrato lanzan error en modo `strict` (default).
2. **Renderiza** transacionalmente: primero render + valida, luego
reemplaza el `<style>` block runtime.
3. **Aplica** los overrides bajo `:root` (o el selector que pases).
Para variables fuera del contrato (locales de la app):
```ts
activeEidos.setCssVariables(
{ '--my-app-custom': 'value' },
{ strict: false }
);
```
### Builders de sistema completo
Por encima de `setCssVariables` hay dos builders que derivan un sistema entero desde una
semilla y lo escriben como bloque gestionado (siguen el tema activo light/dark):
- **`eidos.applyColorScheme(seed, opts)`** — deriva las 31 escalas + 9 roles desde un color
de marca (`buildScheme`). `clearColorScheme()` revierte.
- **`eidos.applyTypeScale(seed, opts)`** — deriva los 8 `--font-size-*` desde un ratio
modular + base (`buildTypeScale`), opcionalmente fluido (`ratioMax`). `clearTypeScale()`
revierte.
Ambos son puros en `eidos/lib` (`build-scheme` / `build-type-scale` ) + un método de
aplicación en `ActiveEidos` . Demos en vivo: `/temas/color` y `/temas/tipografia` .
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
---
## 11. Bundle strategy + `eidos:purge`
`generated/base.css` contiene tokens de TODOS los componentes del
catálogo (~95 components). En producción una app típica usa 5-20.
### El tool
```bash
npm run eidos:purge -- \
--src 'src/**/*.svelte' \
--src 'src/**/*.ts' \
--src 'src/**/*.css' \
--output dist/eidos.purged.css \
--verbose
```
### Cómo decide qué mantener
1. **Foundation siempre kept** : scale, primitive, color, size, opacity,
z-index, shadow, border, radius, space, density, motion, icon,
typography, layout. ~1100 tokens (~95 KB raw / ~11 KB gzip).
2. **Source-scan tokens** : cada `var(--XXX)` y `--XXX:` declaración
encontrada en source → `XXX` pinned.
3. **Component-import detection** : cada `from '...components/{c}'` →
recipe completo de `{c}` pinned.
4. **Data-attr detection** : cada `data-{c}=` (filtrado contra registry
canonical de recipes) → recipe completo de `{c}` pinned.
5. **Cierre transitivo** : si X pinned y X→`var(--Y)`, Y pinned.
Iteración hasta fixed point.
### Resultados medidos
| Perfil | Components | Raw before | Raw after | Reducción | Gzip after |
|---|---|---|---|---|---|
| Minimal (toggle+button+badge) | 3 | 217.7 KB | 97.4 KB | ** − 55%** | 11.1 KB |
| SaaS típico (10 components) | 10 | 217.7 KB | 116.3 KB | ** − 46%** | 13.6 KB |
| 5 páginas demo UIX | 7 | 217.7 KB | 107.3 KB | − 51% | 12.4 KB |
| Exhaustivo (todos) | 62 | 217.7 KB | ~217 KB | − 0.4% | ~25 KB |
**Piso arquitectónico**: ~95 KB raw / ~11 KB gzip (foundation que
toda app necesita).
### Cuándo usarlo
- **En producción**: SIEMPRE. Integra en tu build pipeline.
- **En dev**: opcional. El raw 226 KB es aceptable para iteración local.
- **En SSR**: pre-purge una vez por build, no per-request.
### Limitaciones conocidas
- **Dynamic component selection**: si tu app importa componentes
dinámicamente (`await import(...)`), el scanner los puede perder.
Mitigación: pasa los nombres via `--keep my-component` .
- **`var()` en strings dinámicos**: si construyes `var(--${name})` en
runtime, el scanner no lo ve. Mitigación: declara los nombres
estáticamente en algún archivo escaneable.
---
## 12. Herramientas de validación
| Tool | Comando | Qué valida |
|---|---|---|
| **morfo:check** | `npm run morfo:check` | DOM contracts vs morfo declarations (Playwright walk de 107 demos) |
| **eidos-lint** | `node scripts/eidos-lint.ts {c}` | Recipe CSS selectors vs morfo enum values |
| **eidos-lint-all** | `node scripts/eidos-lint-all.ts` | Igual, todos los componentes |
| **TSC validation** | `npm run generate:eidos-css` (implícito) | Scope algebra + cross-axis collision detection |
| **recipe-css-contract** | `npm test -- recipe-css-contract` | Recipe tokens consumidos + TSC v2 scenarios (17 tests) |
| **component-api-contract** | `npm test -- component-api-contract` | Public API surface por componente |
| **component-visual-attrs** | `npm test -- component-visual-attrs` | Visual data-attrs que el wrapper emite |
| **generated-css** | `npm test -- generated-css` | Estructura del CSS generado |
### Pipeline de validación recomendado pre-commit
```bash
npm run generate:eidos-css # Si tocaste recipes/themes
npm test -- src/uix/eidos # 99/99 tests
npm run check # TS check
npm run morfo:check # DOM contracts (requiere dev server)
node scripts/eidos-lint-all.ts # CSS drift safety net
```
---
## 13. Integración con Sema (`event:*` scope)
fix: address architectural audit findings + sync ecosystem docs
Audit: src/audit-opus-4-6-26.md. All non-words findings remediated.
Code:
- E1: navigation-menu indicator data-state visible -> open (render bug; the
active underline was permanently invisible). Clears the only invalid lint selector.
- SO1 + E2: raw `new ResizeObserver` -> ActiveDom.observeResize in carousel-provider
and the canvas-text useContainerWidth hook (+ s-text / s-text-virtual-list pass
eidos.dom). iframe/popup-safe, lifecycle-tracked.
- A1: defineUixServices now registers `motion`, so attach-mode app.motion is real
and the active-uix fallback becomes the true edge case (test guard updated).
- A2/A4: contracts.ts pins `motion` + `announce` in ActiveUixServiceContract +
publicSurface; dispose() comment corrected.
- SO2: carousel drops the hardcoded `transform 300ms ease-out` (the recipe already
handles it via [data-dragging]); also fixed the recipe's undefined `--duration-base`
token -> `--duration-slow` (it was masked by the inline).
- S1: HapticChannel reduced-motion via an injected ActiveDom port (mirrors
SoundChannelDom) instead of global matchMedia.
- T1: 10 sites repointed `$libs/dom` -> `$adom` (sema x5 + its tests x3, active-uix
value import, arts/prefs).
- M2 / S2 / S3: dead code removed (button `states:['idle','loading']`,
SemaRuntimeChannelId, SEMA_VALENCED_FAMILY_LIST).
Docs:
- Motion-as-service reflected across the ecosystem: CLAUDE.md (aliases + arch +
service note), arts/README, active_architecture, soma SOMA_ARCHITECTURE, eidos
README, eidos-motion.md.
- X1: CLAUDE.md "5 canonical channels" (false) -> the single canonical narrative
(8 book channels; Sema runs 2 + visual meta-channel, Eidos materializes 5).
- X2/X3/X4: sema/README (8 families + intentRequirement/intentGuidance split),
types.ts JSDoc (SEMA_INTENT_POLICY -> SEMA_FAMILY_POLICY), engine.ts cascade 6->5,
alias table ($frontend out, $lang->$langs, +$clipboard).
- E4: codex_audit.md HISTORICO banner.
Deferred: SU2 (test-only layering, not a build violation); Words M1/T5/E3 (WIP).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; sema 152/152,
contracts 31/32 (1 pre-existing words), carousel 4/4, motion 22/22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
> ⚠️ **Superseded (§13 + §14).** El modelo de motion vigente es el de **dos
> momentos** documentado en [`eidos-motion.md`](./eidos-motion.md) (F1– F7): el
> momento `--event` (la firma perceptiva) se declara en `motion.signatures` y se
> genera como CSS contra `data-event-*` directamente — sin el scope TSC `event:*`
> ni el `data-motion-ref` que estas secciones discuten. El motor (`EngineMotion`)
> es un servicio en `arts/motion` (`uix.motion`). Se conservan como contexto
> histórico de la decisión; no son la API actual.
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 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.
---
## 14. Motion (estado actual)
### Lo que YA tienes
| Locación | Cobertura |
|---|---|
| `archetypes.css` | 4 transitions baseline (trigger, indicator, thumb, close) |
| `events.css` | 9 keyframes + reactions a `data-event-*` con intent tinting |
| `components/{c}/{c}.css` | transitions/animations específicas del componente |
Total: ~30 keyframes únicos distribuidos en el árbol + transitions
inline en cada recipe.
### Lo que ESTÁ deferred (eidos-motion.md)
Documento de propuesta sin implementar. Define:
- Atributo `data-motion-ref` (NO existe)
- Registry tipado `EidosConfig.motion` (NO existe)
- Drivers `css / eidos-rect / waapi`
**Estado**: superseded por TSC scope `event:*` para el caso de tokens
ligados a señales. Los drivers `eidos-rect` (medición de rects) y
`waapi` (keyframes runtime) siguen diferidos hasta que aparezca un
consumer real (e.g. `presence.genie` fly-to-target).
### Cuándo añadir un keyframe nuevo
Añade a `events.css` si:
- Reacciona a una señal perceptual concreta (`data-event` o `data-event-family` ).
- Es transversal (varios componentes pueden compartirlo).
Añade al recipe del componente si:
- Es específico (un slider drag, una calendar swap).
- No reacciona a una señal de sema, sino a un `data-state` transition.
---
## 15. Comparación con librerías de referencia
### Bundle size (app típica con 10 componentes)
| Sistema | Raw | Gzip | Strategy |
|---|---|---|---|
| Tailwind v4 | ~30 KB | ~10 KB | JIT atomic classes |
| **Eidos + purge** | **116 KB** | **13.6 KB** | **JIT custom-property purge** |
| Chakra Panda v3 | ~30 KB | ~12 KB | Build-time JIT |
| Mantine | ~60 KB | ~18 KB | Sin purge |
| Radix Themes | ~80 KB | ~22 KB | Sin purge |
| shadcn/ui | varía | varía | Copy-paste, no centralized |
| **Eidos sin purge** | **218 KB** | **25 KB** | Single CSS |
### Theming features
| Feature | Eidos | Radix Themes | Chakra Panda | Mantine | shadcn | Tailwind v4 |
|---|---|---|---|---|---|---|
| **Token scope as data** | ✅ TSC | ❌ implícito | ⚠️ build-time | ❌ runtime | ❌ N/A | ❌ N/A |
| **Auto-inferencia de deps** | ✅ `var()` parse | ❌ | ✅ types | ❌ | N/A | N/A |
| **Cross-axis collision** | ✅ explícito | ❌ | ⚠️ partial | ❌ | N/A | N/A |
| **Composite scopes** | ✅ `[axis:v, …]` | ❌ | ✅ conditional pairs | ❌ | N/A | N/A |
| **9 color roles canónicos** | ✅ libro | ⚠️ 6 accents | ❌ open | ❌ open | ⚠️ 4 roles | ❌ open |
| **6 size canon coordinated** | ✅ | ⚠️ 1-3 | ⚠️ 5 | ⚠️ 5 | ❌ | N/A |
| **Density runtime** | ✅ 3 levels | ❌ | ❌ | ⚠️ partial | ❌ | ❌ |
| **Contract introspection** | ✅ typed | ⚠️ docs | ✅ Panda | ⚠️ docs | ❌ | ❌ |
| **Runtime override** | ✅ contract-aware | ⚠️ via CSS vars | ❌ | ✅ CSSVarsProvider | ⚠️ via CSS | ❌ |
| **Persistence versioned** | ✅ envelope | ❌ | ❌ | ❌ | ❌ | N/A |
| **Perceptual layer (sema)** | ✅ unique | ❌ | ❌ | ❌ | ❌ | ❌ |
### Mental model
| Sistema | Token philosophy |
|---|---|
| Eidos | 7 layers de indirección, scope-as-contract, perceptual integration |
| Radix Themes | 3 layers, accent runtime swap, no scope contract |
| Chakra Panda | Conditional values build-time, recipe system |
| Mantine | Theme provider runtime, string interpolation |
| shadcn | Plano `--primary` + `.dark` , copy-paste components |
| Tailwind v4 | `@theme` directive, atomic utilities, no tokens compuestos |
**Lectura crítica**: Eidos NO es más simple que Tailwind ni más
ergonómico que shadcn. Es **más expresivo** en la dimensión "qué
puede comunicar un componente". Si tu app solo necesita un color
primario y un dark mode, shadcn es la respuesta. Si tu app necesita
diferenciar perceptualmente entre "guardar borrador" (affirm) y
"borrar permanentemente" (loss) con tokens y animaciones distintas,
Eidos es el sistema.
---
## 16. Anti-patterns que NO debes cometer
### A. Declarar tokens derivados en `:root`
```ts
// ❌ INCORRECTO — el bug del Toggle pre-TSC
'palette-solid': { value: '...', scope: 'host' },
'solid-on-bg': 'var(--my-component-palette-solid)' // scope 'root' implícito
```
TSC lanza error al regenerar. Fix: `scope: 'host'` en el consumer.
### B. Usar nombres con segmento "color-" redundante
```ts
// ❌ DEPRECATED (2026-05-27)
'color-affirm-solid': 'var(--color-affirm-solid)'
// ✅ CORRECTO
'affirm-solid': 'var(--color-affirm-solid)'
```
### C. Inventar roles fuera del canon
```ts
// ❌ NO — success/danger/warning/info son de otros modelos
'success': 'green',
'danger': 'red'
// ✅ Usa los 9 canónicos
'fulfill': 'green', // success → fulfill
'threat': 'red' // danger → threat
```
### D. Definir media-queries que cambien tokens canónicos
```css
/* ❌ NO — md cambia significado por viewport */
@media (max-width: 768px) {
:root { --size-md-control-height: 32px; }
}
/* ✅ Componente elige qué size aplica por viewport */
< Toggle size = {{ base: ' sm ' , md: ' md ' } } / >
```
### E. Importar `$libs/dom` directamente en eidos
```ts
// ❌ NO
import { foo } from '$libs/dom';
// ✅ Eidos consume vía ActiveEidos.dom
const eidos = ActiveEidos.require();
eidos.dom.apply(...);
```
### F. Tocar `generated/base.css` a mano
Es output. Cualquier cambio se sobrescribe al regenerar. Si necesitas
cambiar algo, edita `lib/themes/base.ts` o `lib/recipes/base.ts` .
### G. Crear escalas físicas sueltas dentro de una app
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
Las **31 escalas** por defecto cubren las paletas razonables. Un **tema
de marca** SÍ trae su propia paleta como escalas (§25.7) — eso es
legítimo. Lo que NO debes hacer es añadir una escala one-off dentro de
una app cuando remapear un role a una escala existente ya resuelve el caso.
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
### H. Re-exportar entre layers
```ts
// ❌ NO — eidos no re-exporta soma
export * from '$soma/components/toggle';
// ✅ Cada layer expone su propio API
```
---
## 17. FAQ — decisiones polémicas
### ¿Por qué el theming no vive en Morfo? ¿No debería Morfo ser source of truth de todo?
Pregunta CRÍTICA — respuesta detallada en [sección 1.bis ](#1bis-theming-vive-en-eidos-no-en-morfo-por-diseño ).
Resumen: morfo es source-of-truth del **contrato cross-layer** (parts,
events, attrs, archetypes, attr values). Tokens/themes/recipes pertenecen
a Eidos por diseño explícito de la arquitectura (`active_architecture.md`
§9). La regla 2-de-3 lo deriva: los tokens visuales los consume solo
eidos → 1-de-3 → no entra en morfo. La integración eidos↔morfo se hace
**a través del DOM** (los recipes targetean attrs que morfo declara), NO
importando objetos morfo en TS (la regla #6 lo prohíbe explícitamente).
### ¿Por qué 7 capas de indirección? Parece excesivo
Cada capa sirve un override point real:
- Sin capa 1, no puedes traer una paleta Radix custom.
- Sin capa 2, no puedes remapear roles.
- Sin capa 3, no puedes ajustar slots per role.
- Sin capa 4, no puedes overridear un color SOLO para un componente.
- Sin capa 5, no puedes tener palette dinámico por instancia.
- Sin capa 6, no puedes combinar variant × palette.
- Sin capa 7, los recipes mezclan tokens externos con internos.
La capa 4 es la más sospechosa de redundancia. Es candidata a
deprecate si después de 6 meses ningún consumer la usa.
### ¿Por qué TSC y no simplemente convención?
**Convención falla en silencio**. El bug del Toggle (pre-TSC) habría
quedado escondido años. Con TSC, el build falla. Es la diferencia
entre "deberías hacerlo bien" y "no puedes hacerlo mal".
### ¿Por qué no usar Tailwind si es más pequeño?
Tailwind:
- No tiene roles semánticos canónicos (success/danger/warning sí pero
son del mundo Bootstrap).
- No tiene perceptual layer (sema).
- No tiene density runtime.
- No tiene contract introspection programática.
Pero si tu app es simple, ** úsalo**. Eidos justifica su complejidad
sólo cuando la app necesita las dimensiones que Eidos cubre.
### ¿Por qué no atomic classes como Tailwind?
Custom properties permiten:
- **Cascada dinámica** (palette overrides en runtime).
- **Theme switching** sin recompile.
- **Persistencia** del user's theme.
- **Composability** con sema (`event:*` scope).
Atomic classes son más comprimibles pero son estáticas. Imposible
hacer `--palette-solid` cambie con `data-color='affirm'` desde
atomic classes sin generar × N variantes en build.
### ¿Por qué inventar "TSC" y no usar @scope nativo de CSS?
`@scope` (CSS Cascading Modules L6) es bleeding-edge: Chrome 118+,
Firefox 128+, Safari aún no. No es production-ready 2026.
Cuando @scope sea universal, TSC podría re-implementarse encima de
él. La superficie del config (declarations[] + scope) seguiría igual;
sólo cambiaría el CSS emitido.
### ¿Por qué `event:` en TSC y no usar el `data-motion-ref` del doc motion?
Tres razones:
1. **TSC ya existe y funciona** . `data-motion-ref` requeriría un
atributo DOM nuevo, runtime para inyectarlo, registry separado.
2. **Sema ya emite `data-event`** . Reutilizar es 0 coste arquitectónico.
3. **Composable** : `scope: ['event:announce', 'color:affirm']` permite
diferenciar la animación según valencia. `data-motion-ref` perdería
eso o requeriría keys más complejas.
### ¿Cuál es el siguiente paso?
Pendientes deferred:
- Migrar los 5 palette consumers (button, checkbox, switch, radio-group,
toggle-group) a `declarations[]` para uniformidad.
- Implementar el primer caso real de `scope: 'event:*'` (e.g. toast
bg-during-announce).
- Decidir si colapsar la capa 4 (component-color) — defer hasta que
un consumer pida ese punto de extensión.
---
## Referencias
- [`src/uix/eidos/README.md` ](./README.md ) — el doc de arquitectura
vivo (esta info está duplicada parcialmente; este doc es la canónica).
- [`src/uix/eidos/THEMING_AUDIT_2026-05-27.md` ](./THEMING_AUDIT_2026-05-27.md ) —
el journal de cómo llegamos aquí.
- [`src/uix/eidos/eidos-motion.md` ](./eidos-motion.md ) — propuesta
motion (deferred, partially superseded by TSC).
- [`src/uix/eidos/lib/config-types.ts` ](./lib/config-types.ts ) — la
fuente de verdad del TSC type.
- [`src/uix/eidos/lib/render-css.ts` ](./lib/render-css.ts ) — el
generador (parsing, scope algebra, cross-axis detection).
- [`src/uix/eidos/lib/recipes/base.ts` ](./lib/recipes/base.ts ) — el
catálogo de tokens per-component.
- [`src/uix/eidos/lib/themes/base.ts` ](./lib/themes/base.ts ) — el
theme base (primitives + semantics + themes).
- [`scripts/eidos-purge.ts` ](../../../scripts/eidos-purge.ts ) — el
purge tool.
- [`src/uix/eidos/recipe-css-contract.test.ts` ](./recipe-css-contract.test.ts ) —
el test guard (17 tests).
- [`src/uix/active_architecture.md` ](../active_architecture.md ) —
el contexto UIX completo.
- [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md` ](../../docs/GUIA_IMPLEMENTACION_SEMAUIX.md ) —
la doctrina sema/perceptual (fuente del canon de 9 roles).
---
## 18. Cobertura universal de TSC
**Los 15 componentes con `data-color` están en TSC**. No hay
excepciones arquitectónicas — TSC v2.2 cubre las 3 patrones que
antes vivían fuera del modelo:
| Patrón | Solución TSC v2.2 | Componentes |
|---|---|---|
| `data-color` per-parte (no en root) | `parts: ['x', 'y']` en `RecipeTokenMultiDeclaration` (multi-part scope) | `select` (trigger + content) |
| Composite (variant × color) | `scope: ['variant:X', 'color:Y']` (TSC v2 composite) | `avatar` (root + badge) |
| Cross-recipe override desde ancestor | `composition: { foreignRecipe: { targetSelector, tokens } }` | `toggle-group` (modifica Toggle's palette) |
El guard universal `forbids palette-derived tokens at :root scope`
(en `recipe-css-contract.test.ts` ) sigue activo como defensa
secundaria en el CSS final, pero la fuente de verdad es el contrato
de tipos.
### 18.1 Cuándo se añadió cada extensión
- **Multi-part scope** (TSC v2.2): permite que un token cascadee sobre
más de un selector raíz. Necesario cuando `data-color` vive en parts
distintos por razones de portal/cascade (Select Content vive fuera
del árbol DOM del Trigger).
- **Composition** (TSC v2.2): permite que un recipe declare overrides
de los tokens de OTRO recipe, scoped a sus propias condiciones.
Necesario para wrappers compositivos (toggle-group, eventual
button-group, nav-menu, etc.).
Ambas extensiones se validan con el mismo pipeline TSC (scope
algebra + cross-axis collision detection + auto-inferred deps).
---
feat(eidos): variants canon — EIDOS_VARIANTS + per-component lint
Variants son canon del eidos, NO del theme. Decisión arquitectónica
firmemente sostenida: el vocabulario de variants (solid/outline/ghost/
soft/surface/line/pills) está fijo a nivel del framework — paralelo
a las 8 sema families del libro. Theme = retintar lo perceptualmente
fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`.
Cambios:
- lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes
canónicos (control / selection / chip / marker / tabs). Los 5 union
types se derivan via `[number]` indexed access — valor y tipo no
pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con
todos los valores canónicos + utilidades cross-component (`plain`,
`subtle`).
- recipe-css-contract.test.ts: nuevo test "variant CSS selectors per
component match the declared type union". Por cada componente:
extrae el union type de `components/{c}/types.ts` (soporta literal
unions + archetype aliases; cae a advisory mode en Extract<> y
conditional types); compara con `[data-{c}][data-variant='X']`
selectores en `{c}.css`; reporta typos y unauthorized extensions
bidireccionalmente.
- THEMING.md §19: nueva sección "Variants son canon del eidos, NO
del theme" con argumentación (portabilidad, type safety, archetypes
perceptuales paralelos a sema families), tabla de las 3 capas de
la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix
Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado.
- eidos/README.md: tabla de referencia ampliada con §19.
- CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)".
- CONTINUE.md: nota de la decisión arquitectónica.
Variants component-specific permitidos (Banner inline/overlay/
persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada
`components/{c}/types.ts` y el lint los valida contra la CSS del
componente.
Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 6
errores pre-existentes (lib/_demo, soma/components/internal, web/
routes/active) — no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
## 19. Variants son canon del eidos, NO del theme
**Posición arquitectónica firmemente sostenida**: el vocabulario de
variants (`solid`, `outline` , `ghost` , `soft` , `surface` , `line` ,
`pills` , etc.) está **fijo en el sistema** . Un theme NO puede:
- Añadir un variant nuevo (no hay un `branded` , `bubble` , `corporate`
que cada theme invente).
- Redefinir el cascade visual de un variant existente (`outline` significa
"bordered restraint" en todos los themes — solo cambia el COLOR del
border, no su geometría).
### Las 3 capas de la cebolla
| Capa | Qué es | Quién la cambia |
|---|---|---|
| **Sema families / intents** | vocabulario perceptual canon del libro (8 families × 6 intents) | NADIE — fijo |
| **Eidos variants** | archetypes visuales perceptuales (5 archetypes shared + variants component-specific) | NADIE — fijo |
| **Eidos color roles** | los 9 roles canon (`primary`/`affirm`/`risk`/…) | NADIE — fijo |
| **Eidos color palette** | qué hex es cada rol | **THEME** |
| **Tokens internos de recipe** | `--toggle-solid-bg` etc. | **App** (override puntual en `EidosConfig.recipes` ) |
**Theming = retintar lo perceptualmente fijo**. El theme cambia QUÉ
color es `affirm` , no QUÉ significa `outline` .
### Por qué fijos
1. **Portabilidad de componentes.** `<Toggle variant="outline">` debe
renderizar coherentemente en cualquier theme. Theme-defined variants
romperían eso silenciosamente — un componente que asume `outline`
no funcionaría en un theme que no lo declara.
2. **Type safety = parte del contrato.** Los consumers necesitan
`SelectionVariant = 'solid' | 'outline' | 'ghost'` para autocomplete
y TS errors. Un `Record<string, …>` extensible perdería esa garantía.
Radix Themes 3.x, Chakra v3, Mantine — todas las referencias serias
mantienen variants fijos por componente.
3. **Variants son archetypes perceptuales, paralelos a sema families.**
`solid` = "filled emphasis", `outline` = "bordered restraint",
`ghost` = "ambient transparency", `soft` = "tinted background". Eso
es vocabulario perceptual del framework — no decisión de theme.
4. **Hay 5 archetypes, no infinitos.** El catálogo se cierra; el TS
los rechaza si no son canónicos. Si emerge un nuevo archetype
genuinamente perceptual, se añade a `EIDOS_VARIANTS` — pero al
nivel del framework, no al del theme.
### Single source of truth — `EIDOS_VARIANTS`
`src/uix/eidos/lib/types.ts` declara la constante:
```ts
export const EIDOS_VARIANTS = {
control: ['surface', 'outline', 'ghost'],
selection: ['solid', 'outline', 'ghost'],
chip: ['soft', 'solid', 'outline', 'ghost'],
marker: ['solid', 'soft', 'outline'],
tabs: ['line', 'surface', 'pills']
} as const satisfies Readonly< Record < string , readonly string [ ] > >;
export type ControlVariant = (typeof EIDOS_VARIANTS.control)[number];
export type SelectionVariant = (typeof EIDOS_VARIANTS.selection)[number];
// …
```
Los 5 unions se DERIVAN de la const — el valor y el tipo no pueden
desincronizarse. Cada componente narrow al archetype apropiado:
```ts
// components/toggle/types.ts
export type ToggleVariant = SelectionVariant;
// components/accordion/types.ts
export type AccordionVariant = ControlVariant;
```
Para narrowing más fino dentro de un archetype:
```ts
export type AlertDialogCancelVariant = Extract< ButtonVariant , ControlVariant > ;
```
### Variants component-specific
Algunos componentes tienen vocabularios genuinamente únicos:
- `Banner` : `inline | overlay | persistent` (posicionamiento, no
treatment perceptual).
- `Spinner` : `bars | dots | ring` (geometría del indicador).
- `Button` : añade `'plain'` para inline/link-like — no merece archetype
propio porque solo aparece en Button + Code.
Estos viven en cada `components/{c}/types.ts` . El lint
`recipe-css-contract.test.ts > variant CSS selectors per component
match the declared type union` valida bidireccionalmente:
- CSS usa `[data-{c}][data-variant='X']` → X debe estar en el union.
- Type union declara `'X'` → CSS debería tener entries (advisory).
### Lo que un theme SÍ puede
- Cambiar paletas (`ThemeColorSet.scales`, `ThemeColorSet.roles` ).
- Cambiar shadows (`ShadowScale`).
- Cambiar typography styles (`TypographyPrimitiveSet.styles`).
### Lo que un theme NO puede
- Añadir variants. (`ThemeDefinition` no expone `recipes` .)
- Redefinir cascades visuales. (Los recipes son `EidosConfig.recipes` ,
parte del bootstrap del app, no del theme.)
- Cambiar roles de color. (Los 9 roles son canon.)
- Cambiar sizes canon. (`SIZE_PRIMITIVE_KEYS` es fijo.)
### Lo que el app SÍ puede (al boot, no per-theme)
- Override de tokens en `EidosConfig.recipes` — cambia el RESULTADO
del cascade visual, no añade un variant nuevo.
- Crear componentes wrappers propios que compongan los primitives de
eidos con className/style custom.
- Cambiar tokens en runtime via `ActiveEidos.setCssVariables()` /
`clearCssVariables()` (per-app variables CSS).
feat(eidos): variants canon — EIDOS_VARIANTS + per-component lint
Variants son canon del eidos, NO del theme. Decisión arquitectónica
firmemente sostenida: el vocabulario de variants (solid/outline/ghost/
soft/surface/line/pills) está fijo a nivel del framework — paralelo
a las 8 sema families del libro. Theme = retintar lo perceptualmente
fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`.
Cambios:
- lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes
canónicos (control / selection / chip / marker / tabs). Los 5 union
types se derivan via `[number]` indexed access — valor y tipo no
pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con
todos los valores canónicos + utilidades cross-component (`plain`,
`subtle`).
- recipe-css-contract.test.ts: nuevo test "variant CSS selectors per
component match the declared type union". Por cada componente:
extrae el union type de `components/{c}/types.ts` (soporta literal
unions + archetype aliases; cae a advisory mode en Extract<> y
conditional types); compara con `[data-{c}][data-variant='X']`
selectores en `{c}.css`; reporta typos y unauthorized extensions
bidireccionalmente.
- THEMING.md §19: nueva sección "Variants son canon del eidos, NO
del theme" con argumentación (portabilidad, type safety, archetypes
perceptuales paralelos a sema families), tabla de las 3 capas de
la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix
Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado.
- eidos/README.md: tabla de referencia ampliada con §19.
- CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)".
- CONTINUE.md: nota de la decisión arquitectónica.
Variants component-specific permitidos (Banner inline/overlay/
persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada
`components/{c}/types.ts` y el lint los valida contra la CSS del
componente.
Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 6
errores pre-existentes (lib/_demo, soma/components/internal, web/
routes/active) — no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 months ago
### Cuándo añadir un archetype nuevo
Solo si EMERGE un patrón perceptual repetido en ≥3 componentes que no
encaja en los 5 archetypes existentes. Procedimiento:
1. Documentar el archetype con 1 párrafo describiendo el affordance
perceptual (paralelo a "solid = filled emphasis").
2. Añadirlo a `EIDOS_VARIANTS` en `lib/types.ts` .
3. Export el type derivado.
4. Migrar los componentes consumidores a referenciarlo.
5. Actualizar esta sección.
### Comparación con referentes
| Lib | Variants extensibles por theme | Variants extensibles por app |
|---|---|---|
| **Radix Themes 3.x** | ❌ | ❌ (fijos por componente) |
| **Mantine 7+** | ❌ | ❌ (defaultProps + styles override) |
| **Chakra UI v3 (Panda)** | ❌ | ⚠️ via recipes config (compound variants) |
| **Ark UI** | n/a (100% headless, sin opinión) | n/a |
| **shadcn/ui** | n/a (copy-paste, no framework) | ✓ (copia + edita) |
| **activeUIX** | ❌ | ⚠️ via `EidosConfig.recipes` override (cambia tokens, no añade variants) |
activeUIX se alinea con Radix Themes y Mantine: framework con contrato
fijo, theme con flexibilidad acotada al color/spacing. La extensibilidad
extrema (Tailwind, CSS-in-JS plain) es deliberadamente NO el goal —
porque la promesa del framework es portabilidad perceptual entre apps
y themes.
---
## 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
Theming: add `scaling` zoom axis (Radix parity), separate from density
Introduce a global zoom axis independent of density, in parity with Radix
Themes' `scaling` (90/95/100/105/110%). Scaling zooms px metrics INCLUDING
typography (font-size, icon-size, space, control-height); density only moves
layout rhythm + control height and leaves text fixed. The two axes compose
multiplicatively.
- config-types: SCALING_KEYS / ScalingKey / DEFAULT_SCALING; DensityPrimitiveSet
drops the dead `scale` + `contentScale` (kept spaceScale, controlScale).
- primitives/static: STATIC_SCALING (0.9..1.1).
- render-css: appendScaledMetricDeclarations wraps metrics in
calc(<raw>[ * var(--density-x-scale)] * var(--scaling)); appendScalingDeclarations
emits --scaling-{key} + --scaling default; renderScalingBlocks emits
[data-scaling] blocks. line-height/radius/border/shadow excluded.
- config + contract: prune the removed density scalars.
- active-eidos: `scaling` / `scalingSource` options, getScaling() on the
preference source, data-scaling projection + dispose cleanup.
- docs: THEMING.md section 23 + 20.1 reconcile; README density/scaling; SCALING_RFC.md.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser cascade at 90/100/110 scales font/space/control x0.9/x1.1 and leaves
radius/border fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
> 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).
---
Theming: Radix-parity palette + intent auto-derivation + color-model docs
Two-level color model settled (THEMING.md section 25), replacing the anchor RFC:
the palette is the source (scales, directly usable, designable); hierarchy roles
alias scales explicitly; intents auto-derive from the palette by the book's
canonical convention.
- Palette library expanded 12 -> 31 scales at Radix Colors parity (exact values):
radix-scales.ts (19 added: mauve/sage/olive/sand/tomato/ruby/crimson/plum/
violet/iris/indigo/jade/grass/brown/sky/mint/lime/gold/bronze) spread into
base.ts. Each directly usable as --scale-{name}-{step}.
- Intent auto-derivation: CANONICAL_INTENT_SCALES (neutral->gray, affirm->teal,
fulfill->green, risk->amber, threat->red, loss->plum) + completeColorRoleMap.
Intents omitted from a theme role map fill from the convention (identity =
step 9); slots derive normally; override optional. ColorRoleMap: hierarchy
required, intents optional.
- Validation: hierarchy roles required; omitted intents validate the canonical
scale exists in the palette.
- index: export ScalingKey / SCALING_KEYS.
- docs: THEMING.md section 25 (full color model + decisions), section 21 marked
resolved, COLOR_MODEL_RFC resolved (anchor rejected).
- test: base library asserts 31 scales x 12 steps.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser (intents auto-derive: affirm=teal #0E9384, risk=amber #DC6803,
loss=plum #7A3AAD at step 9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
## 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` ](./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
> **Auditoría completa 2026-06-01**: [`THEMING_AUDIT_2026-06-01.md`](./THEMING_AUDIT_2026-06-01.md)
> — 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 31 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` ](./COLOR_MODEL_RFC.md ).
Theming: on-solid contrast by luminance (P2-2) + translucent role surfaces (P2-4)
Two audit P2 quality defects, fixed at engine level.
P2-2 - on-solid text illegible on light solids:
The `contrast` slot defaulted to `--color-content-on-solid` (white) for every
role. On light solids (amber/yellow, risk=orange ~2.3:1) white is sub-AA. The
engine now picks by WCAG contrast (gamma-linearized) of the role step-9: when
white fails (<3:1) it uses `--color-content-on-solid-contrast` (a dark, new
OPTIONAL `content.onSolidContrast` semantic, #1c1917 in base). Only risk flips
to dark in base (5.89:1); purple/red/teal/green keep white (convention, >=3:1).
An explicit `slots.contrast` override is still honored verbatim.
P2-4 - opaque tinted soft surfaces:
The soft variant tint (Button + Badge `{role}-soft-bg`) was opaque (step-1 track
+ opaque color-mix hover) so it did not composite over non-uniform backgrounds.
New derived tokens `--color-{role}-surface` (= a2) + `--color-{role}-surface-hover`
(= a3) are translucent by construction (compositing-inverse alpha). Button/Badge
soft consume them. Toast/Tabs untouched - they are cards, opacity is correct.
- config-types: optional onSolidContrast + CONTENT_COLOR_OPTIONAL_KEYS.
- config: content keySet allows the optional key; validator value-checks it.
- render-css: wcagContrastRatio/wcagRelativeLuminance; luminance pick; emit
on-solid-contrast + surface/surface-hover.
- contract: on-solid-contrast + surface tokens per role.
- themes/base: onSolidContrast #1c1917 (light + dark).
- recipes/base: Button + Badge soft-bg -> surface, soft-bg-hover -> surface-hover.
- docs: THEMING.md section 24 + section 22 item 2; audit P2-2/P2-4 resolved.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser runtime (risk 5.89:1 dark text, surfaces translucent rgba).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
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.
---
Theming: add `scaling` zoom axis (Radix parity), separate from density
Introduce a global zoom axis independent of density, in parity with Radix
Themes' `scaling` (90/95/100/105/110%). Scaling zooms px metrics INCLUDING
typography (font-size, icon-size, space, control-height); density only moves
layout rhythm + control height and leaves text fixed. The two axes compose
multiplicatively.
- config-types: SCALING_KEYS / ScalingKey / DEFAULT_SCALING; DensityPrimitiveSet
drops the dead `scale` + `contentScale` (kept spaceScale, controlScale).
- primitives/static: STATIC_SCALING (0.9..1.1).
- render-css: appendScaledMetricDeclarations wraps metrics in
calc(<raw>[ * var(--density-x-scale)] * var(--scaling)); appendScalingDeclarations
emits --scaling-{key} + --scaling default; renderScalingBlocks emits
[data-scaling] blocks. line-height/radius/border/shadow excluded.
- config + contract: prune the removed density scalars.
- active-eidos: `scaling` / `scalingSource` options, getScaling() on the
preference source, data-scaling projection + dispose cleanup.
- docs: THEMING.md section 23 + 20.1 reconcile; README density/scaling; SCALING_RFC.md.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser cascade at 90/100/110 scales font/space/control x0.9/x1.1 and leaves
radius/border fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
## 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` ](./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.
### 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'` .
---
Theming: on-solid contrast by luminance (P2-2) + translucent role surfaces (P2-4)
Two audit P2 quality defects, fixed at engine level.
P2-2 - on-solid text illegible on light solids:
The `contrast` slot defaulted to `--color-content-on-solid` (white) for every
role. On light solids (amber/yellow, risk=orange ~2.3:1) white is sub-AA. The
engine now picks by WCAG contrast (gamma-linearized) of the role step-9: when
white fails (<3:1) it uses `--color-content-on-solid-contrast` (a dark, new
OPTIONAL `content.onSolidContrast` semantic, #1c1917 in base). Only risk flips
to dark in base (5.89:1); purple/red/teal/green keep white (convention, >=3:1).
An explicit `slots.contrast` override is still honored verbatim.
P2-4 - opaque tinted soft surfaces:
The soft variant tint (Button + Badge `{role}-soft-bg`) was opaque (step-1 track
+ opaque color-mix hover) so it did not composite over non-uniform backgrounds.
New derived tokens `--color-{role}-surface` (= a2) + `--color-{role}-surface-hover`
(= a3) are translucent by construction (compositing-inverse alpha). Button/Badge
soft consume them. Toast/Tabs untouched - they are cards, opacity is correct.
- config-types: optional onSolidContrast + CONTENT_COLOR_OPTIONAL_KEYS.
- config: content keySet allows the optional key; validator value-checks it.
- render-css: wcagContrastRatio/wcagRelativeLuminance; luminance pick; emit
on-solid-contrast + surface/surface-hover.
- contract: on-solid-contrast + surface tokens per role.
- themes/base: onSolidContrast #1c1917 (light + dark).
- recipes/base: Button + Badge soft-bg -> surface, soft-bg-hover -> surface-hover.
- docs: THEMING.md section 24 + section 22 item 2; audit P2-2/P2-4 resolved.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser runtime (risk 5.89:1 dark text, surfaces translucent rgba).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
## 24. Correcciones P2 del engine (2026-06-02)
Dos defectos de calidad de la auditoría
([`THEMING_AUDIT_2026-06-01.md`](./THEMING_AUDIT_2026-06-01.md) P2-2, P2-4),
corregidos **a nivel engine** para que apliquen a todos los temas.
### 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.
### 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.
---
Theming: Radix-parity palette + intent auto-derivation + color-model docs
Two-level color model settled (THEMING.md section 25), replacing the anchor RFC:
the palette is the source (scales, directly usable, designable); hierarchy roles
alias scales explicitly; intents auto-derive from the palette by the book's
canonical convention.
- Palette library expanded 12 -> 31 scales at Radix Colors parity (exact values):
radix-scales.ts (19 added: mauve/sage/olive/sand/tomato/ruby/crimson/plum/
violet/iris/indigo/jade/grass/brown/sky/mint/lime/gold/bronze) spread into
base.ts. Each directly usable as --scale-{name}-{step}.
- Intent auto-derivation: CANONICAL_INTENT_SCALES (neutral->gray, affirm->teal,
fulfill->green, risk->amber, threat->red, loss->plum) + completeColorRoleMap.
Intents omitted from a theme role map fill from the convention (identity =
step 9); slots derive normally; override optional. ColorRoleMap: hierarchy
required, intents optional.
- Validation: hierarchy roles required; omitted intents validate the canonical
scale exists in the palette.
- index: export ScalingKey / SCALING_KEYS.
- docs: THEMING.md section 25 (full color model + decisions), section 21 marked
resolved, COLOR_MODEL_RFC resolved (anchor rejected).
- test: base library asserts 31 scales x 12 steps.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser (intents auto-derive: affirm=teal #0E9384, risk=amber #DC6803,
loss=plum #7A3AAD at step 9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 months ago
## 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).
- **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 **31 escalas** (valores exactos de Radix
Colors, en `lib/themes/radix-scales.ts` + `base.ts` ) — pero es "la paleta", no
"la de Radix": 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.
### 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 9 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.
### 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 (31 escalas Radix) — 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. (= Radix Themes: Radix trae su paleta, tú
puedes traer la tuya.)
---
feat(eidos): runtime theme builder API — eidos.applyColorScheme(seed)
RFC Phase 4: derive a whole-system color scheme from ONE brand seed at runtime.
Packages the demo-only builder into a first-class, tested API.
- build-scheme.ts (pure): buildScheme(seed, opts) composes the uix.color engine
(deriveScheme -> generateScale -> APCA on-solid -> compositing-inverse alpha)
into the `--primitive-{role}-*` (+ `--color-{role}-contrast`) override map.
seed -> { variables, roles }. No DOM. 6 tests.
- ActiveEidos.applyColorScheme(seed, opts) / clearColorScheme(): resolves donor
scales + background from the active theme, writes a managed `uix-eidos-scheme`
style block AFTER the theme block (wins the cascade), and RE-DERIVES on mode
change (follows light/dark). Returns BuildSchemeResult for introspection. opts:
variant (tonal|vibrant|monochrome) + temper (intent coherence, keeps hue) +
per-role overrides + selector. 4 tests (return value, intents, DOM block
ordering + clear, mode re-derivation).
- index.ts: export buildScheme + ApplyColorSchemeOptions + BuildScheme* types.
- temas/color demo: themeOverride now dogfoods buildScheme (drops the duplicated
emitRole/rgbaStr; identical output verified in-browser).
- generated/base.css: regenerated for the loss->plum role fix (binding layer
--primitive-loss-* now points at --scale-plum-*; keeps the contract test green).
- docs: THEMING.md SS26 + COLOR_ENGINE_RFC SS6.2 (status: landed) + README ref row.
Overriding the binding layer reprojects every --color-{role}-{slot} + the neutral
chrome downstream; the 31-scale palette stays put. Math in $color, composition in
eidos/lib (pure), DOM application in ActiveEidos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
## 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 31 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.
feat(eidos): runtime theme builder API — eidos.applyColorScheme(seed)
RFC Phase 4: derive a whole-system color scheme from ONE brand seed at runtime.
Packages the demo-only builder into a first-class, tested API.
- build-scheme.ts (pure): buildScheme(seed, opts) composes the uix.color engine
(deriveScheme -> generateScale -> APCA on-solid -> compositing-inverse alpha)
into the `--primitive-{role}-*` (+ `--color-{role}-contrast`) override map.
seed -> { variables, roles }. No DOM. 6 tests.
- ActiveEidos.applyColorScheme(seed, opts) / clearColorScheme(): resolves donor
scales + background from the active theme, writes a managed `uix-eidos-scheme`
style block AFTER the theme block (wins the cascade), and RE-DERIVES on mode
change (follows light/dark). Returns BuildSchemeResult for introspection. opts:
variant (tonal|vibrant|monochrome) + temper (intent coherence, keeps hue) +
per-role overrides + selector. 4 tests (return value, intents, DOM block
ordering + clear, mode re-derivation).
- index.ts: export buildScheme + ApplyColorSchemeOptions + BuildScheme* types.
- temas/color demo: themeOverride now dogfoods buildScheme (drops the duplicated
emitRole/rgbaStr; identical output verified in-browser).
- generated/base.css: regenerated for the loss->plum role fix (binding layer
--primitive-loss-* now points at --scale-plum-*; keeps the contract test green).
- docs: THEMING.md SS26 + COLOR_ENGINE_RFC SS6.2 (status: landed) + README ref row.
Overriding the binding layer reprojects every --color-{role}-{slot} + the neutral
chrome downstream; the 31-scale palette stays put. Math in $color, composition in
eidos/lib (pure), DOM application in ActiveEidos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
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
feat(eidos): depth Fase 2 (oklab rim halo) + reference-grade /temas/profundidad
Depth engine — Fase 2 (mode-adaptive mezcla):
- New `halo` cue per plane: a top-edge rim-light computed in oklab
(color-mix(in oklab, white N%, transparent); 5/7/8% on raised/overlay/modal).
The `[data-depth]` box-shadow now composes `shadow, halo`. Invisible on light
surfaces (the drop shadow leads), the lift cue on dark surfaces (where the drop
shadow barely shows) — the mode-adaptive answer to "shadow lies in dark",
scoped to the depth channel (global --shadow-* untouched).
- Wired through config-types (DepthPlane.halo) + render-css (declare + compose)
+ config validation + STATIC_DEPTH + regenerated generated/base.css.
Showcase — /temas/profundidad to reference depth (4 -> 9 sections): matches
Material elevation catalog breadth and adds the two axes it lacks (eventful +
open cage):
- Responde a cada estado — dynamic elevation, live interactive control
- La escalera de planos — z-stack of the 5 planes
- Catalogo de planos en reposo — the resting-elevation spec table, our vocabulary
- Luz vs sombra — light/dark side-by-side showing the halo mechanism
- Accesibilidad — never the only channel, reduced-motion, forced-colors, contrast
- Fix: mirror data-theme onto <html> so :root depth tokens stay mode-aware
Docs: DEPTH_ENGINE_RFC (Fase 2 + 5 done, token contract + halo), THEMING 29.
Verified: npm run check 0 errors; depth tests 50/50 (updated the box-shadow
assertion to the shadow,halo composition). Pre-existing `words` recipe-contract
failures unrelated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
`[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` .
feat(eidos): depth adoption — elevated components consume the channel (shadow + halo)
The Fase 2 rim halo now reaches real UI, not just the showcase. Each elevated
component's shadow signal (recipe tokens in recipes/base.ts + the few direct
box-shadow uses) now composes `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`.
Reaches: popover, dialog, drawer, dropdown/context/navigation-menu, menubar,
select, tooltip, card, combobox, command, link-preview, words.
Shadow signal only — z-index stays component-managed (the z bands are finer than
the 5 planos), so zero stacking risk. Verified: dialog keeps its own z-index 71
and composes drop shadow + oklab halo. Retuning a plane now retunes every
component on it.
Verified: npm run check 0 errors; eidos 183/186 (3 pre-existing `words` failures,
unrelated). Regenerated generated/base.css. THEMING 29 updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
**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.
**Atmósfera** (frost, hecha 2026-06-05): cue `blur` por plano + regla **opt-in**
`[data-depth='{plane}'][data-frost]` → superficie translúcida (`color-mix` 80%) +
`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.
**Pendiente** (menor): el cue `scrim` está disponible como token (`--depth-{plane}-scrim`) pero
sin regla cableada — el backdrop dim de los modales lo gestiona hoy cada componente.
## 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` .
**Pendiente** (futuro): adopción por componentes (hoy las recipes usan `--radius-*` con arco;
optar a `data-shape='continuous'` en las superficies rectangulares es una pasada separada, con
cuidado de no squircle-izar avatares/píldoras).
## 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.
---
**Última revisión**: 2026-06-05. Si algo en este doc no coincide con
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
el código, el código gana — pero abre un issue para que actualicemos
el doc.