feat(eidos): applyTheme(seed) — the runtime theme capstone

Composes the five runtime builders (color · type · depth · shape · space)
from one ThemeSeed { color?, type?, depth?, shape?, space? } in a SINGLE
managed write — vs five separate apply* calls. Atomic whole-theme
semantics: axes you provide are set, axes you omit revert to the authored
foundation; clearTheme() reverts all. Per-axis apply{Color,Type,…} stay
for surgical tweaks. Returns ApplyThemeResult for per-axis introspection.

New: ThemeSeed, ApplyThemeOptions, ApplyThemeResult (exported from
$uix/eidos); also exported the previously-omitted ApplySpacingOptions.
2 tests (composition + atomic-replace). Closes the "jaula abierta" arc —
no referent reunites the 5 perceptual axes under one runtime theme builder.
Updated CHANNELS_SYNTHESIS §5 (quintet → sextet). check clean (the 28
errors are the untracked heroscrolling WIP demo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent c121365bed
commit be60c449a8

@ -85,14 +85,19 @@ Un `engine.emit(...)` estampa `data-event-*` (el canal **visual** reacciona —
en color/motion/depth/forma) **y** dispara sound + haptic — desde el **mismo** evento. No hay
sistemas coordinándose a mano; hay un suceso que se expresa por los canales de sema.
## 5. Builders runtime — el quinteto (jaula abierta)
## 5. Builders runtime — el sexteto (jaula abierta)
Cada eje retunable en runtime, mismo patrón (semilla → bloque gestionado), opt-in sobre la
escala authored:
`applyColorScheme` · `applyTypeScale` · `applyDepth` · `applyShape` · `applySpacing`
(Futuro: `applyTheme(seed)` que componga varios desde una semilla compartida.)
Y el **capstone que los compone**: **`applyTheme(seed)`** — una sola semilla
(`{ color?, type?, depth?, shape?, space? }`) compone los cinco ejes en **una única
escritura gestionada** (vs cinco `apply*` sueltos), de forma **atómica**: los ejes que das se
aplican, los que omites revierten a la foundation authored. `clearTheme()` revierte todo. Para
retoques quirúrgicos por eje, los `apply*` individuales siguen ahí. Ningún referente reúne los
cinco ejes perceptuales bajo un único builder de tema en runtime.
## 6. Posición frente a referentes

@ -541,6 +541,37 @@ describe('ActiveEidos config', () => {
eidos.clearDepth(); // reverts cleanly, no throw
});
it('applyTheme composes all five builders from one seed (the runtime capstone)', () => {
const eidos = createThemeBaseEidos();
const result = eidos.applyTheme({
color: '#3b82f6',
type: { base: 16, ratio: 1.25 },
depth: { overlay: { shadow: '0 30px 70px rgb(0 0 0 / 0.5)' } },
shape: { smoothing: 3 },
space: { base: 4 }
});
// every provided axis returns its built result — composed in one call
expect(result.color).toBeDefined();
expect(result.type?.sizes).toHaveLength(8);
expect(result.depth?.variables).toContain(
'--depth-overlay-shadow: 0 30px 70px rgb(0 0 0 / 0.5);'
);
expect(result.shape).toBeDefined();
expect(result.space?.steps).toHaveLength(18);
eidos.clearTheme(); // reverts every axis cleanly, no throw
});
it('applyTheme is atomic — omitted axes revert to the foundation (whole-theme replace)', () => {
const eidos = createThemeBaseEidos();
eidos.applyTheme({ type: { base: 16, ratio: 1.25 }, shape: { smoothing: 3 } });
// re-applying with only color must clear the previously-set type + shape
const result = eidos.applyTheme({ color: '#3b82f6' });
expect(result.color).toBeDefined();
expect(result.type).toBeUndefined();
expect(result.shape).toBeUndefined();
eidos.clearTheme();
});
it('emits the shape channel — --shape-smoothing + data-shape family rules (continuity)', () => {
const css = createThemeBaseEidos().renderStaticCss();
expect(css).toContain('--shape-smoothing: 2;');

@ -186,6 +186,43 @@ interface ActiveEidosSpaceScaleSpec {
readonly options: ApplySpacingOptions;
}
/**
* A whole-system theme seed — one config that composes the five runtime
* builders (color · type · depth · shape · space). Every axis is optional;
* {@link ActiveEidos.applyTheme} sets the axes you provide and reverts the
* ones you omit to the authored foundation (atomic whole-theme semantics).
* For surgical per-axis tweaks use the individual `apply{Color,Type,…}` methods.
*/
export interface ThemeSeed {
/** Brand color seed → whole color scheme (see {@link ActiveEidos.applyColorScheme}). */
readonly color?: string | Oklch;
/** Modular type-scale seed → `--font-size-*` ladder. */
readonly type?: TypeScaleSeed;
/** Per-plane depth cue overrides → `--depth-{plane}-*`. */
readonly depth?: DepthOverrides;
/** Shape channel seed (smoothing / nestGap / families) → `--shape-*`. */
readonly shape?: ShapeSeed;
/** Space scale seed (base unit / growth) → `--space-*`. */
readonly space?: SpaceScaleSeed;
}
/** Options for {@link ActiveEidos.applyTheme}. */
export interface ApplyThemeOptions {
/** CSS selector every axis targets. @default ':root' */
readonly selector?: string;
/** Color-axis options (variant / temper / theme / mode / per-role overrides). */
readonly color?: Omit<ApplyColorSchemeOptions, 'selector'>;
}
/** The composed result of {@link ActiveEidos.applyTheme} — per-axis introspection. */
export interface ApplyThemeResult {
readonly color?: BuildSchemeResult;
readonly type?: BuildTypeScaleResult;
readonly depth?: BuildDepthResult;
readonly shape?: BuildShapeResult;
readonly space?: BuildSpaceScaleResult;
}
export interface ActiveEidosOptions {
readonly config?: EidosConfig | EidosConfigDocument;
readonly themeBase?: EidosConfigPatch;
@ -599,6 +636,62 @@ export class ActiveEidos {
this.apply();
}
/**
* Apply a whole-system theme from one seed — the capstone of the runtime
* builders. Composes color · type · depth · shape · space in a SINGLE managed
* write (vs five separate `apply*` calls), atomically: the axes you provide are
* set, the ones you omit revert to the authored foundation. The "jaula abierta"
* in one call. Returns a per-axis {@link ApplyThemeResult} for introspection.
*
* For surgical per-axis tweaks (leaving the rest untouched) use the individual
* `apply{Color,Type,Depth,Shape,Spacing}` methods instead.
*/
applyTheme(seed: ThemeSeed, options: ApplyThemeOptions = {}): ApplyThemeResult {
const selector = options.selector ?? DEFAULT_SCHEME_SELECTOR;
this.#schemeSpec =
seed.color !== undefined
? { seed: seed.color, options: { ...options.color, selector } }
: undefined;
this.#typeScaleSpec =
seed.type !== undefined ? { seed: seed.type, options: { selector } } : undefined;
this.#depthSpec =
seed.depth !== undefined ? { planes: seed.depth, options: { selector } } : undefined;
this.#shapeSpec =
seed.shape !== undefined ? { seed: seed.shape, options: { selector } } : undefined;
this.#spaceScaleSpec =
seed.space !== undefined ? { seed: seed.space, options: { selector } } : undefined;
this.apply();
return {
color: this.#schemeSpec ? this.#buildSchemeResult() : undefined,
type: seed.type !== undefined ? buildTypeScale(seed.type) : undefined,
depth: seed.depth !== undefined ? buildDepth(seed.depth) : undefined,
shape: seed.shape !== undefined ? buildShape(seed.shape) : undefined,
space: seed.space !== undefined ? buildSpaceScale(seed.space) : undefined
};
}
/** Remove an applied theme, reverting EVERY axis to the authored foundation. */
clearTheme(): void {
if (
!this.#schemeSpec &&
!this.#typeScaleSpec &&
!this.#depthSpec &&
!this.#shapeSpec &&
!this.#spaceScaleSpec
) {
return;
}
this.#schemeSpec = undefined;
this.#typeScaleSpec = undefined;
this.#depthSpec = undefined;
this.#shapeSpec = undefined;
this.#spaceScaleSpec = undefined;
this.apply();
}
#buildSchemeResult(): BuildSchemeResult {
const spec = this.#schemeSpec;
if (!spec) throw new ActiveEidosConfigError('no color scheme applied');

@ -107,7 +107,11 @@ export type {
ApplyColorSchemeOptions,
ApplyTypeScaleOptions,
ApplyDepthOptions,
ApplyShapeOptions
ApplyShapeOptions,
ApplySpacingOptions,
ThemeSeed,
ApplyThemeOptions,
ApplyThemeResult
} from './active-eidos.svelte';
export type {
EidosConfigDocument,

Loading…
Cancel
Save

Powered by TurnKey Linux.