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%.
|
|
|
|
|
|
- **Compatible con** persistencia versionada, themes CSS-only,
|
|
|
|
|
|
runtime overrides, dark/light, density (compact/comfortable/spacious),
|
|
|
|
|
|
reduced motion, multi-axis breakpoints.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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): jamás añades. Usa las 30 escalas Radix existentes.
|
|
|
|
|
|
- **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 | orange | Caution, warning |
|
|
|
|
|
|
| threat | red | Active danger |
|
|
|
|
|
|
| loss | purple | Posterior gravity, deep |
|
|
|
|
|
|
|
|
|
|
|
|
Cada escala genera 12 steps + 12 alpha steps = 24 tokens × 9 roles =
|
|
|
|
|
|
**216 color primitives**. Es la mayor parte del bloat del foundation.
|
|
|
|
|
|
|
|
|
|
|
|
### 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 }
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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)
|
|
|
|
|
|
|
|
|
|
|
|
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 nuevas escalas físicas
|
|
|
|
|
|
|
|
|
|
|
|
Las 30 escalas Radix cubren todas las paletas razonables. Si crees
|
|
|
|
|
|
necesitar una nueva, replantéate el role mapping antes.
|
|
|
|
|
|
|
|
|
|
|
|
### 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).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**Última revisión**: 2026-05-27. Si algo en este doc no coincide con
|
|
|
|
|
|
el código, el código gana — pero abre un issue para que actualicemos
|
|
|
|
|
|
el doc.
|