7.1 KiB
| title | type | audience | status | source |
|---|---|---|---|---|
| Eidos Theming — Comparación + decisiones | notes | human + agent | current | extracted from src/uix/eidos/THEMING.md (was §15 + §17) |
Eidos Theming — Comparación + decisiones
Material E3 del theming: cómo se compara eidos con las librerías de referencia, y el FAQ de las decisiones polémicas (el porqué de las elecciones). Extraído de
THEMING.md(referencia E1).
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 | ⚠️ canon + guard¹ | ⚠️ 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 | ❌ | ❌ | ❌ | ❌ | ❌ |
¹ El bundle --size-* existe + emite, pero los componentes NO lo consumen
directamente (re-declaran su mapeo size→fuente). La auditoría 2026-06-15 formalizó
los 3 arquetipos (control · compact · dense, THEMING.md §5) y añadió un guard
de coherencia (sin literales px/rem en font-size-*/icon-size-* de recipe). El
refactor a consumir el bundle queda deferido; el guard impide la deriva. Por eso ⚠️
(canon + enforcement) y no ✅ (consumo pleno).
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.
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.
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:
- TSC ya existe y funciona.
data-motion-refrequeriría un atributo DOM nuevo, runtime para inyectarlo, registry separado. - Sema ya emite
data-event. Reutilizar es 0 coste arquitectónico. - Composable:
scope: ['event:announce', 'color:affirm']permite diferenciar la animación según valencia.data-motion-refperderí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.