You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/theming/guide.md

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 is canon/tsc.md; the transversal systems every recipe must consume are canon/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:

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.

Powered by TurnKey Linux.