Brand-prompt → Tier-2 token-override generator. Local deterministic
generator by default; pluggable async LLM hook. WCAG-correct
contrast enforcement with AA / AAA lock policies.
──────────────────────────────────────────────────────────────────
Module surface
──────────────────────────────────────────────────────────────────
generateThemeFromPrompt(prompt, opts?)
- opts.generate: optional async LLM hook (host wires real LLM)
- opts.enforce: 'aa' (default) | 'aaa' | 'off'
- Always runs the contrast pass when enforce !== 'off'
- WAVE 13.E.1
localGenerate(prompt)
- Synchronous; 7 hand-curated palettes (midnight, citrus,
forest, ocean, rose, graphite, violet) + default fallback
- Keyword-matched via regex; deterministic for caching
Contrast utilities (Wave 13.E.2):
parseHex / toHex — input + output round-trip safe
relativeLuminance — WCAG 2.x luminance
contrast(a, b) — pair contrast ratio
report(fg, bg) — { ratio, aa, aaa }
auditTheme(theme) — 4 canonical text/accent pairings
adjustForContrast(fg,bg,t) — iteratively darken/lighten until ≥ t
enforceContrast(theme,'aa') — returns adjusted ThemeProposal
applyTheme(theme, target?)
- Writes 11 --bl-* custom properties onto the target element
- Returns a tear-down fn (saved prior values, restored on call)
──────────────────────────────────────────────────────────────────
Quality gates
──────────────────────────────────────────────────────────────────
✓ 18/18 tests passing (hex / contrast math / generator /
enforcement / applyTheme cleanup)
✓ tsc build clean
✓ Zero runtime deps, pure functions where possible
──────────────────────────────────────────────────────────────────
Roadmap (lands in subsequent commit)
──────────────────────────────────────────────────────────────────
13.E.1 Deterministic prompt → palette generator
13.E.2 Contrast checker + AA / AAA enforcement
Showcase route /futurism/theme-studio (MAG.5) lands in paired
showcase commit.
46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import type { ThemeProposal } from './types.js';
|
|
|
|
/**
|
|
* Apply a `ThemeProposal` as CSS custom-property overrides on a
|
|
* target element. Defaults to `document.documentElement` so the
|
|
* entire page re-tints in one call.
|
|
*
|
|
* Returns a function that *removes* the overrides — useful for
|
|
* temporary preview surfaces (e.g. the showcase studio).
|
|
*/
|
|
export function applyTheme(
|
|
theme: ThemeProposal,
|
|
target?: HTMLElement,
|
|
): () => void {
|
|
if (typeof document === 'undefined') return () => {};
|
|
const el = target ?? document.documentElement;
|
|
const mapping: Array<[keyof ThemeProposal, string]> = [
|
|
['accent', '--bl-accent'],
|
|
['surface', '--bl-surface'],
|
|
['surfaceCard', '--bl-surface-card'],
|
|
['surfaceMuted', '--bl-surface-muted'],
|
|
['textPrimary', '--bl-text-primary'],
|
|
['textSecondary', '--bl-text-secondary'],
|
|
['success', '--bl-success'],
|
|
['warning', '--bl-warning'],
|
|
['danger', '--bl-danger'],
|
|
['info', '--bl-info'],
|
|
['border', '--bl-border'],
|
|
];
|
|
const previous = mapping.map(
|
|
([_, css]) => [css, el.style.getPropertyValue(css)] as const,
|
|
);
|
|
for (const [k, css] of mapping) {
|
|
const v = theme[k];
|
|
if (typeof v === 'string') {
|
|
el.style.setProperty(css, v);
|
|
}
|
|
}
|
|
return () => {
|
|
for (const [css, prev] of previous) {
|
|
if (prev) el.style.setProperty(css, prev);
|
|
else el.style.removeProperty(css);
|
|
}
|
|
};
|
|
}
|