feat(eidos): shape Fase 4 — applyShape runtime builder

Completes the runtime-builder quartet (color, type, depth, shape). buildShape
(pure) + ActiveEidos.applyShape/clearShape: dial `smoothing` (live continuity /
squircle intensity) + `nestGap`, or override/add `families`, written as a managed
block that wins over the foundation. Exported from $uix/eidos.

Verified: check 0 errors; eidos config 56/56.

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

@ -561,6 +561,14 @@ describe('ActiveEidos config', () => {
expect(css).toContain('--shape-smoothing: 3;'); expect(css).toContain('--shape-smoothing: 3;');
}); });
it('applyShape retunes the channel at runtime (jaula-abierta shape builder)', () => {
const eidos = createThemeBaseEidos();
const result = eidos.applyShape({ smoothing: 1.4, families: { continuous: 'squircle' } });
expect(result.variables).toContain('--shape-smoothing: 1.4;');
expect(result.families).toContain("[data-shape='continuous'] { corner-shape: squircle; }");
eidos.clearShape(); // reverts cleanly, no throw
});
it('emits the Phase 3 typography scales + optical tracking', () => { it('emits the Phase 3 typography scales + optical tracking', () => {
const css = createThemeBaseEidos().renderStaticCss() const css = createThemeBaseEidos().renderStaticCss()
expect(css).toContain('--tracking-tight: -0.02em;') expect(css).toContain('--tracking-tight: -0.02em;')

@ -54,6 +54,7 @@ import {
type BuildTypeScaleResult type BuildTypeScaleResult
} from './lib/build-type-scale'; } from './lib/build-type-scale';
import { buildDepth, type DepthOverrides, type BuildDepthResult } from './lib/build-depth'; import { buildDepth, type DepthOverrides, type BuildDepthResult } from './lib/build-depth';
import { buildShape, type ShapeSeed, type BuildShapeResult } from './lib/build-shape';
import { collectFontPreloads, type FontPreload } from './lib/font-preload'; import { collectFontPreloads, type FontPreload } from './lib/font-preload';
import type { Oklch } from '$color'; import type { Oklch } from '$color';
import type { EngineMotion } from '$motion'; import type { EngineMotion } from '$motion';
@ -158,6 +159,17 @@ interface ActiveEidosDepthSpec {
readonly options: ApplyDepthOptions; readonly options: ApplyDepthOptions;
} }
/** Options for {@link ActiveEidos.applyShape}. */
export interface ApplyShapeOptions {
/** CSS selector the variable override targets. @default ':root' */
readonly selector?: string;
}
interface ActiveEidosShapeSpec {
readonly seed: ShapeSeed;
readonly options: ApplyShapeOptions;
}
export interface ActiveEidosOptions { export interface ActiveEidosOptions {
readonly config?: EidosConfig | EidosConfigDocument; readonly config?: EidosConfig | EidosConfigDocument;
readonly themeBase?: EidosConfigPatch; readonly themeBase?: EidosConfigPatch;
@ -219,6 +231,7 @@ export class ActiveEidos {
#schemeSpec: ActiveEidosSchemeSpec | undefined; #schemeSpec: ActiveEidosSchemeSpec | undefined;
#typeScaleSpec: ActiveEidosTypeScaleSpec | undefined; #typeScaleSpec: ActiveEidosTypeScaleSpec | undefined;
#depthSpec: ActiveEidosDepthSpec | undefined; #depthSpec: ActiveEidosDepthSpec | undefined;
#shapeSpec: ActiveEidosShapeSpec | undefined;
#unsubscribe: (() => void) | undefined; #unsubscribe: (() => void) | undefined;
#lastAttrs: ActiveEidosLastAttrs | undefined; #lastAttrs: ActiveEidosLastAttrs | undefined;
#disposed = false; #disposed = false;
@ -528,6 +541,26 @@ export class ActiveEidos {
this.apply(); this.apply();
} }
/**
* Retune the shape channel at runtime — the shape analogue of {@link applyColorScheme} /
* {@link applyTypeScale} / {@link applyDepth}. Dial `smoothing` (corner continuity / squircle
* intensity) + `nestGap`, or override/add `families`, written as a managed block that wins over
* the foundation. The jaula-abierta runtime of the shape channel.
*/
applyShape(seed: ShapeSeed, options: ApplyShapeOptions = {}): BuildShapeResult {
this.#shapeSpec = { seed, options };
const result = buildShape(seed);
this.apply();
return result;
}
/** Remove the applied shape retune, reverting to the foundation's shape system. */
clearShape(): void {
if (!this.#shapeSpec) return;
this.#shapeSpec = undefined;
this.apply();
}
#buildSchemeResult(): BuildSchemeResult { #buildSchemeResult(): BuildSchemeResult {
const spec = this.#schemeSpec; const spec = this.#schemeSpec;
if (!spec) throw new ActiveEidosConfigError('no color scheme applied'); if (!spec) throw new ActiveEidosConfigError('no color scheme applied');
@ -623,6 +656,16 @@ export class ActiveEidos {
} else { } else {
this.#removeStyle(host, depthStyleId); this.#removeStyle(host, depthStyleId);
} }
// The runtime shape retune (applyShape) is written last so its `--shape-*` + family rules
// win over the static foundation's shape system at equal specificity.
const shapeStyleId = `${this.#styleId}-shape`;
const shapeCss = this.#renderShapeCss();
if (shapeCss) {
this.#writeStyle(host, shapeStyleId, shapeCss);
} else {
this.#removeStyle(host, shapeStyleId);
}
} }
#renderSchemeCss(): string { #renderSchemeCss(): string {
@ -657,6 +700,15 @@ export class ActiveEidos {
return body ? `${selector} {\n${body}\n}` : ''; return body ? `${selector} {\n${body}\n}` : '';
} }
#renderShapeCss(): string {
if (!this.#shapeSpec) return '';
const result = buildShape(this.#shapeSpec.seed);
const selector = this.#shapeSpec.options.selector ?? ':root';
const body = result.variables.map((line) => `\t${line}`).join('\n');
const varBlock = body ? `${selector} {\n${body}\n}` : '';
return [varBlock, ...result.families].filter(Boolean).join('\n');
}
dispose(): void { dispose(): void {
if (this.#disposed) return; if (this.#disposed) return;
this.#disposed = true; this.#disposed = true;

@ -28,6 +28,8 @@ export type {
} from './lib/build-type-scale'; } from './lib/build-type-scale';
export { buildDepth, depthDeclarations } from './lib/build-depth'; export { buildDepth, depthDeclarations } from './lib/build-depth';
export type { DepthOverrides, BuildDepthResult } from './lib/build-depth'; export type { DepthOverrides, BuildDepthResult } from './lib/build-depth';
export { buildShape } from './lib/build-shape';
export type { ShapeSeed, BuildShapeResult } from './lib/build-shape';
export { collectFontPreloads } from './lib/font-preload'; export { collectFontPreloads } from './lib/font-preload';
export type { FontPreload } from './lib/font-preload'; export type { FontPreload } from './lib/font-preload';
export { export {
@ -102,7 +104,8 @@ export type {
ActiveEidosUserOptions, ActiveEidosUserOptions,
ApplyColorSchemeOptions, ApplyColorSchemeOptions,
ApplyTypeScaleOptions, ApplyTypeScaleOptions,
ApplyDepthOptions ApplyDepthOptions,
ApplyShapeOptions
} from './active-eidos.svelte'; } from './active-eidos.svelte';
export type { export type {
EidosConfigDocument, EidosConfigDocument,

@ -0,0 +1,42 @@
/**
* Runtime shape retune — the shape analogue of `build-scheme.ts` (color),
* `build-type-scale.ts` (typography) and `build-depth.ts` (depth). Pure + isomorphic, DOM-free.
*
* Given a seed it emits the shape overrides for a managed block: `--shape-smoothing` (live
* continuity / squircle intensity) + `--shape-nest-gap` as `:root` variables, and any
* `families` as `[data-shape='{family}']` rules that win over the foundation. The jaula-abierta
* runtime of the shape channel — dial the corner continuity from a theme editor, no rebuild.
*
* `ActiveEidos.applyShape(seed)` writes the result as a managed block; the composition is here,
* the DOM application lives on ActiveEidos, matching the other three builders.
*/
export interface ShapeSeed {
/** Superellipse exponent for the `continuous` family (1 = arc, 2 = squircle). */
readonly smoothing?: string | number;
/** Concentric nesting gap (`--shape-nest-gap`). */
readonly nestGap?: string;
/** `data-shape='{family}'` → `corner-shape` value — override or add families at runtime. */
readonly families?: Record<string, string>;
}
export interface BuildShapeResult {
/** `:root` variable declarations (`--shape-smoothing` / `--shape-nest-gap`). */
readonly variables: readonly string[];
/** Full `[data-shape='{family}'] { corner-shape: … }` rule strings. */
readonly families: readonly string[];
}
/** Build the shape override declarations + family rules from a runtime seed. */
export function buildShape(seed: ShapeSeed): BuildShapeResult {
const variables: string[] = [];
if (seed.smoothing !== undefined) variables.push(`--shape-smoothing: ${seed.smoothing};`);
if (seed.nestGap !== undefined) variables.push(`--shape-nest-gap: ${seed.nestGap};`);
const families: string[] = [];
for (const [family, value] of Object.entries(seed.families ?? {})) {
families.push(`[data-shape='${family}'] { corner-shape: ${value}; }`);
}
return { variables, families };
}
Loading…
Cancel
Save

Powered by TurnKey Linux.