docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Eidos Theming — Architecture Reference
type: reference
audience: human + agent
authority: E1 reference — the theming system: mental model, token layers, roles, sizes, naming, tooling
status: current
source: migrated from src/uix/eidos/THEMING.md (2026-07-02, docs-book F7.3)
---
# Eidos Theming — Architecture Reference
> **Audience**: any dev opening the repo who needs to understand how theming
> works in UIX. It covers the mental model, the contracts, the tooling and
> the traps. If after reading it you still don't know where a new token
> goes, this doc failed — open an issue.
**TL;DR**:
- **9 canonical color roles** (`primary`, `secondary` , `tertiary` ,
`neutral` , `affirm` , `fulfill` , `risk` , `threat` , `loss` ).
- **6 canonical sizes** + `full` (`xxs..xxl`).
- **3 public token levels**: foundation (stable), per-component recipe
(overrideable), private (`--_*`, no external contract).
- The **Token Scope Contract (TSC)** decides WHERE each token is emitted
(`:root` / `[data-{c}]` / `[data-{c}][data-color='X']` / etc.) and
validates transitivity at generation time.
- **226 KB raw / 25 KB gzip** of foundation CSS by default. Use
`npm run eidos:purge` for production apps → − 46 to − 55%.
- **The color model**: a palette of 33 scales (designable) → hierarchy
roles (explicit aliases) → intents (auto-derived by the book's
convention, identity = step 9). See §25.
- **Compatible with** versioned persistence, CSS-only themes, runtime
overrides, dark/light, density (compact/comfortable/spacious), scaling
(zoom 90– 110, a separate axis), reduced motion, multi-axis breakpoints.
---
## Table of contents
1. [Mental model ](#1-mental-model )
1bis. [Theming lives in Eidos, not in Morfo (by design) ](#1bis-theming-lives-in-eidos-not-in-morfo-by-design )
2. [The layers of Eidos's CSS ](#2-the-layers-of-eidoss-css )
3. [The 7 token layers ](#3-the-7-token-layers )
4. [The 9 canonical color roles ](#4-the-9-canonical-color-roles )
5. [The size canon ](#5-the-size-canon )
6. [Naming conventions ](#6-naming-conventions )
7. [Token Scope Contract (TSC) ](#7-token-scope-contract-tsc )
8. [How to add a new component ](#8-how-to-add-a-new-component )
9. [How to define a theme ](#9-how-to-define-a-theme )
10. [How to override tokens at runtime ](#10-how-to-override-tokens-at-runtime )
11. [Bundle strategy + `eidos:purge` ](#11-bundle-strategy--eidospurge )
12. [Validation tooling ](#12-validation-tooling )
13. [Sema integration (`event:*` scope) ](#13-sema-integration-event-scope )
14. [Motion ](#14-motion )
15. [Comparison with reference libraries ](#15-comparison-with-reference-libraries )
16. [Anti-patterns you must NOT commit ](#16-anti-patterns-you-must-not-commit )
17. [FAQ — controversial decisions ](#17-faq--controversial-decisions )
18. [Universal TSC coverage ](#18-universal-tsc-coverage )
19. [Variants are eidos canon, NOT the theme's ](#19-variants-are-eidos-canon-not-the-themes )
20. [Theming-engine corrections (2026-06-01) ](#20-theming-engine-corrections-2026-06-01 )
21. [Two-level color model (RFC — RESOLVED in §25) ](#21-two-level-color-model-rfc--resolved-in-25 )
22. [Pending theming improvements ](#22-pending-theming-improvements )
23. [The `scaling` axis (global zoom) ](#23-the-scaling-axis-global-zoom--2026-06-02 )
24. [P2 engine corrections (2026-06-02) ](#24-p2-engine-corrections-2026-06-02 )
25. [The color model — palette + derived roles/intents ](#25-the-color-model--palette--derived-rolesintents-2026-06-02 )
26. [Runtime theme builder — `eidos.applyColorScheme` ](#26-runtime-theme-builder--eidosapplycolorscheme-2026-06-04 )
27. [Wide-gamut OKLCH output (default-on) ](#27-wide-gamut-oklch-output-default-on-2026-06-04 )
28. [Forced-colors accessibility + the border ramp ](#28-forced-colors-accessibility--the-border-ramp-2026-06-05 )
29. [Depth — the unified, eventful channel ](#29-depth--the-unified-eventful-channel-2026-06-05 )
30. [Shape — continuity + families + nesting ](#30-shape--continuity--families--nesting--eventful-2026-06-05 )
31. [Structure (space · density · scale) ](#31-structure-space--density--scale--space-as-rhythm-2026-06-05 )
32. [Focus ring — the parameterized two-ring model ](#32-focus-ring--the-parameterized-two-ring-model-2026-06-11 )
33. [Themeable stepper glyphs (`spin-field`) ](#33-themeable-stepper-glyphs-spin-field--2026-06-11 )
34. [`spin-field` — the stepper-field's shared visual ](#34-spin-field--the-stepper-fields-shared-visual-number-field--css-field--2026-06-11 )
35. [The scale canon — the theming audit (2026-06-15) ](#35-the-scale-canon--the-theming-audit-2026-06-15 )
36. [The canonical trigger→panel gap — a token-driven offset (2026-06-22) ](#36-the-canonical-triggerpanel-gap--a-token-driven-offset-2026-06-22 )
37. [Touch-target — 44px on touch, pointer-gated (2026-06-28) ](#37-touch-target--44px-on-touch-pointer-gated-2026-06-28 )
38. [The state layer — unified neutral feedback (2026-06-28) ](#38-the-state-layer--unified-neutral-feedback-2026-06-28 )
---
## 1. Mental model
Eidos is UIX's **visual layer** . It owns NO behavior and NO state. It reads
from the DOM what the previous layers wrote, and applies styles.
```
Morfo declares the genetics (which attrs / events / parts exist)
↓
Soma transcribes behavior (data-state, data-color, aria-*, focus, …)
↓
Sema emits signals (data-event-* during the perceptual hold)
↓
Eidos applies the visual (tokens, themes, recipes, archetypes, motion)
```
**What Eidos owns**:
- The `--*` custom-property namespace.
- The entrypoint's CSS layers (§2: generated foundation, archetypes,
events, recipes) + the theme blocks `ActiveEidos` injects.
- The `ActiveEidos` runtime that injects foundation + theme CSS.
- Tooling: generation, validation, purge, lint.
**What Eidos does NOT own**:
- Components' logical state (that's soma).
- The definition of which events exist (that's morfo).
- Firing perceptual signals (that's sema).
**The 2-of-3 rule**: a system extension (an attribute, a token, a
convention) is only justified when **at least two of the three layers**
(soma, sema, eidos) consume it. The extensions that entered with eidos's
vote: `archetype` , `events[].semantic.{family,intent}` ,
`events[].prewrite[]` , `data-starting-style` / `data-ending-style` .
---
## 1.bis Theming lives in Eidos, not in Morfo (by design)
> **This is the most frequent architectural question — and the most
> important answer for not breaking the system.**
A new dev's reasonable intuition is: *"if morfo is the cross-layer source
of truth, visual tokens should live in morfo too"*. **NO.** UIX's explicit
design says the opposite. This section exists to close the case with
citations, before the confusion drags a PR into violating the architecture.
### The two canonical quotes in the repo
**[`architecture/active-architecture.md`](../architecture/active-architecture.md) §9 (What this architecture is NOT)**:
> **Not a classic design system.** Tokens, themes and recipes belong to
> Eidos, not to the core.
**[`architecture/overview.md`](../architecture/overview.md) §2 (Eidos)**:
> The visual layer: **tokens, themes, per-component CSS recipes**, archetype
> rules, event reactions and Svelte wrappers over soma's headless
> providers…
>
> Eidos reads from the DOM what the other layers write — **it never imports
> soma or sema internals**.
Those two sentences, by themselves, close any debate about where theming
lives. If a future proposal contradicts them, the proposal must be rejected
or the canonical doc must be updated first — not after.
### The 2-of-3 rule derives it mechanically
**`active-architecture.md` §7 #12 ** and ** `overview.md` §5** say the same
thing:
> A morfo extension is only justified when **at least two of the three
> layers** (soma, sema, eidos) consume it.
Applied to theming:
| Who consumes the visual tokens? | |
|---|---|
| Soma (the behavior runtime) | ❌ no |
| Sema (perceptual signals) | ❌ no |
| Eidos (the visual layer) | ✅ yes |
| **Count** | **1-of-3** |
**1-of-3 ≠ 2-of-3 → tokens do NOT go in morfo, by rule**. The visual
integration falls into eidos automatically through the 2-of-3 discipline,
with nobody having to decide it case by case.
### What is the morfo↔theming relationship, then?
Morfo is the source of truth of the **cross-layer contract** :
- Parts (which parts exist)
- Events (which events it may fire)
- Attrs and their enumerated values (which attrs appear in the DOM, with
which values)
- Archetypes (the transversal classification)
- Declarative states
**Theming integrates with morfo in ONE PRECISE SENSE**: eidos recipes
target DOM attrs that morfo declares. Without morfo, the attrs wouldn't
exist in the DOM and the eidos selectors would be dead.
```
MORFO declares data-color.values = ['primary', 'affirm', 'threat', ...]
↓
SOMA emits < button data-color = "affirm" > (to the DOM)
↓
EIDOS recipe targets scope: 'color:affirm' → [data-toggle][data-color='affirm']
↓
BROWSER cascade resolves the CSS rule
```
The channel between morfo and eidos is **the DOM** , not TypeScript objects.
This is critical and armored by hard rule #6:
**`active-architecture.md` §7 #6 **:
> **Eidos consumes DOM and data-*, not Soma/Sema internals.** If it needs
> something, it must be declared in morfo or emitted in a sema signal.
**`overview.md` §5** repeats it verbatim.
### What eidos must NEVER do
```ts
// ❌ ARCHITECTURAL VIOLATION — Eidos importing morfo at 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;
// using validValues to validate TSC scope:'color:X'
```
Even with good intent (validating that `scope: 'color:affirm'` matches a
morfo-declared value), **this import violates rule #6** and breaks the
morfo↔eidos boundary. If you want that validation, the correct defense is
eidos-lint at the DOM/CSS level, not TS coupling.
### The correct defense: eidos-lint at the DOM/CSS level
Validating that eidos recipes target values morfo declares IS DONE, but at
the DOM/CSS level:
```bash
node scripts/eidos-lint.ts toggle
```
It classifies every `[data-*]` selector as:
- **morfo-backed** — declared in morfo; soma emits it with a valid value
- **eidos-only** — an attr added by the wrapper (data-variant, data-size)
- **invalid** — references a morfo-backed attr with a value outside the
enum → a bug
This closes the loop architecturally without cross-layer imports.
### Recap of the canonical split
| Concept | Source of truth | Justification |
|---|---|---|
| Parts (which parts exist) | **Morfo** | Cross-layer: soma emits, eidos selects, sema references |
| Events + semantic | **Morfo** | Cross-layer: soma triggers, sema dispatches, eidos reacts |
| Archetypes | **Morfo** | Cross-layer: soma emits, sema cascades, eidos selects |
| Attr values (`data-color.values`) | **Morfo** | Cross-layer: soma validates, eidos targets, sema references |
| Declarative states | **Morfo** | Cross-layer: soma emits data-state, eidos selects |
| **The 9 systemic canonical roles** | **Eidos** (`lib/themes/base.ts`) | Only eidos materializes them |
| **The 6 canonical sizes** | **Eidos** (`lib/config-types.ts`) | Only eidos coordinates them |
| **Visual variants** (solid/outline/ghost) | **The eidos wrapper** | Only eidos renders them |
| **Tokens** (`--toggle-solid-on-bg`) | **Eidos** (`lib/recipes/base.ts`) | Only eidos consumes them |
| **Themes** (light/dark/custom) | **Eidos** (`lib/themes/`) | Only eidos composes them |
| **Theme persistence** | **Eidos** (`toDocument()`) | Only eidos serializes it |
| **TSC scope axes** (color, state, variant, size, event) | **Eidos** (they refer to morfo-emitted attrs) | Hardcoded in the TSC because they are axes of the DOM contract |
### The canonical sentence
> **Morfo declares the contract. Eidos declares the theming. The DOM
> connects them.**
This is NOT a compromise. It IS the design. Morfo's purity (declarative TS,
no runtime, no visual-layer imports) DEPENDS on theming living outside.
### Future proposals that MUST be rejected citing this section
1. ** "Let's put the Toggle's visual tokens in its morfo so morfo is the
source of truth of everything"** — violates §9 and the 2-of-3 rule.
2. ** "Let's make the TSC validate `color:affirm` by importing
`toggleMorfo.data['data-color'].values` "** — violates rule #6 (eidos
does not import morfo internals in TS).
3. ** "Let's put `variant: 'solid' | 'outline'` in the Toggle's morfo"** —
violates 2-of-3 (variants are consumed by eidos alone).
4. ** "Let's define `size` in the morfo with its 6 canonical values"** —
violates 2-of-3 (the 6 sizes are the visual system; only eidos
materializes them with coordinated tokens).
If the proposal has merit, the right move is to **update the canonical doc
(`active-architecture.md` §9) FIRST** — never afterwards.
### What SHOULD enter morfo regarding theming
- A component exposing `color` as a prop → must declare
`data-color.values: ['primary', 'affirm', ...]` in its morfo. Those
values are cross-layer (eidos targets, soma emits, sema could reference).
- A component exposing `state` (open/closed) → declares
`data-state.values: ['open', 'closed']` . Same.
- A component adding purely visual attrs (`data-variant`, `data-size` ) that
NOBODY else needs → they do **NOT** go in morfo; the eidos wrapper adds
them directly.
### Consistency with the canonical docs
This section **introduces no new doctrine** . It gathers and consolidates
what was scattered across:
- [`architecture/active-architecture.md` ](../architecture/active-architecture.md )
§3 (Morfo = the single cross-layer articulation point), §7 #6 (Eidos
imports no internals), §7 #12 (the 2-of-3 rule), §9 (tokens belong to
Eidos).
- [`architecture/overview.md` ](../architecture/overview.md ) §2 (Eidos = the
visual layer with tokens), §4 (not a classic design system), §5 (Eidos
consumes DOM and data-\*).
- This same reference, §1 (Mental model) and §7 (TSC).
If any of those canonical docs contradicts this section, **the canonical
doc wins**. This section consolidates; it doesn't decide.
---
## 2. The layers of Eidos's CSS
`src/uix/eidos/index.css` is the entrypoint (the source of truth for the
order is the file itself). It imports, in order:
```
1. generated/base.css ← foundation + recipe tokens + @font -face (generated)
2. archetypes.css ← transversal rules per data-archetype
3. events.css ← reactions to data-event-* (sema)
4. components/{c}/{c}.css ← aggregated recipes: layout primitives + spin-field
```
Outside the entrypoint but part of the visual layer:
- **Code-split recipes**: most components are NOT in `index.css` — each
`.svelte` imports its own CSS and Vite emits a per-component chunk. The
ABSENCE of an `@import` is deliberate; re-adding it would double-load.
- **Shared partials** (`lib/menu-indicator.css`): imported by the component
that uses them, not by the entrypoint.
- **Themes**: CSS blocks injected at runtime by `ActiveEidos`
(`uix-eidos-theme`), not a static `@import` . `themes/fonts.css` is
superseded — the `@font-face` live in `EidosConfig` and come out in
`generated/base.css` .
### Why this order matters
- `generated/base.css` declares tokens (it doesn't style). If recipes
loaded first, the tokens wouldn't be available.
- `archetypes.css` sets the interactive baseline (cursor, hover, focus
ring). Specific recipes override.
- `events.css` reacts to `data-event-*` with `animation: @keyframes` (not
`transition` ) because signals are transient and the animation must
complete independently of the signal's lifetime.
- Specific recipes come last → higher cascade priority in equal-specificity
conflicts.
### What each layer concretely does
| Layer | Kind | Purpose |
|---|---|---|
| `generated/base.css` | `:root` + some `[data-{c}]` blocks + `@font-face` | Foundation tokens (scale, primitive, color, size, density, typography, recipe tokens) + fonts (config-driven) |
| `archetypes.css` | `[data-archetype='X']` selectors | Transversal baseline styling per archetype (the inventory is `ARCHETYPE_VOCABULARY` , morfo/types.ts) |
| `events.css` | global `[data-event-*]` hints | The compositor hint + the reduced-motion cap (the signatures live in `EidosConfig.motion` — see [motion.md ](./motion.md ) §15) |
| `components/{c}/{c}.css` | `[data-{c}-*]` selectors | The component's own recipe (aggregated or code-split; partials like `lib/menu-indicator.css` are imported by their consumer) |
### Rules for touching each layer
- **`generated/base.css`**: **NEVER hand-edit** . It is generator output. To
change it, edit `lib/recipes/base.ts` or `lib/themes/base.ts` and run
`npm run generate:eidos-css` .
- **`archetypes.css`**: add entries only when the archetype is declared in
some morfo. Low-specificity rules (one attribute).
- **`events.css`**: add reactions only for signals sema emits. Use
`animation: @keyframes` , NOT `transition` . Read `data-event-intent`
(signal-bound), NEVER `data-intent` (state-bound).
- **`components/{c}/{c}.css`**: owned by whoever maintains the component.
Follows the naming convention (§6).
---
## 3. The 7 token layers
Eidos composes an element's final color by crossing 7 levels of
indirection. Each level serves a distinct purpose:
```
┌─ Layer 1: --scale-{name}-{step} :root (stable)
│ Radix physical scales (12 steps + alpha): --scale-teal-9 = #12a594
│
├─ Layer 2: --primitive-{role}-{step} :root (stable)
│ Role → scale mapping: --primitive-affirm-9 = var(--scale-teal-9)
│
├─ Layer 3: --color-{role}-{slot} :root (stable)
│ Semantic slot: --color-affirm-solid = var(--primitive-affirm-9)
│
├─ Layer 4: --{component}-{role}-{slot} :root (stable)
│ Per-component alias: --button-affirm-solid = var(--color-affirm-solid)
│ (NOTE: the "color-" segment was dropped on 2026-05-27)
│
├─ Layer 5: --{component}-palette-{slot} [data-{c}] (DYNAMIC)
│ Per-instance dynamic palette: changes with data-color
│
├─ Layer 6: --{component}-{variant}-{slot} [data-{c}] (host) (DYNAMIC)
│ The variant × palette combination
│
└─ Layer 7: --_{component}-{slot} [data-{c}] (private)
Private token consumed directly by the recipe CSS
```
**Scope rules**:
- Layers 1-4 are constant → `:root` .
- Layer 5 changes per instance → `[data-{c}]` and
`[data-{c}][data-color='X']` .
- Layer 6 depends on 5 → it MUST live at `[data-{c}]` (the TSC enforces
it).
- Layer 7 is private → always at `[data-{c}]` .
### Why so many layers
**Not accidental.** Each hop serves an extension point:
| Layer | Overriding it allows | Usage example |
|---|---|---|
| 1 | Changing the Radix physical scale | The brand wants its own teal |
| 2 | Changing which scale a role maps to | "Affirm" uses green instead of teal |
| 3 | Changing the per-role slot mapping | Affirm's "solid" uses step 10 instead of 9 |
| 4 | Changing a component-specific token | Toggle wants its affirm distinct from the global |
| 5 | The per-instance runtime | `<Toggle color="affirm" />` swaps the palette |
| 6 | Combining variant × color | The toggle's solid variant with the affirm color |
| 7 | Recipe-internal | The recipe decides which internal token serves what |
In practice, **most apps ONLY touch layers 1-3** (brand customization).
Layers 4-7 belong to the component catalog.
### When to create a new token at each layer
- **Layer 1** (scale): an app rarely; a **brand theme** DOES bring or
extend its own palette (§25.7). The default **33 scales** cover the
general case.
- **Layer 2** (primitive): rarely. Only if you add a new canonical role
(which would change the book canon — don't).
- **Layer 3** (color): if you add a new `{slot}` (rare). The canonical slot
inventory and its default step live in the code — **single source** :
`COLOR_ROLE_SLOTS` (`lib/config-types.ts`, with each slot's rationale in
its JSDoc) + `DEFAULT_COLOR_ROLE_SLOT_STEPS` (`lib/render-css.ts`). The
list is not copied here: it already drifted twice (border-hover retired;
bg2/separator/text-strong added).
- **Layer 4** (component-color): when adding color support to a new
component. Generated automatically by `lib/recipes/base.ts` .
- **Layer 5** (palette): when the component accepts a `data-color` prop and
needs a dynamic palette. TSC `scope: 'host'` + `scope: 'color:X'`
overrides.
- **Layer 6** (variant): when a variant (`solid`, `outline` , etc.) combines
the palette + something specific. TSC `scope: 'host'` .
- **Layer 7** (private): the recipe consumes it. Convention: the `_`
prefix.
---
## 4. The 9 canonical color roles
The roles come from the book *Diseñando lo que ocurre* . THEY ARE CANON. Do
NOT invent new ones.
```
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
```
**Strict rules**:
- The 9 names are the only valid ones. Do NOT use `success` , `warning` ,
`danger` , `info` — those belong to other models (Bootstrap, etc.).
- **`primary`/`secondary`/`tertiary`** are **hierarchical** : use them when
the difference is "more vs less prominent". No evaluative load.
- **`neutral`** is the default. No semantic load.
- **`affirm`/`fulfill`/`risk`/`threat`/`loss`** are **evaluative** : they
communicate what happens with the action.
- If `intent === 'neutral'` , `color` (the hierarchy override) may apply. If
`intent` is evaluative, the `intent` WINS and `color` is ignored.
**Mapping to physical scales** (in the base theme): the standing assignment
lives in the code — **single source** : `THEME_BASE_COLOR_ROLES`
(`lib/themes/base.ts`), with each choice's rationale in its comments (e.g.
`tertiary: 'indigo'` is a RESERVED hierarchy slot no component consumes
yet; `loss: 'plum'` to avoid colliding with `primary: 'purple'` ). This
table was copied here twice and diverged both times (primary, risk) — hence
it is now a pointer.
> **Convention ≠ authorship.** `CANONICAL_INTENT_SCALES`
> (`lib/config-types.ts`) is the book's **convention** for auto-deriving
> intents from a palette (identity = step 9; e.g. `risk→amber`). The **base
> theme** is authorship and may deviate (e.g. `risk: 'orange'`). The
> hierarchy (`primary`/`secondary`/`tertiary`) is always the theme's
> choice. The complete model is **§25**.
The **palette** is **33 scales** of 12 steps + 12 alpha = 24 tokens each
(**792 `--scale-*` tokens** — the bulk of the foundation's bloat). On top of
it, the 9 roles alias via `--primitive-{role}-{step}` (9 × 24 = **216
primitives**).
### Why 9 roles and not 4 (like shadcn) or 14 (like Mantine)
The 9 are the result of the book's perceptual analysis:
- 3 hierarchy roles cover the "visual prominence" dimension.
- 1 neutral covers the unloaded default.
- 5 intent roles cover the five distinct evaluative valences.
Any system with fewer loses perceptual resolution. Any system with more
falls into redundancy (success vs fulfill, danger vs threat — they are not
the same).
### Per-component subset
Each component exposes its own subset of the 9. Examples:
| Component | Subset | Excludes |
|---|---|---|
| Toggle | primary, secondary, neutral, affirm, risk, threat | fulfill, loss (doesn't apply) |
| Button | all 9 | — |
| Badge | primary, secondary, neutral, affirm, fulfill, risk, threat, loss | tertiary (not canonical for it) |
Why subsets: a toggle is neither completion nor irreversible loss. Exposing
fulfill/loss in its API would be semantically wrong.
---
## 5. The size canon
```
xxs · xs · sm · md · lg · xl · xxl | full
───────────────────────────────────── ───────
6 canonical sizes (physical) 1 layout size
```
`md` is the default. `full` is not physical — it is layout semantics
(`100%` / `100vw` / `100dvh` depending on context). It generates no fixed
tokens.
Each canonical size generates coordinated tokens:
```
--size-md-control-height: 36px
--size-md-font-size: 16px /* = var(--font-size-md), 1:1. Bundle IN ADOPTION (Phase-D decision 2026-07-02; pilot: toggle) */
--size-md-font-line-height: 1.45
--size-md-icon-size: 18px /* = --icon-size-md */
--size-md-padding-inline: 12px
--size-md-padding-block: 8px
--size-md-gap: 8px
--size-md-radius: 6px
```
> **There is only one `md`** (1:1 — the 2026-06-17 override, below): the
> control bundle's `--size-md-font-size` = `var(--font-size-md)` = 16px,
> identical to the typographic scale. The earlier "two `md`s" doctrine
> (compact 14px control vs 16px body) was **revoked** — control text
> follows the typographic scale 1:1.
### The key rule: `md` does NOT change per viewport
Responsiveness decides **which active size is used** ; it does NOT redefine
the tokens. If your Toggle uses `sm` on mobile and `md` on desktop, both
tokens are available and the wrapper picks one.
```svelte
<!-- Correct -->
< Toggle size = {{ base: ' sm ' , md: ' md ' } } / >
<!-- Incorrect -->
@media (max-width: 768px) {
:root { --toggle-height-md: 32px; } /* do NOT redefine a size's token per viewport */
}
```
### Per-component subset
As with color, each component exposes the size subset its recipe supports.
Categories:
| Category | Subset | Examples |
|---|---|---|
| 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 |
The declaration itself is each component's recipe + types (demos mirror it
1:1 — the parity rule is
docs(book): F7.5 (1/2) — checklist + audit guide + demo guide moved to docs/guides/
Three English guides moved verbatim (copy + link touch-ups, no
translation): COMPONENT_COMPLETION_CHECKLIST -> guides/
completion-checklist.md, COMPONENT_AUDIT_GUIDE -> guides/
component-audit.md, DEMO_AUTHORING_GUIDE -> guides/demo-authoring.md.
Tooling moved with its doc in the same pass: docs-check I5 now parses
the checklist at the new path (verified: 0 errors, rule tables found)
and component-audit.ts's two path strings updated. One reconciliation
in the audit guide's s5, applied as link-don't-copy: the copied v1
6-tab template list (stale vs the v2 9-tab guide it links, drawer
canary vs button) is replaced by a pointer to demo-authoring.md; the
domain canaries in s7 stay. demo-authoring's harness links now reach
into web/routes/uix/lib/ (the modules stay with the code); the deleted
pendiente_color_demos.md reference is marked TODO(reconcile). Corpus
swept: building-a-component (frontmatter related: + phases 6/8),
authoring, architecture/soma, theming/reference s5-parity,
testing-and-tooling, getting-started, README (E4 rows + component-audit
row added + two stale F7.3 paths in 'I want to' fixed). Remaining
warns = links to component-guide.md, which lands in F7.5 (2/2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
[`demo-authoring.md` ](../guides/demo-authoring.md ) §6).
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
### Container→part derivation: capped at `md` (2026-06-19 norm)
When a part derives its `size` from its **container** (e.g. `Dialog.Close`
inherits the dialog's size), the part **follows the container's size only
on the steps below `md` **; at `md` and above (`lg` / `xl` / `full` ) it
**caps at the normal `md` density**. A wider container — or `full` — does
NOT fatten its controls: `full` is layout (it fills the viewport), not a
bigger control size.
| container size | part size |
|---|---|
| `xs` / `sm` | `xs` / `sm` (follows the step) |
| `md` / `lg` / `xl` / `full` | `md` (normal) |
This matches the references: Radix separates `size` (density) from `width`
(full); Mantine's `fullScreen` **ignores** `size` ; Material 3's full-screen
is a **layout type** (top app bar) with standard controls. None enlarges
controls because the dialog is `full` .
First consumer: `Dialog.Close` — `components/dialog/context.ts` publishes
the dialog's size and Close derives it. An explicit `size` prop on the part
always wins.
### The size→font mapping: universal 1:1 (2026-06-17 override)
> **Supersedes Phase 7's `control`/`compact`/`dense` archetypes.** The user
> decided an enterprise-grade framework needs **one source of truth**:
> control text follows the typographic scale **1:1** — `font-size-{size}` =
> `var(--font-size-{size})` — in ALL components. So changing
> `--font-size-md` to 15px re-adapts the whole theme without touching a
> single recipe. The `md` control is **16px** (NOT 14 anymore: the "compact
> control md=14" doctrine is revoked).
**The scale** (xs/sm/md/lg/xl/xxl): **12 · 14 · 16 · 18→20 · 24→28 ·
32→48** (`lib/primitives/typography.ts`; lg/xl/xxl are fluid `clamp()` ).
Recipes brought to 1:1 (2026-06-17): button, badge, breadcrumb, calendar,
pagination, radio-group, toolbar, file-upload, tag-group, stepper, toggle,
tooltip — plus the whole field family (field, spin/date/time/color-field,
search/password-field, select, editable, tags-input), already 1:1 since the
fields sprint.
**Legitimate exceptions** (NOT control text → 1:1 doesn't apply):
- **avatar/marker** — the font is the initial inside the circle, scaled to
the **diameter** (24→96px): a proportional glyph, not a control.
- **accordion** — the trigger is a section heading: it uses the **prose**
scale `--text-N-size` (xs→text-2 … full→text-6), its own coherent
progression.
- **words / palabras / chronos** — excluded WIP tracks.
The `--size-*` bundle (above) is **in adoption** (a Phase-D decision,
2026-07-02 — it had been orphaned for a year). The pattern is set by the
pilot (`toggle`, `lib/recipes/base.ts` ): the recipe consumes `--size-{k}-*`
where its value IS the canonical coordinate (height, font-size — identical
alias chains, zero visual change) and keeps its own value where it
**deliberately deviates** (px/gap tighter than the bundle's padding) — the
deviation stays visible instead of buried in a parallel re-declaration. The
sweep across the rest of the catalog is the open workstream; the bundle was
already **realigned to 1:1** (2026-06-29: `--size-md-font-size` =
`var(--font-size-md)` = 16px).
**Hard rule (the coherence guard)** — `recipe-css-contract.test.ts` :
> No recipe `font-size-*` / `icon-size-*` token may be a **px/rem literal**
> — it MUST reference `--font-size-*` / `--icon-size-*` (otherwise the text
> stops following the typographic scale, `--scaling` and
> `applyTypeScale()`).
It closes the hole the earlier (CSS-only) guard left — recipe-token values
are custom properties, not the `font-size:` property.
(`words`/`palabras`/`chronos` excluded — an active track.) The refactor to
*consume* the archetype bundle (instead of re-declaring the mapping) stays
a follow-up; the guard is what **prevents the drift** .
### Fields: input 1:1 + the label one step below (2026-06-17 override)
The **field** family applies the **universal 1:1** (above) to the
**input**, and adds a rule of its own for the **label** :
- **Input/control → 1:1** (like every control): **12 · 14 · 16 · 18→20 ·
24→28**.
- **Label → one typographic step below the input**: **10 · 12 · 14 · 16 ·
18**. The label is the **only** element in the system that deliberately
steps down — label↔input hierarchy, not the collapsed drift that was
rejected.
| Recipe | input | label |
|---|---|---|
| `field` (generic) | `control-font-size-*` 1:1 | `label-font-size-*` one step below → covers everything `<Field>` wraps |
| `spin-field` (number/css), `date/time/color-field` | `font-size-*` 1:1 | date/time/color: own label via CSS calc; spin: the Field's label |
| `search-field` , `password-field` , `select` , `editable` , `tags-input` | `font-size-*` 1:1 | the Field's label |
**Segmented-field labels** (only date/time/color have
`[data-X-field-label]` ):
`font-size: calc(1em - (var(--font-size-md) - var(--font-size-sm)))` — the
input (inherited `1em` ) minus one scale step (md− sm = 2px, tokenized). The
generic `Field` uses per-size `label-font-size-*` tokens.
**Untouched** (already coherent): `pin-input` (`cell-font-size-*` already
scales), `combobox` (no font of its own). **Excluded** : `words` , `chronos` .
Non-fields (button/calendar/badge…) keep their archetype.
**Pending**: at `lg` the segmented label (calc → 18) and the `Field` 's
(token → 16) diverge by 2px because the input's `lg` is **fluid** (18→20).
At xs/sm/md (fixed) they agree. Resolve by giving the segmented ones
per-size tokens, or by dropping the field input's fluid `lg` .
### The typographic scale ↔ the icon scale
They are two parallel scales — `--font-size-{name}`
(`lib/primitives/typography.ts`) and `--icon-size-{name}`
(`lib/primitives/static.ts` → `STATIC_ICON.size` ) — both scaled by density
(`× --scaling`). **They are not independent: the icon accompanies the
text.**
The optical rule was validated **by eye, not by formula** (the test bench
[`/uix/icon-scale-study` ](../../web/routes/uix/icon-scale-study/+page.svelte )):
the icon weighs *one point above the text* — `≈ font + 2` at body sizes,
growing toward `≈ line-height` at the large ones (an icon **tied to
`font-size` below, to the `line-height` above**). It is not a constant:
| size | `--font-size` (px) | line-height (px) | `--icon-size` (px) | icon − font |
|---|---|---|---|---|
| xxs | 10 | 15 | 12 | +2 |
| xs | 12 | 18 | 14 | +2 |
| sm | 14 | 20 | 16 | +2 |
| md | 16 | 23 | 18 | +2 |
| lg | 20 | 27 | 20 | 0 |
| xl | 24→28 | 34 | 32 | +4 |
| xxl | 32→48 | 50 | 52 | +4 |
(The `lg` – `xxl` headings are fluid `clamp()` ; the `font-size` column shows
the desktop max. The icon jump `lg 20 → xl 32` is faithful to the text jump
`20 → 28` — there is no in-between size because the typography has none
either.)
That is why a component **never invents icon sizes in px** : it declares
`var(--icon-size-{name})` and inherits this correlation. The canonical
`size` already pairs both axes (`--size-md-font-size` +
`--size-md-icon-size` ). Archetype nuances:
- **Control icons follow the font 1:1** (the 2026-06-17 override): each
control recipe's `icon-size-{size}` references `var(--icon-size-{size})` ,
parallel to the 1:1 font. The icon scale preserves `icon ≈ font+2` at
body (md: font 16 → icon 18). Applied to **button** + **search-field** .
**Exceptions** : `password-field` — its `icon-size-*` is NOT a glyph but
the **visibility-trigger button's box** (the glyph is 65% of it),
control-coupled on purpose; `radio-cards` — the icon follows the card's
**title** font.
- **Density ⊥ typography** (compact/comfortable/spacious): density scales
ONLY layout — `space` (`× --density-space-scale`) + `control-height`
(`× --density-control-scale`). ** `--font-size-*` and `--icon-size-*`
carry no density** — only the global zoom `--scaling` (Radix parity)
touches them. Consequence: at 1:1 the icon stays **coupled to the text at
every density** (font 16 / icon 18 constant; only the control's box
tightens: 32.4 / 36 / 40.3). That is why an icon sized from
`--control-height-*` (density-coupled) decouples from the text — an
anti-pattern unless the element IS a control (e.g. the password-field's
trigger).
- **Cards / titles** (radio-cards, empty-states): the icon **accompanies
the title's font**, never a hand-inflated size — e.g. radio-cards =
`16/16/18/18/20` (follows its title). To emphasize, raise the **title's
font** (the icon follows); don't inflate the icon.
Changing the scale = editing `STATIC_ICON` +
`components/icon/create-icon.ts` + regenerating
(`npm run generate:eidos-css`). Nothing else consumes it raw.
---
## 6. Naming conventions
### Public tokens (consumable)
```
--{prefix}-{slot}
```
Where `{prefix}` is one of:
| Prefix | Meaning | Example |
|---|---|---|
| `--scale-{name}-{step}` | Radix physical scale | `--scale-teal-9` |
| `--primitive-{role}-{step}` | Role → step | `--primitive-affirm-9` |
| `--color-{role}-{slot}` | Color role × slot | `--color-affirm-solid` |
| `--font-{kind}-{key}` | Typography | `--font-family-primary` |
| `--size-{key}-{slot}` | Size primitive | `--size-md-control-height` |
| `--space-{n}` | Spacing scale | `--space-3` |
| `--radius-{key}` | Radius scale | `--radius-md` |
| `--border-width-{key}` | Border-width scale (linear `none·thin·medium·thick·heavy` = 0/1/2/3/4) — §35 | `--border-width-thick` |
| `--ring-inset-width` | The inset-ring's default width (the inner `box-shadow` ring) — §35 | `--ring-inset-width` |
| `--shadow-{n}` | Shadow scale (drop) | `--shadow-3` |
| `--shadow-inset-{key}` | Inner-shadow / recessed (`subtle·deep`, mode-aware) — §35 | `--shadow-inset-subtle` |
| `--blur-{key}` | Blur scale (backdrop/frost, `none·sm·md·lg·xl·xxl` ) — §35 | `--blur-lg` |
| `--depth-{plane}-translucency` | Per-plane frost opacity = **a function of elevation** (higher = more opaque) — §29 | `--depth-modal-translucency` |
| `--gradient-{name}` | A named, themeable gradient — a token or **role-derived** via `buildGradient` /`applyGradients` (the 6th builder) — §29 | `--gradient-aurora` |
| `--gradient-angle-{dir}` | Gradient direction (8 compass points) — §35 | `--gradient-angle-to-r` |
| `--breakpoint-{key}` | Responsive breakpoint (source = ActiveDom) — §35 | `--breakpoint-md` |
| `--z-index-{key}` | Z-index layer (depth planes) | `--z-index-modal` |
| `--z-index-overlay-{key}` | The flat overlay micro-band (portaled overlays + modals) — §35 | `--z-index-overlay-floating` |
| `--opacity-{key}` | Opacity — a dual numeric (`0..100`) + semantic (`disabled·muted·…`) scale — §35 | `--opacity-disabled` |
| `--tracking-{key}` | Letter-spacing scale (incl. `caps` for UPPERCASE) — §35 | `--tracking-caps` |
| `--leading-{key}` | Line-height scale | `--leading-ui` |
| `--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-height-md` |
| `--{c}-{role}-{slot}` | Component color | `--toggle-affirm-solid` |
| `--{c}-palette-{slot}` | Component runtime palette | `--toggle-palette-solid` |
### Private tokens (component-internal)
```
--_{c}-{slot}
```
The `_` prefix means: do NOT consume this from outside the component's
recipe. It is internal. Example:
```css
[data-toggle] {
--_toggle-bg: var(--toggle-solid-bg); /* private */
--_toggle-on-bg: var(--toggle-palette-solid); /* private */
}
```
### Strict rules
1. **All public Eidos tokens carry the bare `--` prefix** , with no layer
sub-prefix. Reason: debug clarity. See `--toggle-bg` and you know it is
Eidos. See `--bg` and you don't know where it came from.
2. **NEVER use `--eidos-`** as a prefix. The layer is already implicit in
the `$uix/eidos/components/{c}` path.
3. **NEVER use `--soma-`, `--air-` or `--terra-`** . Those layers are dead
or own no tokens.
4. **Component tokens follow the pattern** `--{component-kebab}-...` . The
component kebab is the directory name.
5. **Don't abbreviate component names** . `dropdown-menu` does not become
`ddmenu` . Authorship clarity is worth 6 chars.
6. **Do NOT include the intermediate "color-" segment** in color tokens.
`--toggle-affirm-solid` (correct), `--toggle-color-affirm-solid`
(deprecated 2026-05-27).
7. **Slots follow a fixed vocabulary** : for layers 3-5 the canonical
inventory is `COLOR_ROLE_SLOTS` (`lib/config-types.ts` — see §3, layer
3; not copied here). For layers 6-7 (recipe-level):
`bg, fg, border, on-bg, on-fg, on-border, hover-bg, on-hover-bg` .
### Generated tokens vs authorship
Tokens in `generated/base.css` are **output** . To add a new one, you edit:
- `lib/themes/base.ts` for primitives, scales, theme variants.
- `lib/recipes/base.ts` for component tokens.
And run `npm run generate:eidos-css` .
---
## 7. Token Scope Contract (TSC)
> **Moved to [`canon/tsc.md`](../canon/tsc.md)** (the visual canon, E2).
> The Token Scope Contract — the available scopes, the three ways to
> declare a token, the `scopeCovers` algebra, cross-axis collision
> detection, multi-part scope and cross-recipe composition (v2.2), and the
> 5-layer defense pipeline — lives there as its own chapter. Summary: the
> TSC decides WHERE each token is emitted (root, per component, per color,
> per event) and validates at generation time that every dependency is
> available in the consumer's scope.
---
## 8. How to add a new component
> **Moved to [`guide.md`](./guide.md)** (the E4 guide). The steps to add a
> new component — deciding which tokens it needs, the recipe, the scope and
> validation — live there next to the define-a-theme guide.
>
> **Which transversal systems the recipe MUST consume** (state-layer,
> focus, tokenized elevation, 1:1 typography, opacity, motion, logical
> axes) is its own canon:
> [`canon/recipe-contract.md`](../canon/recipe-contract.md), enforced by
> `component-audit`'s R-4.x rules.
---
## 9. How to define a theme
> **Moved to [`guide.md`](./guide.md)** (the E4 guide).
---
## 10. How to override tokens at runtime
`ActiveEidos.setCssVariables()` allows contract-aware runtime overrides:
```ts
activeEidos.setCssVariables({
'--color-primary-solid': 'rebeccapurple',
'size-md-control-height': '40px', // works without -- too
'shadow-3': '0 10px 28px rgb(20 20 20 / 0.16)'
});
```
Eidos:
1. **Validates** each name against `getCssContract()` . Tokens outside the
contract throw in `strict` mode (the default).
2. **Renders** transactionally: first render + validate, then replace the
runtime `<style>` block.
3. **Applies** the overrides under `:root` (or the selector you pass).
For variables outside the contract (the app's locals):
```ts
activeEidos.setCssVariables(
{ '--my-app-custom': 'value' },
{ strict: false }
);
```
### Whole-system builders
Above `setCssVariables` sit two builders that derive an entire system from
a seed and write it as a managed block (they follow the active light/dark
theme):
- **`eidos.applyColorScheme(seed, opts)`** — derives the 33 scales + 9
roles from a brand color (`buildScheme`). `clearColorScheme()` reverts.
- **`eidos.applyTypeScale(seed, opts)`** — derives the 8 `--font-size-*`
from a modular ratio + base (`buildTypeScale`), optionally fluid
(`ratioMax`). `clearTypeScale()` reverts.
Both are pure in `eidos/lib` (`build-scheme` / `build-type-scale` ) + an
application method on `ActiveEidos` . Live demos: `/temas/color` and
`/temas/tipografia` .
---
## 11. Bundle strategy + `eidos:purge`
`generated/base.css` contains the tokens of EVERY component in the catalog
(~95 components). In production a typical app uses 5-20.
### The tool
```bash
npm run eidos:purge -- \
--src 'src/**/*.svelte' \
--src 'src/**/*.ts' \
--src 'src/**/*.css' \
--output dist/eidos.purged.css \
--verbose
```
### How it decides what to keep
1. **Foundation always 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-scanned tokens** : every `var(--XXX)` and `--XXX:` declaration
found in source → `XXX` pinned.
3. **Component-import detection** : every `from '...components/{c}'` →
`{c}` 's full recipe pinned.
4. **Data-attr detection** : every `data-{c}=` (filtered against the
canonical recipe registry) → `{c}` 's full recipe pinned.
5. **Transitive closure** : if X is pinned and X→`var(--Y)`, Y is pinned.
Iterated to a fixed point.
### Measured results
| Profile | Components | Raw before | Raw after | Reduction | Gzip after |
|---|---|---|---|---|---|
| Minimal (toggle+button+badge) | 3 | 217.7 KB | 97.4 KB | ** − 55%** | 11.1 KB |
| Typical SaaS (10 components) | 10 | 217.7 KB | 116.3 KB | ** − 46%** | 13.6 KB |
| 5 UIX demo pages | 7 | 217.7 KB | 107.3 KB | − 51% | 12.4 KB |
| Exhaustive (all) | 62 | 217.7 KB | ~217 KB | − 0.4% | ~25 KB |
**The architectural floor**: ~95 KB raw / ~11 KB gzip (the foundation every
app needs).
### When to use it
- **In production**: ALWAYS. Integrate it into your build pipeline.
- **In dev**: optional. The raw 226 KB is fine for local iteration.
- **In SSR**: pre-purge once per build, not per request.
### Known limitations
- **Dynamic component selection**: if your app imports components
dynamically (`await import(...)`), the scanner can miss them. Mitigation:
pass the names via `--keep my-component` .
- **`var()` in dynamic strings**: if you build `var(--${name})` at runtime,
the scanner doesn't see it. Mitigation: declare the names statically in
some scannable file.
---
## 12. Validation tooling
| Tool | Command | What it validates |
|---|---|---|
| **morfo:check** | `npm run morfo:check` | DOM contracts vs the morfo declarations (a Playwright walk of the demos) |
| **eidos-lint** | `node scripts/eidos-lint.ts {c}` | Recipe CSS selectors vs the morfo enum values |
| **eidos-lint-all** | `node scripts/eidos-lint-all.ts` | Same, all components |
| **TSC validation** | `npm run generate:eidos-css` (implicit) | Scope algebra + cross-axis collision detection |
| **recipe-css-contract** | `npm test -- recipe-css-contract` | Consumed recipe tokens + the TSC v2 scenarios |
| **component-api-contract** | `npm test -- component-api-contract` | Each component's public API surface |
| **component-visual-attrs** | `npm test -- component-visual-attrs` | The visual data-attrs the wrapper emits |
| **generated-css** | `npm test -- generated-css` | The generated CSS's structure |
### The recommended pre-commit validation pipeline
```bash
npm run generate:eidos-css # if you touched recipes/themes
npm test -- src/uix/eidos # the eidos suite
npm run check # TS check
npm run morfo:check # DOM contracts (needs the dev server)
node scripts/eidos-lint-all.ts # the CSS-drift safety net
```
---
## 13. Sema integration (`event:*` scope)
> ⚠️ **Superseded.** The current motion model is the **two-moment** one
> documented in [`motion.md`](./motion.md) (F1– F7): the `--event` moment
> (the perceptual signature) is declared in `motion.signatures` and
> generated as CSS against `data-event-*` directly — without the TSC
> `event:*` scope or the `data-motion-ref` this section used to discuss.
> The engine (`EngineMotion`) is a service in `arts/motion` (`uix.motion`).
> The original body (the decision's historical context) lives in
> [`changelog.md §13`](./changelog.md).
---
## 14. Motion
Eidos's motion system — the **two-moment model** (the perceptual `--event`
during a signal's *hold* + `--state` for persistent transitions), the
`EngineMotion` engine (relocated to `arts/motion` , exposed as `uix.motion`
and consumed by soma and eidos) and how a preset is authored — is its own
system and lives in [`motion.md` ](./motion.md ). Roadmap F1– F7 implemented
(2026-06-04). This section covers only what touches **theming** .
> **Status note.** Earlier versions of this doc described motion as
> "deferred" and pointed at `data-motion-ref` / the "TSC `event:*` scope"
> (§13) as its future. That is **obsolete**: the perceptual signature
> migrated into the `signatures` registry (not `events.css`) and the engine
> is a service in `arts/motion` today. [`motion.md`](./motion.md) is the
> canonical, current reference.
### Draggable surfaces: the "pickup" lift
When the user **grabs and drags** a surface, it must **rise toward them**
(depth: "I picked it up"). It is a transversal cue — NOT component-specific
— so the scale factor is a **global motion token** , not a per-recipe
literal:
| Token | Value | Use |
|---|---|---|
| `--motion-scale-lift` | `1.02` | the pickup scale while dragging (the only `>1` in the `--motion-scale-*` family) |
**The pattern (every draggable applies it the same way)** — gated by the
drag data-attr its morfo declares (`data-dragging`, `data-grabbed` , …),
paired with a shadow elevation, and suppressed under reduced motion:
```css
/* float-panel, a dragging slider thumb, a sortable item, a drawer… */
[data-x][data-dragging] {
scale: var(--motion-scale-lift); /* rises toward the user */
box-shadow: var(--…-shadow-active); /* + elevates the shadow */
}
@media (prefers-reduced-motion: reduce) {
[data-x][data-dragging] { scale: 1; transition: none; }
}
```
Rules:
- **`scale` (not `transform` )** to compose with the position, which travels
via `translate` (distinct properties) → the lift never fights the 1:1
drag.
- The `scale` change is **transitioned** (lift on grab / settle on
release); during the move it stays **static** (composited, no per-frame
cost).
- `will-change: translate, scale` for the duration of the gesture.
- A theme re-themes the lift in `STATIC_MOTION.scale.lift`
(`primitives/static.ts`) — every draggable inherits it. Don't redefine
the 1.02 per component.
Today `float-panel` consumes it
(`[data-float-panel-content][data-dragging]`); a slider/sortable adding
drag must read the SAME token, not invent its own.
---
## 15. Comparison with reference libraries
> **Moved to [`notes.md`](./notes.md)** (E3). The comparison of eidos with
> Radix, Ark, Mantine and others lives there, next to the decisions FAQ.
---
## 16. Anti-patterns you must NOT commit
### A. Declaring derived tokens at `:root`
```ts
// ❌ WRONG — the pre-TSC Toggle bug
'palette-solid': { value: '...', scope: 'host' },
'solid-on-bg': 'var(--my-component-palette-solid)' // implicit 'root' scope
```
The TSC throws on regeneration. Fix: `scope: 'host'` on the consumer.
### B. Names with the redundant "color-" segment
```ts
// ❌ DEPRECATED (2026-05-27)
'color-affirm-solid': 'var(--color-affirm-solid)'
// ✅ CORRECT
'affirm-solid': 'var(--color-affirm-solid)'
```
### C. Inventing roles outside the canon
```ts
// ❌ NO — success/danger/warning/info belong to other models
'success': 'green',
'danger': 'red'
// ✅ Use the 9 canonical ones
'fulfill': 'green', // success → fulfill
'threat': 'red' // danger → threat
```
### D. Media queries that change canonical tokens
```css
/* ❌ NO — md changes meaning per viewport */
@media (max-width: 768px) {
:root { --size-md-control-height: 32px; }
}
/* ✅ The component picks which size applies per viewport */
< Toggle size = {{ base: ' sm ' , md: ' md ' } } / >
```
### E. Importing `$libs/dom` directly in eidos
```ts
// ❌ NO
import { foo } from '$libs/dom';
// ✅ Eidos consumes via ActiveEidos.dom
const eidos = ActiveEidos.require();
eidos.dom.apply(...);
```
### F. Hand-editing `generated/base.css`
It is output. Any change is overwritten on regeneration. To change
something, edit `lib/themes/base.ts` or `lib/recipes/base.ts` .
### G. Creating loose physical scales inside an app
The default **33 scales** cover the reasonable palettes. A **brand theme**
DOES bring its own palette as scales (§25.7) — that is legitimate. What you
must NOT do is add a one-off scale inside an app when remapping a role to
an existing scale already solves the case.
### H. Re-exporting between layers
```ts
// ❌ NO — eidos does not re-export soma
export * from '$soma/components/toggle';
// ✅ Each layer exposes its own API
```
---
## 17. FAQ — controversial decisions
> **Moved to [`notes.md`](./notes.md)** (E3).
---
## References
- [`architecture/eidos.md` ](../architecture/eidos.md ) — the visual layer as
a module (the living architecture chapter).
- [`THEMING_AUDIT_2026-06-01.md` ](../../src/uix/eidos/THEMING_AUDIT_2026-06-01.md ) —
the theming audit (the journal of how we got here).
- [`motion.md` ](./motion.md ) — the motion system (the two-moment model,
F1– F7; see §14).
- [`src/uix/eidos/lib/config-types.ts` ](../../src/uix/eidos/lib/config-types.ts ) —
the source of truth of the TSC type.
- [`src/uix/eidos/lib/render-css.ts` ](../../src/uix/eidos/lib/render-css.ts ) —
the generator (parsing, scope algebra, cross-axis detection).
- [`src/uix/eidos/lib/recipes/base.ts` ](../../src/uix/eidos/lib/recipes/base.ts ) —
the per-component token catalog.
- [`src/uix/eidos/lib/themes/base.ts` ](../../src/uix/eidos/lib/themes/base.ts ) —
the base theme (primitives + semantics + themes).
- [`scripts/eidos-purge.ts` ](../../scripts/eidos-purge.ts ) — the purge
tool.
- [`src/uix/eidos/recipe-css-contract.test.ts` ](../../src/uix/eidos/recipe-css-contract.test.ts ) —
the test guard.
- [`architecture/active-architecture.md` ](../architecture/active-architecture.md ) —
the full UIX context.
docs(book): F7.6 (1/2) — decision logs moved to docs/decisions/ (verbatim Spanish)
LIBRO_VARIACIONES_Y_EXTENSIONES -> decisions/book-deviations.md and
GUIA_IMPLEMENTACION_SEMAUIX -> decisions/guia-semantica-historica.md,
both moved AS-IS in Spanish: the deviations registry is a logbook of the
author's literal decisions ('transcrita literal') and carries proposed
doctrinal text destined for the Spanish book — translating it would
destroy that function (s G calls itself bitacora); the guia was already
status: historical (Fase 6) and the plan exempts it explicitly. English
frontmatter added to both; internal cross-links repointed (CANON,
theming/reference, book-deviations D.11). docs-check's I2 phantom-field
exemption follows the moved file (it matched by the LIBRO_VARIACIONES
filename; now also matches decisions/book-deviations.md). Corpus swept:
CANON x3, README (E2/E3 strata + tables — also fixed the pre-Fase-6
leftover row still calling the guia 'authoritative for any new wrapper'
and the unswept eidos/TSC.md stratum mention), building-a-component D.4,
completion-checklist G-1.1 + header, theming/reference, architecture
x5 (active-architecture, eidos, morfo, overview, sema x2).
docs:check 0 errors, 11-warn baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- [`guia-semantica-historica.md` ](../decisions/guia-semantica-historica.md ) —
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
the sema/perceptual doctrine (the historical seed of the 9-role canon;
[`CANON.md` ](../CANON.md ) rules today).
---
## 18. Universal TSC coverage
> **Moved to [`canon/tsc.md`](../canon/tsc.md)**. The v2.2 extensions
> (multi-part scope and cross-recipe composition) that take the TSC to
> universal coverage live with the rest of the contract there.
---
## 19. Variants are eidos canon, NOT the theme's
**A firmly-held architectural position**: the variant vocabulary (`solid`,
`outline` , `ghost` , `soft` , `surface` , `line` , `pills` , etc.) is **fixed in
the system**. A theme can NOT:
- Add a new variant (there is no `branded` , `bubble` , `corporate` each
theme invents).
- Redefine an existing variant's visual cascade (`outline` means "bordered
restraint" in every theme — only the border's COLOR changes, not its
geometry).
### The 3 layers of the onion
| Layer | What it is | Who changes it |
|---|---|---|
| **Sema families / intents** | the book's canonical perceptual vocabulary (8 families × 6 intents) | NOBODY — fixed |
| **Eidos variants** | perceptual visual archetypes (5 shared archetypes + component-specific variants) | NOBODY — fixed |
| **Eidos color roles** | the 9 canonical roles (`primary`/`affirm`/`risk`/…) | NOBODY — fixed |
| **The eidos color palette** | which hex each role is | **THE THEME** |
| **Recipe-internal tokens** | `--toggle-solid-bg` etc. | **The app** (a targeted override in `EidosConfig.recipes` ) |
**Theming = retinting the perceptually fixed.** The theme changes WHICH
color `affirm` is, not WHAT `outline` means.
### Why fixed
1. **Component portability.** `<Toggle variant="outline">` must render
coherently in any theme. Theme-defined variants would break that
silently — a component assuming `outline` wouldn't work in a theme that
doesn't declare it.
2. **Type safety = part of the contract.** Consumers need
`SelectionVariant = 'solid' | 'outline' | 'ghost'` for autocomplete and
TS errors. An extensible `Record<string, …>` would lose that guarantee.
Radix Themes 3.x, Chakra v3, Mantine — every serious reference keeps
variants fixed per component.
3. **Variants are perceptual archetypes, parallel to sema families.**
`solid` = "filled emphasis", `outline` = "bordered restraint",
`ghost` = "ambient transparency", `soft` = "tinted background". That is
the framework's perceptual vocabulary — not a theme decision.
4. **There are 5 archetypes, not infinitely many.** The catalog closes; TS
rejects non-canonical ones. If a genuinely perceptual new archetype
emerges, it is added to `EIDOS_VARIANTS` — at the framework level, not
the theme's.
### The single source of truth — `EIDOS_VARIANTS`
`src/uix/eidos/lib/types.ts` declares the constant:
```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];
// …
```
The 5 unions are DERIVED from the const — the value and the type cannot
desynchronize. Each component narrows to the appropriate archetype:
```ts
// components/toggle/types.ts
export type ToggleVariant = SelectionVariant;
// components/accordion/types.ts
export type AccordionVariant = ControlVariant;
```
For finer narrowing within an archetype:
```ts
export type AlertDialogCancelVariant = Extract< ButtonVariant , ControlVariant > ;
```
### Component-specific variants
Some components have genuinely unique vocabularies:
- `Banner` : `inline | overlay | persistent` (positioning, not perceptual
treatment).
- `Spinner` : `bars | dots | ring` (the indicator's geometry).
- `Button` : adds `'plain'` for inline/link-like — it doesn't deserve its
own archetype because it only appears in Button + Code.
These live in each `components/{c}/types.ts` . The lint
`recipe-css-contract.test.ts > variant CSS selectors per component match
the declared type union` validates bidirectionally:
- The CSS uses `[data-{c}][data-variant='X']` → X must be in the union.
- The type union declares `'X'` → the CSS should have entries (advisory).
### What a theme CAN do
- Change palettes (`ThemeColorSet.scales`, `ThemeColorSet.roles` ).
- Change shadows (`ShadowScale`).
- Change typography styles (`TypographyPrimitiveSet.styles`).
### What a theme can NOT do
- Add variants. (`ThemeDefinition` exposes no `recipes` .)
- Redefine visual cascades. (Recipes are `EidosConfig.recipes` , part of the
app's bootstrap, not the theme's.)
- Change color roles. (The 9 roles are canon.)
- Change the size canon. (`SIZE_PRIMITIVE_KEYS` is fixed.)
### What the app CAN do (at boot, not per theme)
- Override tokens in `EidosConfig.recipes` — it changes the visual
cascade's RESULT; it adds no new variant.
- Create its own wrapper components composing eidos primitives with custom
className/style.
- Change tokens at runtime via `ActiveEidos.setCssVariables()` /
`clearCssVariables()` (per-app CSS variables).
### When to add a new archetype
Only when a repeated perceptual pattern EMERGES in ≥3 components and fits
none of the 5 existing archetypes. Procedure:
1. Document the archetype with 1 paragraph describing the perceptual
affordance (parallel to "solid = filled emphasis").
2. Add it to `EIDOS_VARIANTS` in `lib/types.ts` .
3. Export the derived type.
4. Migrate the consuming components to reference it.
5. Update this section.
### Comparison with the references
| Lib | Theme-extensible variants | App-extensible variants |
|---|---|---|
| **Radix Themes 3.x** | ❌ | ❌ (fixed per component) |
| **Mantine 7+** | ❌ | ❌ (defaultProps + styles override) |
| **Chakra UI v3 (Panda)** | ❌ | ⚠️ via recipes config (compound variants) |
| **Ark UI** | n/a (100% headless, no opinion) | n/a |
| **shadcn/ui** | n/a (copy-paste, not a framework) | ✓ (copy + edit) |
| **activeUIX** | ❌ | ⚠️ via an `EidosConfig.recipes` override (changes tokens; adds no variants) |
activeUIX aligns with Radix Themes and Mantine: a framework with a fixed
contract, a theme with flexibility bounded to color/spacing. Extreme
extensibility (Tailwind, plain CSS-in-JS) is deliberately NOT the goal —
because the framework's promise is perceptual portability across apps and
themes.
---
> **§20– §38 — the chronicle moved to [`changelog.md`](./changelog.md).**
> These sections were dated sprint records (corrections, incidents,
> commits) mixed with doctrine. The full chronicle now lives in the
> changelog **with the same §N numbering**; below, each section keeps the
> standing decision in a sentence + the pointer to the living source (RFC /
> config / generator). Historical `§N` citations across the corpus and the
> code keep resolving here.
## 20. Theming-engine corrections (2026-06-01)
Chronicle in [`changelog.md §20` ](./changelog.md ). Standing: density emits
real per-level scalars (`data-density` drives `--density-space-scale` /
`--density-control-scale` ) and the `contrast` slot resolves legibly on
solids (APCA on-solid with a flip; see §25 and `lib/render-css.ts` ).
## 21. Two-level color model (RFC — RESOLVED in §25)
The RFC is resolved — chronicle in [`changelog.md §21` ](./changelog.md ).
The standing model is §25 +
[`rfc-color-model.md` ](../rfcs/rfc-color-model.md ).
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 22. Pending theming improvements
A historical, resolved backlog — chronicle in
[`changelog.md §22` ](./changelog.md ). The prioritized audit that absorbed
it is
[`THEMING_AUDIT_2026-06-01.md` ](../../src/uix/eidos/THEMING_AUDIT_2026-06-01.md )
(the full scorecard).
## 23. The `scaling` axis (global zoom) — 2026-06-02
Chronicle in [`changelog.md §23` ](./changelog.md ). Standing: `data-scaling`
(`90`– `110`) is the **global zoom** — it multiplies `space` ,
`control-height` , `font-size` , `icon-size` (typography YES); it does NOT
scale radius / border / shadow. Orthogonal to density (which does NOT touch
typography) and multiplies with it. Full design:
[`rfc-scaling.md` ](../rfcs/rfc-scaling.md ).
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 24. P2 engine corrections (2026-06-02)
Chronicle in [`changelog.md §24` ](./changelog.md ). Standing: the per-role
`surface` /`soft` tints are **translucent by construction**
(`--color-{role}-surface` = `a2` , `-surface-hover` = `a3` ) — they compose
over non-uniform backgrounds.
## 25. The color model — palette + derived roles/intents (2026-06-02)
Chronicle in [`changelog.md §25` ](./changelog.md ). Standing (the canonical
color model, three layers):
- **The palette** — functional 12-step + 12-alpha scales, theme-designable
(`lib/themes/color-scales.ts` + `base.ts` ); a scale's identity = step 9
(the solid). Directly usable: `var(--scale-{name}-{step})` .
- **Hierarchy roles** (`primary`/`secondary`/`tertiary`) — explicit aliases
to a scale: `THEME_BASE_COLOR_ROLES` (`lib/themes/base.ts`), see §4.
- **Intents** — auto-derived from the palette by the book's convention
(`CANONICAL_INTENT_SCALES`, `lib/config-types.ts` ); the theme may deviate
(convention ≠ authorship, §4).
The per-role slots are `COLOR_ROLE_SLOTS` (§3, layer 3 — the single
source). Detail and rationale:
[`rfc-color-model.md` ](../rfcs/rfc-color-model.md ) +
[`rfc-color-engine.md` ](../rfcs/rfc-color-engine.md ).
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 26. Runtime theme builder — `eidos.applyColorScheme` (2026-06-04)
Chronicle in [`changelog.md §26` ](./changelog.md ). Standing:
`buildScheme(seed, opts)` (pure, `lib/build-scheme.ts` ) +
`ActiveEidos.applyColorScheme(seed, opts)` / `clearColorScheme()` — derives
a full scheme (roles + `a1..a12` alphas + APCA on-solid) from the active
theme and re-derives on mode change. Layers: `uix.color` = the math ·
`build-scheme` = pure composition · `ActiveEidos` = DOM application. API:
docs(book): F7.4 (4/4) — color-engine RFC translated; rfcs/ batch COMPLETE
COLOR_ENGINE_RFC (619 L, the largest RFC) -> rfcs/rfc-color-engine.md,
Spanish to English, same section numbering (s0 TL;DR, s6 generator +
s6.1 isomorphism + s6.2 deriveScheme/buildScheme, s7 wide-gamut
strategies, s8 APCA, s10 frozen invariants, s11 phases with 4-bis
implemented, s13 bloat-doc absorption, s14 comparison). Its
THEMING_AUDIT citation notes the audit is retired; the s25/s26 canon
citations point into theming/reference.md. decisions.md row repointed +
Status aligned (implemented through phase 4-bis); the index's naming
note rewritten — the deferred eidos-RFC rename is now DONE via stubs,
while the arts DESIGN_* docs keep their legacy names in place (their
citations were not swept).
F7.4 complete: 7 RFCs live at docs/rfcs/rfc-{scaling, structure, depth,
shape, typography, color-model, color-engine}.md; MOTION_SERVICE_RFC
stays in src (foreign/concurrent). docs:check 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
[`rfc-color-engine.md` ](../rfcs/rfc-color-engine.md ) §6.2/§7.
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
## 27. Wide-gamut OKLCH output (default-on) (2026-06-04)
Chronicle in [`changelog.md §27` ](./changelog.md ). Standing: every palette
step emits hex (the fallback) + an `oklch()` sibling that wins where
supported — default-on, no flag (`appendColorScaleDeclarations`,
`lib/render-css.ts` ). The REAL wide gamut lives in the generator
(`buildScheme` keeps the unclamped OKLCH → `result.wideGamut` ).
## 28. Forced-colors accessibility + the border ramp (2026-06-05)
Chronicle in [`changelog.md §28` ](./changelog.md ). Standing: the foundation
always emits `@media (forced-colors: active)` (focus via `outline` —
box-shadow dies in HCM) and `@media (prefers-contrast: more)` (borders and
de-emphasized text reinforced via `:root:root` ); the `border` slot =
**step 7** of the scale (`DEFAULT_COLOR_ROLE_SLOT_STEPS`).
## 29. Depth — the unified, eventful channel (2026-06-05)
Chronicle in [`changelog.md §29` ](./changelog.md ). Standing: depth is ONE
channel — `data-depth='{plane}'` (`flush · raised · overlay · modal ·
recessed`) coheres surface + shadow + halo + z at rest, and the event
signature (`present-rise` / `press-squeeze` ) moves it in the event-moment.
Config-driven planes (`EidosConfig.depth.planes`); overlays compose
`var(--depth-{plane}-shadow), var(--depth-{plane}-halo)` . Canonical guide:
[`rfc-depth.md` ](../rfcs/rfc-depth.md ). Demo:
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`/temas/profundidad` .
## 30. Shape — continuity + families + nesting + eventful (2026-06-05)
Chronicle in [`changelog.md §30` ](./changelog.md ). Standing: shape is a
channel — `--shape-smoothing` + `data-shape='{family}'`
(`rounded · continuous · cut · scoop`) over `corner-shape` , degrading to
the `border-radius` arc; nested harmony via `[data-shape-nest]`
(concentric); the squircle is the **surface tier's default**
(`renderShapeBlocks` `:where` + `--shape-surface-default` ; `shape` =
opt-OUT). Runtime builder `applyShape(seed)` . Canonical guide:
[`rfc-shape.md` ](../rfcs/rfc-shape.md ). Demo:
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
`/temas/forma` .
## 31. Structure (space · density · scale) — space as rhythm (2026-06-05)
Chronicle in [`changelog.md §31` ](./changelog.md ). Standing: the three
structural axes are state-only — density (`data-density`), scaling
(`data-scaling`, §23) and space: `--space-{key}` =
`calc(value · var(--density-space-scale) · var(--scaling))` , regenerable
from a modular/fluid seed (`buildSpaceScale` + `applySpacing` ). Canonical
guide:
[`rfc-structure.md` ](../rfcs/rfc-structure.md ).
docs(book): F7.3 (9/9) — THEMING reference translated; theming/ + canon/ batch COMPLETE
src/uix/eidos/THEMING.md (1499 L, Spanish) translated to English as
docs/theming/reference.md, same s1-s38 numbering: the mental model,
s1.bis (theming lives in eidos — the 2-of-3 derivation and the canonical
split), the CSS layers, the 7 token layers, the 9 roles, the size canon
(universal 1:1 + label step-down + container cap + the icon scale), the
naming conventions, the s7/s8/s9/s13/s15/s17/s18 stubs repointed into the
book (canon/tsc, theming/guide, theming/notes, theming/changelog,
theming/motion), runtime overrides + builders, bundle/purge, validation
tooling, s14 motion (the pickup lift), s16 anti-patterns, s19 variants
canon, and the s20-s38 standing-decision stubs. One internal
reconciliation applied: the s35 stub cited the control/compact/dense size
archetypes that s5 declares superseded by the universal 1:1 — aligned.
One stale v1 DEMO_AUTHORING s12.8 citation in s5 replaced by the v2 s6
parity rule. Stub at the old path carries the full s1-s38 map (the most
sN-cited doc in the repo: code comments, CLAUDE.md, RFCs). Corpus links
swept (CANON, docs map, comparison, glossary, eidos chapter, canon/tsc,
theming/guide + notes). docs:check 0 errors (260 docs).
F7.3 complete: docs/canon/ (tsc, recipe-contract) + docs/theming/
(reference, guide, notes, channels, motion, motion-guide, changelog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
Demo: `/temas/estructura` .
## 32. Focus ring — the parameterized two-ring model (2026-06-11)
Chronicle in [`changelog.md §32` ](./changelog.md ). Standing: ONE focus
model — the canonical ring (`--focus-ring` + the recipes' `*-focus-shadow` )
is two parameterized rings (`--focus-ring-inner-width`, default `0` = outer
frame only; `STATIC_FOCUS_RING` , `primitives/static.ts` ); the foundation's
ring excludes field-internal elements. A11y note §28: under forced-colors
the visible focus is `outline` (the per-component migration of box-shadow →
outline is open work).
## 33. Themeable stepper glyphs (`spin-field`) — 2026-06-11
Chronicle in [`changelog.md §33` ](./changelog.md ). Standing: the stepper's
glyphs (`spin-field`) are themeable recipe tokens, not hardcoded SVG.
## 34. `spin-field` — the stepper-field's shared visual (`number-field` / `css-field`) — 2026-06-11
Chronicle in [`changelog.md §34` ](./changelog.md ). Standing: `number-field`
and `css-field` share ONE visual layer via the structural identity
`data-spin-field*` (`components/spin-field/spin-field.css`, aggregated in
`index.css` ) — no clone.
## 35. The scale canon — the theming audit (2026-06-15)
Chronicle in [`changelog.md §35` ](./changelog.md ). Standing: every scale
axis (blur · inner-shadow · inset-ring · gradients · breakpoints ·
container · opacity — dual numeric+semantic · border-width · tracking) is
theme-retunable and **recipes consume the token, never a literal** (the
R-2.x/R-4.x guards). The inventory lives in `EidosConfig`
(`lib/primitives/*` + `config-types.ts` ) and the naming table in §6;
breakpoints source from `ActiveDom` . The size→font mapping is the universal
1:1 of §5 (the audit's `control · compact · dense` size archetypes were
superseded by the 2026-06-17 override).
## 36. The canonical trigger→panel gap — a token-driven offset (2026-06-22)
Chronicle in [`changelog.md §36` ](./changelog.md ). Standing: the
trigger→panel gap is an OFFSET of the positioning engine via
`@property --floating-gap` — menus `0` , panels `--space-1-5` .
## 37. Touch-target — 44px on touch, pointer-gated (2026-06-28)
Chronicle in [`changelog.md §37` ](./changelog.md ). Standing: the 44px touch
targets apply ONLY under `@media (pointer: coarse)` ; markers via the
label-row, no DOM restructuring.
## 38. The state layer — unified neutral feedback (2026-06-28)
Chronicle in [`changelog.md §38` ](./changelog.md ). Standing: neutral
interactive feedback (hover/press/selected) is the MD3 state layer — the
`--state-hover` / `--state-press` / `--state-selected` tokens composed as
`background-image: linear-gradient(...)` in `archetypes.css` ; bespoke
per-component hovers are deprecated (R-4.3).
---
**Last revision**: 2026-07-02. If anything in this doc disagrees with the
code, the code wins — but open an issue so we update the doc.