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.
105 lines
4.2 KiB
105 lines
4.2 KiB
/**
|
|
* Theme studio — the `ThemeDraft` model + its application glue.
|
|
*
|
|
* A `ThemeDraft` is a superset of the framework's `ThemeSeed`: the six
|
|
* perceptual axes `applyTheme` composes (color · type · shape · depth · space ·
|
|
* gradient) PLUS the non-seed knobs a theme author still wants — font families,
|
|
* global roundness, and a raw escape hatch for advanced tokens (focus-ring /
|
|
* state-layer / border widths …).
|
|
*
|
|
* It maps onto the three application mechanisms `ActiveEidos` already exposes,
|
|
* which touch DISJOINT token sets (no cascade conflict):
|
|
*
|
|
* - `seed` → `eidos.applyTheme(seed, { selector })`
|
|
* - `fonts` · `radiusFactor` · `advanced`
|
|
* → `eidos.setCssVariables(map, { selector, strict: false })`
|
|
* - structure (mode / density / scaling) is driven by the page's reactive
|
|
* sources, NOT here — it lives on `data-*` attrs / `modeSource`.
|
|
*
|
|
* Route-level glue (single consumer, per the "ruta-herramienta" decision). If a
|
|
* reusable `<ThemeBuilder>` is extracted later, this graduates to `src/` with a
|
|
* unit test; today its only real risk is DOM integration, verified live in the
|
|
* studio preview.
|
|
*/
|
|
import type { ActiveEidos, ThemeSeed } from '$uix/eidos'
|
|
import type { EidosCssVariableMap } from '$uix/eidos/lib/config-types'
|
|
|
|
export type ThemeMode = 'light' | 'dark'
|
|
|
|
/** Font-family overrides → `--font-family-{key}`. */
|
|
export interface ThemeFonts {
|
|
readonly primary?: string
|
|
readonly display?: string
|
|
readonly mono?: string
|
|
}
|
|
|
|
/** The CSS selector the whole draft is scoped to (the preview region). */
|
|
export const PREVIEW_SELECTOR = '[data-estudio-preview]'
|
|
|
|
/**
|
|
* Everything the studio edits. The `seed` axes go through `applyTheme`; the
|
|
* rest are token overrides through `setCssVariables`.
|
|
*/
|
|
export interface ThemeDraft {
|
|
readonly seed: ThemeSeed
|
|
readonly fonts?: ThemeFonts
|
|
/** Global roundness knob → `--radius-factor` (foundation default 1). */
|
|
readonly radiusFactor?: number
|
|
/** Raw token → value escape hatch (focus-ring, state-layer, border, opacity…). */
|
|
readonly advanced?: Readonly<Record<string, string>>
|
|
}
|
|
|
|
/** Map the non-seed axes of a draft to a `--token` map for `setCssVariables`. */
|
|
export function draftToVariables(draft: ThemeDraft): EidosCssVariableMap {
|
|
const out: Record<string, string> = {}
|
|
if (draft.fonts?.primary) out['--font-family-primary'] = draft.fonts.primary
|
|
if (draft.fonts?.display) out['--font-family-display'] = draft.fonts.display
|
|
if (draft.fonts?.mono) out['--font-family-mono'] = draft.fonts.mono
|
|
if (draft.radiusFactor !== undefined) out['--radius-factor'] = String(draft.radiusFactor)
|
|
if (draft.advanced) Object.assign(out, draft.advanced)
|
|
return out
|
|
}
|
|
|
|
export interface ApplyDraftOptions {
|
|
readonly selector?: string
|
|
/** Force the color axis donor mode (light/dark) for the scoped scheme. */
|
|
readonly mode?: ThemeMode
|
|
}
|
|
|
|
/**
|
|
* Apply a whole draft to the (scoped) preview: the seed via `applyTheme`, the
|
|
* font / radius / advanced overrides via `setCssVariables`. Both target the same
|
|
* selector so the preview region — and nothing else — re-skins.
|
|
*/
|
|
export function applyDraft(
|
|
eidos: ActiveEidos,
|
|
draft: ThemeDraft,
|
|
options: ApplyDraftOptions = {}
|
|
): void {
|
|
const selector = options.selector ?? PREVIEW_SELECTOR
|
|
eidos.applyTheme(draft.seed, {
|
|
selector,
|
|
color: options.mode ? { mode: options.mode } : undefined
|
|
})
|
|
// `strict: false` — some knobs (`--radius-factor`) and every advanced token
|
|
// (`--state-*`, hand-written in archetypes.css) live outside the generated
|
|
// contract, so a contract-validated write would reject them.
|
|
eidos.setCssVariables(draftToVariables(draft), { selector, strict: false })
|
|
}
|
|
|
|
/** Revert every mechanism the draft touched. */
|
|
export function clearDraft(eidos: ActiveEidos): void {
|
|
eidos.clearTheme()
|
|
eidos.clearCssVariables()
|
|
}
|
|
|
|
/** A copy-pasteable `applyTheme(…)` (+ overrides) snippet for the current draft. */
|
|
export function draftToCode(draft: ThemeDraft): string {
|
|
let code = `eidos.applyTheme(${JSON.stringify(draft.seed, null, 2)})`
|
|
const vars = draftToVariables(draft)
|
|
if (Object.keys(vars).length > 0) {
|
|
code += `\n\neidos.setCssVariables(\n ${JSON.stringify(vars, null, 2).replace(/\n/g, '\n ')},\n { strict: false }\n)`
|
|
}
|
|
return code
|
|
}
|