5.9 KiB
| title | type | audience | status | source |
|---|---|---|---|---|
| Eidos Theming — Authoring guides | guide | human + agent | current | migrated from src/uix/eidos/THEMING_GUIDE.md (2026-07-02, docs-book F7.3; originally extracted from THEMING §8 + §9) |
Eidos Theming — Authoring guides
The two theming how-tos: add a new component and define a theme. Extracted from
THEMING.md(the E1 reference) to live as an E4 guide. The token contract holding all of this together iscanon/tsc.md; the transversal systems every recipe must consume arecanon/recipe-contract.md.
How to add a new component
Assuming you already have the morfo, the soma and the eidos wrapper
scaffolding (src/uix/eidos/components/{name}/):
Step 1 — Decide which tokens you need
Look at similar components (button, toggle, switch). Identify which
dimensions your component exposes:
- Does it have
data-color? → palette tokens. - Does it have
data-variant? → variant tokens. - Does it have
data-size? → size tokens. - How many parts does it have? → per-part tokens.
Step 2 — Add the recipe in lib/recipes/base.ts
// Within THEME_BASE_RECIPE_TOKENS:
'my-component': {
// size tokens — scope 'root' (stable)
'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)',
// dynamic palette — 'host' default + per-color overrides
'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' }
]
},
// derived tokens — scope 'host' (deps inferred)
'solid-bg': {
value: 'var(--my-component-palette-solid)',
scope: 'host'
}
}
Step 3 — Regenerate
npm run generate:eidos-css
If your config violates the TSC, the regeneration tells you:
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.
Step 4 — Write the recipe CSS
src/uix/eidos/components/my-component/my-component.css:
[data-my-component] {
/* Private tokens — only this recipe reads them */
--_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;
}
Step 5 — Wire the CSS
Code-split components import their own CSS from the wrapper .svelte;
layout primitives and shared visuals aggregate in index.css:
@import './components/my-component/my-component.css';
Step 6 — Verify
npm run generate:eidos-css
npm test -- src/uix/eidos
npm run morfo:check
node --import tsx/esm scripts/eidos-lint.ts my-component
Common anti-pattern: declaring composed tokens at :root
// ❌ WRONG — the TSC will fail
'my-component': {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-bg': 'var(--my-component-palette-solid)' // ← implicit 'root' scope, deps live in 'host'
}
// ✓ CORRECT
'my-component': {
'palette-solid': { value: 'var(--color-neutral-solid)', scope: 'host' },
'solid-bg': {
value: 'var(--my-component-palette-solid)',
scope: 'host'
}
}
How to define a theme
Eidos supports 3 modes of theme definition:
Mode 1 — A patch over the base theme (recommended)
Change only what you need; everything else follows the base:
import { ActiveEidos } from '$uix/eidos';
ActiveEidos.create({
themeBase: {
semantics: {
color: {
roles: {
primary: 'blue', // primary uses the Radix blue scale
secondary: 'plum'
}
}
},
primitives: {
typography: {
families: {
primary: { family: 'Inter' }
}
}
}
},
applyDom: true
});
Mode 2 — A full config
For authoring from scratch:
import { ActiveEidos, defineEidosConfig } from '$uix/eidos';
const config = defineEidosConfig({
primitives: { /* … */ },
semantics: { color: { /* … */ } },
themes: { /* … */ }
});
ActiveEidos.create({ config, applyDom: true });
Mode 3 — A CSS-only theme (no TypeScript)
Eidos publishes the contract as empty CSS for externals to fill:
const contract = activeEidos.renderContractCss({
themeSelector: "[data-theme='acme-light']"
});
// Output:
// [data-theme='acme-light'] {
// --scale-blue-9: ;
// --color-primary-solid: ;
// --size-md-control-height: ;
// ...
// }
The consumer fills in the values:
[data-theme='acme-light'] {
--scale-blue-9: #006adc;
--color-primary-solid: var(--scale-blue-9);
--size-md-control-height: 38px;
}
And loads that CSS alongside Eidos's. With themeSource: 'css',
ActiveEidos generates no theme of its own.
Versioned persistence
const document = activeEidos.toDocument();
// → { kind: 'uix.eidos-config', version: 1, options: {...} }
const json = activeEidos.serialize();
localStorage.setItem('user-theme', json);
// Later
const hydrated = createActiveEidos({
config: JSON.parse(localStorage.getItem('user-theme')!),
prefs, dom
});
The document envelope carries a version for future migrations.