From 1aa9180641e79c1616fffd7f895ce62b63314d4e Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Tue, 7 Jul 2026 11:49:45 -0400
Subject: [PATCH 01/33] feat: rework to oklch and add themes
---
.changeset/twelve-shrimps-report.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/twelve-shrimps-report.md
diff --git a/.changeset/twelve-shrimps-report.md b/.changeset/twelve-shrimps-report.md
new file mode 100644
index 000000000..28c4b94f6
--- /dev/null
+++ b/.changeset/twelve-shrimps-report.md
@@ -0,0 +1,5 @@
+---
+'@fuzdev/fuz_css': minor
+---
+
+feat: rework to oklch and add themes
From 4b59e87713e4c4f85cbda15172863d118ff43a8a Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Tue, 7 Jul 2026 14:18:24 -0400
Subject: [PATCH 02/33] feat: themes and oklch
---
.changeset/twelve-shrimps-report.md | 56 +-
CLAUDE.md | 56 +-
examples/vite-preact/src/App.tsx | 10 +-
examples/vite-react/src/App.tsx | 10 +-
examples/vite-solid/src/App.tsx | 10 +-
examples/vite-svelte/src/App.svelte | 14 +-
src/lib/css_bundled_resolution.ts | 22 +-
src/lib/css_class_composites.ts | 72 +-
src/lib/css_class_definitions.ts | 68 +-
src/lib/css_class_generators.ts | 2 +-
src/lib/css_plugin_options.ts | 5 -
src/lib/css_variable_utils.ts | 2 +-
src/lib/gen_fuz_css.ts | 2 -
src/lib/generate_css.ts | 3 -
src/lib/oklch.ts | 178 +
src/lib/ramps.ts | 341 ++
src/lib/style.css | 1397 +++---
src/lib/style_rule_parser.ts | 94 +-
src/lib/theme.css | 2307 ++++++----
src/lib/theme.gen.css.ts | 4 +-
src/lib/theme.ts | 27 +-
src/lib/themes.ts | 56 +-
src/lib/themes/base.ts | 9 +
src/lib/themes/brutalist.ts | 52 +
src/lib/themes/dark_only.ts | 26 +
src/lib/themes/high_contrast.ts | 15 +
src/lib/themes/low_contrast.ts | 13 +
src/lib/themes/necromancer.ts | 54 +
src/lib/themes/sunset_ember.ts | 36 +
src/lib/themes/terminal.ts | 57 +
src/lib/variable_graph.ts | 8 +-
src/lib/variables.ts | 2673 +++++------
src/lib/vite_plugin_fuz_css.ts | 2 -
src/lib/wcag.ts | 31 +
src/routes/ThemeForm.svelte | 6 +-
src/routes/docs/borders/+page.svelte | 10 +-
src/routes/docs/buttons/+page.svelte | 6 +-
src/routes/docs/chips/+page.svelte | 8 +-
src/routes/docs/classes/+page.svelte | 27 +-
src/routes/docs/colors/+page.svelte | 170 +-
src/routes/docs/colors/ColorSwatch.svelte | 29 +-
src/routes/docs/colors/ColorSwatchItem.svelte | 71 +-
src/routes/docs/colors/HueSwatch.svelte | 3 +-
src/routes/docs/shading/+page.svelte | 2 +-
src/routes/docs/shadows/+page.svelte | 28 +-
src/routes/docs/themes/+page.svelte | 27 +
src/routes/docs/typography/+page.svelte | 2 +-
src/test/class_variable_index.test.ts | 6 +-
...css_bundled_resolution.diagnostics.test.ts | 20 +-
src/test/css_bundled_resolution.test.ts | 154 +-
.../css_bundled_resolution.variables.test.ts | 17 +-
src/test/css_literal.test.ts | 2 +-
src/test/css_plugin_options.test.ts | 4 +-
src/test/css_ruleset_parser.modifiers.test.ts | 8 +-
src/test/css_ruleset_parser.parse.test.ts | 6 +-
src/test/css_variable_utils.test.ts | 4 +-
src/test/fixtures/css_classes_fixture.json | 4046 +++++------------
src/test/generate_classes_css.test.ts | 4 +-
src/test/modified_class_interpreter.test.ts | 4 +-
src/test/oklch.test.ts | 137 +
src/test/ramps.test.ts | 189 +
src/test/style_rule_parser.test.ts | 10 +-
src/test/styles.test.ts | 9 +-
src/test/themes.test.ts | 30 +
src/test/variable.test.ts | 2 +-
src/test/variable_graph.test.ts | 42 +-
src/test/variables.test.ts | 7 +-
src/test/vite_plugin_examples.test.ts | 22 +-
68 files changed, 6465 insertions(+), 6359 deletions(-)
create mode 100644 src/lib/oklch.ts
create mode 100644 src/lib/ramps.ts
create mode 100644 src/lib/themes/base.ts
create mode 100644 src/lib/themes/brutalist.ts
create mode 100644 src/lib/themes/dark_only.ts
create mode 100644 src/lib/themes/high_contrast.ts
create mode 100644 src/lib/themes/low_contrast.ts
create mode 100644 src/lib/themes/necromancer.ts
create mode 100644 src/lib/themes/sunset_ember.ts
create mode 100644 src/lib/themes/terminal.ts
create mode 100644 src/lib/wcag.ts
create mode 100644 src/test/oklch.test.ts
create mode 100644 src/test/ramps.test.ts
diff --git a/.changeset/twelve-shrimps-report.md b/.changeset/twelve-shrimps-report.md
index 28c4b94f6..c217dd2ac 100644
--- a/.changeset/twelve-shrimps-report.md
+++ b/.changeset/twelve-shrimps-report.md
@@ -2,4 +2,58 @@
'@fuzdev/fuz_css': minor
---
-feat: rework to oklch and add themes
+feat: rework the color system to derived OKLCH, add semantic roles, cascade layers, and themes
+
+The color system is now derived: curve knobs → ramp stops → color stops →
+utility classes, computed in pure CSS (`calc()`/`pow()`/`oklch()`), fitted to
+minimize the perceptual delta from the old HSL palette. Breaking changes:
+
+- **`color_` family renamed to `palette_`**: variables `--color_a_50` →
+ `--palette_a_50`, token classes `.color_a_50` → `.palette_a_50`, semantic
+ component classes `.color_a`–`.color_j` → `.palette_a`–`.palette_j`, and
+ the hue+intensity class families `border_color_X_NN` → `border_palette_X_NN`,
+ `outline_color_X_NN` → `outline_palette_X_NN`, `shadow_color_X_NN` →
+ `shadow_palette_X_NN`. `bg_X_NN` classes keep their names. The
+ `border_color_NN` alpha-ramp family (no hue letter) keeps its names.
+- **`--hue_a`…`--hue_j` values reinterpreted** as OKLCH hue angles (blue is
+ now `250`, not HSL `210`). Any consumer CSS doing `hsl(var(--hue_x) …)`
+ breaks — use `oklch( var(--hue_x))` or the palette/role stops.
+- **`--tint_hue`/`--tint_saturation` removed** → `--hue_neutral` (defaults to
+ `var(--hue_f)`) + `--neutral_chroma` (peak chroma of the neutral scales).
+- **Absolute `_light`/`_dark` variants removed** (~260 generated variables
+ like `--color_a_50_light`/`--shade_20_dark` and their classes, including
+ `shade_XX_light/dark`). Write the literal color or define one custom
+ property instead.
+- **New curve knobs** (the promoted theme API): `--chroma_scale`,
+ `--hue_shift`, `--palette_lightness_00/_100/_curve` (same trio for
+ `shade_`/`text_`), `--palette_chroma_min/_max/_curve` (clamped per stop by
+ baked worst-hue sRGB gamut caps), plus derived per-stop variables
+ (`--palette_lightness_NN`, `--palette_chroma_NN`, `--chroma_shape_NN`,
+ `--hue_shift_NN`) that themes can pin individually.
+- **New semantic role knobs**: `--hue_accent`, `--hue_positive`,
+ `--hue_negative`, `--hue_caution`, `--hue_info`, with derived role stops
+ `--accent_50`, `--accent_60`, `--negative_40`, `--negative_50` and the
+ `--selection_color` site variable. Links, focus, selection, selected
+ states, `accent-color`, and disabled-active feedback all route through
+ them; focus now follows the element color (via `--outline_color`) with the
+ accent as fallback.
+- **Cascade layers**: all shipped CSS is layered `fuz.base` (defaults) <
+ `fuz.theme` (theme overrides) < `fuz.utilities` (generated classes);
+ consumers' unlayered styles beat everything. `render_theme_style` lost its
+ `specificity` option (the `:root:root` hack) and gained
+ `layer?: string | null` (default `'fuz.theme'`); `generate_theme_css` lost
+ its specificity parameter; the `theme_specificity` generator option is
+ removed.
+- **`color-mix()` interpolation moved from `in hsl` to `in oklab`** in button
+ fills/borders, composites, and shadow classes.
+- **Themes**: one module per theme under `themes/`
+ (`@fuzdev/fuz_css/themes/necromancer.ts` etc.); `themes.ts` exports the
+ curated `default_themes` registry (base, low contrast, high contrast,
+ terminal green) with unregistered expressive exemplars (necromancer,
+ sunset ember, brutalist). The contrast themes are rewritten as curve-knob
+ overrides.
+- **New design-time modules**: `ramps.ts` (fitted knob constants, numeric
+ evaluators, CSS emitters), `oklch.ts` (OKLCH↔sRGB + gamut math), `wcag.ts`
+ (luminance/contrast), with tests gating every default stop for gamut,
+ monotonicity, and contrast (AA/AAA thresholds the old palette partly
+ failed).
diff --git a/CLAUDE.md b/CLAUDE.md
index c6948b9c9..2c30c03b5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -74,7 +74,7 @@ should answer "what specific gap in the defaults does this close?" — the most
common misuse is hand-spacing elements that flow margin already spaces, or
re-declaring typography/color the element already carries. When you do style,
work down the ladder and stop at the first rung that suffices: right semantic
-element → built-in class convention (`.selected`, `.color_a`) → composite
+element → built-in class convention (`.selected`, `.palette_a`) → composite
(`box`, `row`, `panel`) → token class (`p_md`, `gap_lg`) → literal
(`display:flex`) → `
diff --git a/src/routes/ThemeEditor.svelte b/src/routes/ThemeEditor.svelte
new file mode 100644
index 000000000..97f588a72
--- /dev/null
+++ b/src/routes/ThemeEditor.svelte
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+ {#each quick_knobs as knob (knob.name)}
+ editor.set_value(knob.name, value, editing_scheme)}
+ onreset={() => editor.reset(knob.name)}
+ />
+ {/each}
+
+
+
+ {#each axes as { axis, title } (axis)}
+ {@const knobs = semantic_knobs(axis)}
+ {#if knobs.length}
+
+ {title}
+
+ {#each knobs as knob (knob.name)}
+ editor.set_value(knob.name, value, editing_scheme)}
+ onreset={() => editor.reset(knob.name)}
+ />
+ {/each}
+
+ {#if axis === 'color'}
+
+ palette hues (the letter slots - moving these makes the theme palette-tier)
+
+ {#each palette_knobs as knob (knob.name)}
+ editor.set_value(knob.name, value, editing_scheme)}
+ onreset={() => editor.reset(knob.name)}
+ />
+ {/each}
+
+
+ {/if}
+
+ {/if}
+ {/each}
+
+
+ Output
+
+ The copyable Theme object, and the CSS it renders — the theme's own footprint.
+
+
+
+
+
+
+
diff --git a/src/routes/ThemeForm.svelte b/src/routes/ThemeForm.svelte
deleted file mode 100644
index 5ce62cd2c..000000000
--- a/src/routes/ThemeForm.svelte
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-{#if selected_variable}
- (selected_variable = null)}>
-
- {#snippet children({close})}
-
-
-
- ok
-
- {/snippet}
-
-
-{/if}
-
-
diff --git a/src/routes/docs/colors/+page.svelte b/src/routes/docs/colors/+page.svelte
index 1e082c46c..2a5d1e6c0 100644
--- a/src/routes/docs/colors/+page.svelte
+++ b/src/routes/docs/colors/+page.svelte
@@ -52,7 +52,8 @@
Hues use letters so themes can reassign colors without breaking semantics -- "a" is blue by
default but could be any color. Meaning attaches through the role knobs layered on top:
- --hue_accent (links, focus, selection, selected states) defaults to
+ --hue_accent (links, focus, selection, selected states -- what other systems call
+ the "primary" color, named here for what it communicates) defaults to
--hue_a, --hue_negative to --hue_c, and so on. Retarget
a role to move just that meaning; rotate a letter to move the palette.
diff --git a/src/routes/docs/themes/+page.svelte b/src/routes/docs/themes/+page.svelte
index 1147c58d3..465225cdb 100644
--- a/src/routes/docs/themes/+page.svelte
+++ b/src/routes/docs/themes/+page.svelte
@@ -1,7 +1,5 @@
@@ -98,9 +142,9 @@
module_path="theme.ts"
/> and .
-
+ Selecting a theme loads its knobs into the editor below.
- (editing_theme = t)} />
+
@@ -115,21 +159,18 @@
content={`import {necromancer_theme} from '@fuzdev/fuz_css/themes/necromancer.ts';`}
/>
- (editing_theme = t)} />
+
+
+
+
+ Drag a knob and the whole page rethemes live -- extreme values can make the page hard to read,
+ which is an honest signal, not a bug. Every edit updates a temporary "{UNSAVED_THEME_NAME}"
+ theme in the picker above; it persists across navigation until you leave or reset, so copy
+ the Theme object below to keep it. Knob sizes reflect leverage: the big controls
+ reshape the whole system, the small ones are surgical escape hatches.
+
+
+
-
-{#if editing_theme}
- (editing_theme = null)}>
-
- {
- console.log(`update theme`, theme); // eslint-disable-line no-console
- alert('todo'); // eslint-disable-line no-alert
- }}
- />
-
-
-{/if}
diff --git a/src/routes/theme_editor_state.svelte.ts b/src/routes/theme_editor_state.svelte.ts
new file mode 100644
index 000000000..c3f3e181a
--- /dev/null
+++ b/src/routes/theme_editor_state.svelte.ts
@@ -0,0 +1,219 @@
+import {SvelteMap} from 'svelte/reactivity';
+
+import type {Theme} from '$lib/theme.ts';
+import type {StyleVariable} from '$lib/variable.ts';
+import {default_variables} from '$lib/variables.ts';
+import {theme_knob_by_name} from '$lib/knobs.ts';
+import type {ColorSchemeVariant} from '$lib/variable_data.ts';
+
+// TODO upsteam to fuz_ui
+
+/**
+ * The name of the in-progress theme shown in pickers. Never `'base'`, which
+ * `render_theme_style`/`ThemeRoot` special-case to render nothing, and never
+ * a registry/exemplar name, which would collide with `ThemeInput`'s
+ * name-keyed selection.
+ */
+export const UNSAVED_THEME_NAME = 'unsaved';
+
+const default_variable_by_name: Map = new Map(
+ default_variables.map((v) => [v.name, v]),
+);
+
+export interface SlotOverride {
+ light?: string;
+ dark?: string;
+}
+
+export interface ThemeEditorSnapshotData {
+ name: string;
+ based_on: string;
+ overrides: Array<[string, SlotOverride]>;
+}
+
+/**
+ * State for the inline theme editor: a base theme selected by name plus a map
+ * of per-variable slot overrides, merged into a draft `Theme` on the fly.
+ *
+ * Scheme semantics: variables whose effective definition is scheme-adaptive
+ * (dual-slot) edit the slot of the scheme being viewed; single-slot variables
+ * always edit the light (base) slot so the change applies to both schemes.
+ * When a fresh light-slot override lands on a variable whose default is
+ * dual-slot, the merge preserves the default's dark slot explicitly — a
+ * theme's `:root` block beats the base defaults' `:root.dark` by cascade
+ * layer order, so omitting it would silently change dark mode too. A base
+ * theme that itself sets only light slots (e.g. dark-only mirrors) chose
+ * those cross-scheme semantics deliberately and is left alone.
+ */
+export class ThemeEditorState {
+ readonly themes: Array = [];
+
+ name: string = $state.raw('new theme');
+ based_on: string = $state.raw('base');
+ readonly overrides: SvelteMap = new SvelteMap();
+
+ constructor(themes: Array) {
+ this.themes = themes;
+ }
+
+ readonly base_theme: Theme = $derived(
+ this.themes.find((t) => t.name === this.based_on) ?? this.themes[0]!,
+ );
+
+ readonly base_variable_by_name: Map = $derived(
+ new Map(this.base_theme.variables.map((v) => [v.name, v])),
+ );
+
+ readonly dirty: boolean = $derived(this.overrides.size > 0);
+
+ /**
+ * True when the draft (or its base) moves palette-tier knobs — the letter
+ * hues — making it an exemplar-tier theme per the two-tier policy.
+ */
+ readonly is_palette_tier: boolean = $derived.by(() => {
+ for (const name of this.overrides.keys()) {
+ if (theme_knob_by_name.get(name)?.tier === 'palette') return true;
+ }
+ for (const v of this.base_theme.variables) {
+ if (theme_knob_by_name.get(v.name)?.tier === 'palette') return true;
+ }
+ return false;
+ });
+
+ readonly merged_variables: Array = $derived.by(() => {
+ const merged: Array = [];
+ const seen: Set = new Set();
+ for (const v of this.base_theme.variables) {
+ seen.add(v.name);
+ const m = this.#merge_variable(v.name);
+ if (m) merged.push(m);
+ }
+ for (const name of this.overrides.keys()) {
+ if (seen.has(name)) continue;
+ const m = this.#merge_variable(name);
+ if (m) merged.push(m);
+ }
+ return merged;
+ });
+
+ #merge_variable(name: string): StyleVariable | null {
+ const o = this.overrides.get(name);
+ const b = this.base_variable_by_name.get(name);
+ const light = o?.light ?? b?.light;
+ let dark = o?.dark ?? b?.dark;
+ // preserve a scheme-adaptive default's dark slot for fresh light-only
+ // overrides - see the class comment for the cascade-layer rationale
+ if (o?.light !== undefined && dark === undefined && !b) {
+ dark = default_variable_by_name.get(name)?.dark;
+ }
+ if (dark !== undefined && dark === light) dark = undefined;
+ if (light === undefined && dark === undefined) return null;
+ const merged: StyleVariable = {name};
+ if (light !== undefined) merged.light = light;
+ if (dark !== undefined) merged.dark = dark;
+ return merged;
+ }
+
+ /** The live-applied theme, stably named so pickers key it consistently. */
+ readonly draft: Theme = $derived({name: UNSAVED_THEME_NAME, variables: this.merged_variables});
+
+ /** The copyable theme, carrying the user's chosen name. */
+ readonly output: Theme = $derived({name: this.name, variables: this.merged_variables});
+
+ /**
+ * The value a scheme currently renders for a variable, accounting for the
+ * theme layer's light slots beating the base defaults' dark slots.
+ */
+ display_value(name: string, scheme: ColorSchemeVariant): string | undefined {
+ const o = this.overrides.get(name);
+ const b = this.base_variable_by_name.get(name);
+ const d = default_variable_by_name.get(name);
+ if (scheme === 'light') return o?.light ?? b?.light ?? d?.light;
+ return o?.dark ?? b?.dark ?? o?.light ?? b?.light ?? d?.dark ?? d?.light;
+ }
+
+ changed(name: string): boolean {
+ return this.overrides.has(name);
+ }
+
+ set_value(name: string, value: string, scheme: ColorSchemeVariant): void {
+ const o = this.overrides.get(name);
+ const b = this.base_variable_by_name.get(name);
+ const d = default_variable_by_name.get(name);
+ const adaptive = d?.dark !== undefined || b?.dark !== undefined || o?.dark !== undefined;
+ const slot = adaptive ? scheme : 'light';
+ this.overrides.set(name, {...o, [slot]: value});
+ }
+
+ reset(name: string): void {
+ this.overrides.delete(name);
+ }
+
+ reset_all(): void {
+ this.overrides.clear();
+ }
+
+ /**
+ * Loads a theme as the new base: overrides clear and the editor edits on
+ * top of its flattened variables (flatten-on-load composition).
+ *
+ * @mutates `this`
+ */
+ load_theme(theme: Theme): void {
+ if (theme.name === UNSAVED_THEME_NAME) return;
+ this.based_on = theme.name;
+ this.overrides.clear();
+ this.name = theme.name === 'base' ? 'new theme' : `custom ${theme.name}`;
+ }
+
+ to_snapshot(): ThemeEditorSnapshotData {
+ return {
+ name: this.name,
+ based_on: this.based_on,
+ overrides: Array.from(this.overrides.entries()).map(([name, o]) => [name, {...o}]),
+ };
+ }
+
+ /**
+ * @mutates `this`
+ */
+ restore_snapshot(data: ThemeEditorSnapshotData): void {
+ this.name = data.name;
+ this.based_on = data.based_on;
+ this.overrides.clear();
+ for (const [name, o] of data.overrides) {
+ this.overrides.set(name, {...o});
+ }
+ }
+}
+
+const escape_single_quotes = (s: string): string =>
+ s.replaceAll('\\', '\\\\').replaceAll("'", "\\'");
+
+/**
+ * Renders a theme as a copyable TypeScript module.
+ */
+export const render_theme_ts = (theme: Theme): string => {
+ const identifier =
+ theme.name
+ .toLowerCase()
+ .replaceAll(/[^a-z0-9]+/gu, '_')
+ .replaceAll(/^_+|_+$/gu, '') || 'custom';
+ const variables = theme.variables
+ .map((v) => {
+ const parts = [`name: '${escape_single_quotes(v.name)}'`];
+ if (v.light !== undefined) parts.push(`light: '${escape_single_quotes(v.light)}'`);
+ if (v.dark !== undefined) parts.push(`dark: '${escape_single_quotes(v.dark)}'`);
+ return `\t\t{${parts.join(', ')}},`;
+ })
+ .join('\n');
+ return `import type {Theme} from '@fuzdev/fuz_css/theme.ts';
+
+export const ${identifier}_theme: Theme = {
+ name: '${escape_single_quotes(theme.name)}',
+ variables: [
+${variables}
+ ],
+};
+`;
+};
diff --git a/src/test/knobs.test.ts b/src/test/knobs.test.ts
new file mode 100644
index 000000000..e77f5950f
--- /dev/null
+++ b/src/test/knobs.test.ts
@@ -0,0 +1,70 @@
+import {test, assert, describe} from 'vitest';
+
+import {theme_knobs, theme_knob_by_name, theme_knob_hook_names} from '$lib/knobs.ts';
+import {default_variables} from '$lib/variables.ts';
+
+const declared_names = new Set(default_variables.map((v) => v.name));
+
+describe('theme_knobs', () => {
+ test('knob names are unique', () => {
+ const names = theme_knobs.map((k) => k.name);
+ assert.strictEqual(new Set(names).size, names.length);
+ });
+
+ test('non-hook knobs resolve to declared variables', () => {
+ for (const knob of theme_knobs) {
+ if (knob.hook) continue;
+ assert.isTrue(
+ declared_names.has(knob.name),
+ `Knob "${knob.name}" is not declared in default_variables`,
+ );
+ }
+ });
+
+ test('hook knobs are not declared variables', () => {
+ // a hook knob that gains a declaration should drop its stale `hook` flag
+ for (const name of theme_knob_hook_names) {
+ assert.isFalse(
+ declared_names.has(name),
+ `Hook knob "${name}" is declared in default_variables - remove its hook flag`,
+ );
+ }
+ });
+
+ test('ranges and steps are sane', () => {
+ for (const knob of theme_knobs) {
+ if (knob.range) {
+ assert.isBelow(knob.range[0], knob.range[1], `Knob "${knob.name}" has an inverted range`);
+ }
+ if (knob.step !== undefined) {
+ assert.isAbove(knob.step, 0, `Knob "${knob.name}" has a nonpositive step`);
+ }
+ if (knob.kind === 'enum') {
+ assert.isAbove(knob.values?.length ?? 0, 0, `Enum knob "${knob.name}" needs values`);
+ }
+ }
+ });
+
+ test('palette tier is exactly the letter hues', () => {
+ const palette_names = theme_knobs.filter((k) => k.tier === 'palette').map((k) => k.name);
+ assert.deepEqual(palette_names, [
+ 'hue_a',
+ 'hue_b',
+ 'hue_c',
+ 'hue_d',
+ 'hue_e',
+ 'hue_f',
+ 'hue_g',
+ 'hue_h',
+ 'hue_i',
+ 'hue_j',
+ ]);
+ });
+
+ test('theme_knob_by_name indexes every knob', () => {
+ assert.strictEqual(theme_knob_by_name.size, theme_knobs.length);
+ for (const knob of theme_knobs) {
+ assert.strictEqual(theme_knob_by_name.get(knob.name), knob);
+ }
+ });
+});
diff --git a/src/test/styles.test.ts b/src/test/styles.test.ts
index 9fbafd508..b29f759cd 100644
--- a/src/test/styles.test.ts
+++ b/src/test/styles.test.ts
@@ -2,6 +2,7 @@ import {test, assert} from 'vitest';
import {readFileSync} from 'node:fs';
import * as exported_variables from '$lib/variables.ts';
+import {theme_knob_hook_names} from '$lib/knobs.ts';
import css_classes_text from './fixtures/css_classes_fixture.json?raw';
// vitest replaces this with an empty string because CSS isn't opted into being processed,
@@ -52,8 +53,11 @@ test('variables in the CSS exist', () => {
/**
* These variables are known to be in the CSS but not in the exported variables.
* This means they can be contextually used when defined, but otherwise have a fallback.
+ * Hook knobs from the catalog (e.g. `heading_font_weight`) are included
+ * automatically — they're defined as CSS-consumed-but-undeclared.
*/
const known_without_variables = new Set([
+ ...theme_knob_hook_names,
'fill', // contextual variable set by button palette classes (e.g., .palette_a sets --fill: var(--palette_a_40))
'button_fill',
'button_fill_hover',
diff --git a/src/test/themes.test.ts b/src/test/themes.test.ts
index c72d33d7b..f685067d2 100644
--- a/src/test/themes.test.ts
+++ b/src/test/themes.test.ts
@@ -3,16 +3,20 @@ import {test, assert, describe} from 'vitest';
import {default_themes, DEFAULT_THEME} from '$lib/themes.ts';
import {necromancer_theme} from '$lib/themes/necromancer.ts';
import {sunset_ember_theme} from '$lib/themes/sunset_ember.ts';
-import {brutalist_theme} from '$lib/themes/brutalist.ts';
+import {brutalish_theme} from '$lib/themes/brutalish.ts';
import {create_terminal_theme, terminal_green_theme} from '$lib/themes/terminal.ts';
import {default_variables} from '$lib/variables.ts';
import {StyleVariable} from '$lib/variable.ts';
+import {theme_knob_hook_names} from '$lib/knobs.ts';
+
+// declared variables plus the hook knobs style.css consumes via fallbacks
+const known_names = new Set([...default_variables.map((v) => v.name), ...theme_knob_hook_names]);
/** Shipped exemplar themes that deliberately stay out of the registry. */
const exemplar_themes = [
necromancer_theme,
sunset_ember_theme,
- brutalist_theme,
+ brutalish_theme,
terminal_green_theme,
create_terminal_theme(70), // amber, exercises the factory
];
@@ -71,7 +75,6 @@ describe('default_themes', () => {
});
test('theme variable names exist in default_variables', () => {
- const known_names = new Set(default_variables.map((v) => v.name));
for (const theme of default_themes) {
for (const variable of theme.variables) {
assert.isTrue(
@@ -92,7 +95,6 @@ describe('exemplar themes', () => {
});
test('all exemplar variables validate and exist in default_variables', () => {
- const known_names = new Set(default_variables.map((v) => v.name));
for (const theme of exemplar_themes) {
assert.isAbove(theme.variables.length, 0);
for (const variable of theme.variables) {
From 990448e0702cb82cc73c60f6d09b7f6fc448f4e5 Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Fri, 10 Jul 2026 14:03:01 -0400
Subject: [PATCH 05/33] wip
---
.changeset/twelve-shrimps-report.md | 12 +-
CLAUDE.md | 2 +-
package-lock.json | 8 +-
package.json | 2 +-
src/lib/css_class_definitions.ts | 54 ++++----
src/lib/ramps.ts | 12 +-
src/lib/variable_data.ts | 4 +-
src/routes/KnobControl.svelte | 119 ++++++++++--------
src/routes/ThemeEditor.svelte | 38 ++----
src/routes/docs/borders/+page.svelte | 8 +-
src/routes/docs/buttons/+page.svelte | 36 +++---
src/routes/docs/chips/+page.svelte | 12 +-
src/routes/docs/classes/+page.svelte | 17 +--
src/routes/docs/colors/+page.svelte | 12 +-
src/routes/docs/colors/ColorSwatch.svelte | 8 +-
src/routes/docs/colors/ColorSwatchItem.svelte | 7 +-
src/routes/docs/colors/HueSwatch.svelte | 8 +-
src/routes/docs/shadows/+page.svelte | 24 ++--
src/routes/theme_editor_state.svelte.ts | 16 +--
.../css_bundled_resolution.variables.test.ts | 2 +-
src/test/ramps.test.ts | 8 +-
21 files changed, 208 insertions(+), 201 deletions(-)
diff --git a/.changeset/twelve-shrimps-report.md b/.changeset/twelve-shrimps-report.md
index 9e5ff6f5f..e04918025 100644
--- a/.changeset/twelve-shrimps-report.md
+++ b/.changeset/twelve-shrimps-report.md
@@ -15,14 +15,17 @@ minimize the perceptual delta from the old HSL palette. Breaking changes:
`outline_color_X_NN` → `outline_palette_X_NN`, `shadow_color_X_NN` →
`shadow_palette_X_NN`. `bg_X_NN` classes keep their names. The
`border_color_NN` alpha-ramp family (no hue letter) keeps its names.
+ At the TS level, the letter list in `variable_data.ts` renames with the
+ family: `ColorVariant`/`color_variants` → `PaletteVariant`/`palette_variants`.
- **`--hue_a`…`--hue_j` values reinterpreted** as OKLCH hue angles (blue is
now `250`, not HSL `210`). Any consumer CSS doing `hsl(var(--hue_x) …)`
breaks — use `oklch( var(--hue_x))` or the palette/role stops.
- **`--tint_hue`/`--tint_saturation` removed** → `--hue_neutral` (defaults to
`var(--hue_f)`) + `--neutral_chroma` (peak chroma of the neutral scales).
-- **Absolute `_light`/`_dark` variants removed** (~260 generated variables
- like `--color_a_50_light`/`--shade_20_dark` and their classes, including
- `shade_XX_light/dark`). Write the literal color or define one custom
+- **Absolute `_light`/`_dark` variants removed**: the ~286 generated
+ variables (`--color_a_50_light`-style, including `--shade_XX_light/dark`)
+ and all their classes — `color_X_NN_light/dark`, `bg_X_NN_light/dark`, and
+ `shade_NN_light/dark`. Write the literal color or define one custom
property instead.
- **New curve knobs** (the promoted theme API): `--chroma_scale`,
`--hue_shift`, `--palette_lightness_00/_100/_curve` (same trio for
@@ -43,7 +46,8 @@ minimize the perceptual delta from the old HSL palette. Breaking changes:
`specificity` option (the `:root:root` hack) and gained
`layer?: string | null` (default `'fuz.theme'`); `generate_theme_css` lost
its specificity parameter; the `theme_specificity` generator option is
- removed.
+ removed. Custom `base_css` input is re-layered into `fuz.base` in bundled
+ output — its own `@layer` identities aren't preserved.
- **`color-mix()` interpolation moved from `in hsl` to `in oklab`** in button
fills/borders, composites, and shadow classes.
- **Themes**: one module per theme under `themes/`
diff --git a/CLAUDE.md b/CLAUDE.md
index c86cfb348..c5b1dd4fc 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -184,7 +184,7 @@ reaches any theme/base hook without a dedicated token class.
## Variable naming
See [variables.ts](src/lib/variables.ts) for definitions,
-[variable_data.ts](src/lib/variable_data.ts) for size/color variants.
+[variable_data.ts](src/lib/variable_data.ts) for size/palette variants.
**Colors (OKLCH, derived):**
diff --git a/package-lock.json b/package-lock.json
index 8977d1364..4b6d59c39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@changesets/changelog-git": "^0.2.1",
"@fuzdev/blake3_wasm": "^0.1.1",
"@fuzdev/fuz_code": "^0.47.0",
- "@fuzdev/fuz_ui": "^0.206.6",
+ "@fuzdev/fuz_ui": "^0.206.7",
"@fuzdev/fuz_util": "^0.65.2",
"@fuzdev/gro": "^0.206.0",
"@fuzdev/mdz": "^0.3.0",
@@ -837,9 +837,9 @@
}
},
"node_modules/@fuzdev/fuz_ui": {
- "version": "0.206.6",
- "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.206.6.tgz",
- "integrity": "sha512-LJzI0GgqGYojwhpR/vjTX5CEY5XJN3Nz2Qg/akgB0foa7u8YMrwT3nXPbH3LmTkjvP6eGI6j4lELHUdEEouPnw==",
+ "version": "0.206.7",
+ "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.206.7.tgz",
+ "integrity": "sha512-UUO+6zVK6Ot9NmZLaKWASJ6odjjLOUDObIWOjSKtoPBnSu62UvzlGYdSGEHYk1hSR58wNj0qndhWNDzmf/WDdg==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index b9ba3a86d..c7b31b017 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"@changesets/changelog-git": "^0.2.1",
"@fuzdev/blake3_wasm": "^0.1.1",
"@fuzdev/fuz_code": "^0.47.0",
- "@fuzdev/fuz_ui": "^0.206.6",
+ "@fuzdev/fuz_ui": "^0.206.7",
"@fuzdev/fuz_util": "^0.65.2",
"@fuzdev/gro": "^0.206.0",
"@fuzdev/mdz": "^0.3.0",
diff --git a/src/lib/css_class_definitions.ts b/src/lib/css_class_definitions.ts
index cf4259c42..cf270bb9e 100644
--- a/src/lib/css_class_definitions.ts
+++ b/src/lib/css_class_definitions.ts
@@ -19,7 +19,7 @@ import {
import {
space_variants,
distance_variants,
- color_variants,
+ palette_variants,
intensity_variants,
shade_variants,
shade_scale_variants,
@@ -96,30 +96,30 @@ export const css_class_definitions: Record ({
- name: `hue_${hue}`,
- css: `--hue: var(--hue_${hue});`,
+ (letter: string) => ({
+ name: `hue_${letter}`,
+ css: `--hue: var(--hue_${letter});`,
}),
- color_variants,
+ palette_variants,
),
// Palette intensity classes (text color)
...generate_classes(
- (hue: string, intensity: string) => ({
- name: `palette_${hue}_${intensity}`,
- css: `color: var(--palette_${hue}_${intensity}); --text_color: var(--palette_${hue}_${
+ (letter: string, intensity: string) => ({
+ name: `palette_${letter}_${intensity}`,
+ css: `color: var(--palette_${letter}_${intensity}); --text_color: var(--palette_${letter}_${
intensity
});`,
}),
- color_variants,
+ palette_variants,
intensity_variants,
),
// Palette intensity classes (background color)
...generate_classes(
- (hue: string, intensity: string) => ({
- name: `bg_${hue}_${intensity}`,
- css: `background-color: var(--palette_${hue}_${intensity});`,
+ (letter: string, intensity: string) => ({
+ name: `bg_${letter}_${intensity}`,
+ css: `background-color: var(--palette_${letter}_${intensity});`,
}),
- color_variants,
+ palette_variants,
intensity_variants,
),
// Darken/lighten overlays (non-adaptive, alpha-based)
@@ -164,26 +164,26 @@ export const css_class_definitions: Record ({
- name: `border_palette_${hue}_${intensity}`,
- css: `border-color: var(--palette_${hue}_${intensity}); --border_color: var(--palette_${
- hue
+ (letter: string, intensity: string) => ({
+ name: `border_palette_${letter}_${intensity}`,
+ css: `border-color: var(--palette_${letter}_${intensity}); --border_color: var(--palette_${
+ letter
}_${intensity});`,
}),
- color_variants,
+ palette_variants,
intensity_variants,
),
// Outline colors using shade scale
...generate_property_classes('outline-color', shade_variants, (v) => `var(--shade_${v})`),
// Outline colors using palette hue + intensity (sets both property and contextual variable)
...generate_classes(
- (hue: string, intensity: string) => ({
- name: `outline_palette_${hue}_${intensity}`,
- css: `outline-color: var(--palette_${hue}_${intensity}); --outline_color: var(--palette_${
- hue
+ (letter: string, intensity: string) => ({
+ name: `outline_palette_${letter}_${intensity}`,
+ css: `outline-color: var(--palette_${letter}_${intensity}); --outline_color: var(--palette_${
+ letter
}_${intensity});`,
}),
- color_variants,
+ palette_variants,
intensity_variants,
),
@@ -235,11 +235,11 @@ export const css_class_definitions: Record ({
- name: `shadow_palette_${hue}_${intensity}`,
- css: `--shadow_color: var(--palette_${hue}_${intensity});`,
+ (letter: string, intensity: string) => ({
+ name: `shadow_palette_${letter}_${intensity}`,
+ css: `--shadow_color: var(--palette_${letter}_${intensity});`,
}),
- color_variants,
+ palette_variants,
intensity_variants,
),
diff --git a/src/lib/ramps.ts b/src/lib/ramps.ts
index b68592200..35d5deff0 100644
--- a/src/lib/ramps.ts
+++ b/src/lib/ramps.ts
@@ -24,7 +24,7 @@
import {
numeric_scale_variants,
- type ColorVariant,
+ type PaletteVariant,
type ColorSchemeVariant,
type NumericScaleVariant,
} from './variable_data.ts';
@@ -69,7 +69,7 @@ export interface ChromaRampKnobs {
}
/** OKLCH hue angles for the 10 palette hues, fitted from the HSL palette. */
-export const PALETTE_HUES: Record = {
+export const PALETTE_HUES: Record = {
a: 250, // blue
b: 144, // green
c: 24, // red
@@ -215,7 +215,7 @@ export const ramp_chroma = (
* Computes the default OKLCH color of a palette stop (`--palette_X_NN`).
*/
export const palette_stop_oklch = (
- letter: ColorVariant,
+ letter: PaletteVariant,
stop: NumericScaleVariant,
scheme: ColorSchemeVariant,
): Oklch => [
@@ -318,8 +318,10 @@ export const render_hue_shift_offset_css = (
* Renders the derived default of a palette color stop, e.g. `--palette_a_50`.
* One definition serves both schemes — the scheme flip lives in the knobs.
*/
-export const render_palette_stop_css = (letter: ColorVariant, stop: NumericScaleVariant): string =>
- render_ramp_color_css(`var(--hue_${letter})`, stop);
+export const render_palette_stop_css = (
+ letter: PaletteVariant,
+ stop: NumericScaleVariant,
+): string => render_ramp_color_css(`var(--hue_${letter})`, stop);
/**
* Renders a color derived from the palette ramps at a stop for an arbitrary
diff --git a/src/lib/variable_data.ts b/src/lib/variable_data.ts
index 2d789e5f2..8e1bb1ed4 100644
--- a/src/lib/variable_data.ts
+++ b/src/lib/variable_data.ts
@@ -135,8 +135,8 @@ export const icon_sizes = {
icon_size_xl3: '256px',
};
-export type ColorVariant = ArrayElement;
-export const color_variants = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] as const;
+export type PaletteVariant = ArrayElement;
+export const palette_variants = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] as const;
export type IntensityVariant = NumericScaleVariant;
export const intensity_variants = numeric_scale_variants;
diff --git a/src/routes/KnobControl.svelte b/src/routes/KnobControl.svelte
index 907659a35..c0f2bfcb7 100644
--- a/src/routes/KnobControl.svelte
+++ b/src/routes/KnobControl.svelte
@@ -52,61 +52,67 @@
diff --git a/src/routes/ThemeEditor.svelte b/src/routes/ThemeEditor.svelte
index 97f588a72..e0d3b7c39 100644
--- a/src/routes/ThemeEditor.svelte
+++ b/src/routes/ThemeEditor.svelte
@@ -1,5 +1,5 @@
-
-
-
+
+
+
name
@@ -83,7 +83,7 @@
{/each}
-
+
{editor.is_palette_tier ? 'palette-tier' : 'semantic-tier'}
{#if editor.dirty}
-
editor.reset_all()}
+ editor.reset_all()}
>reset all ({editor.overrides.size})
{/if}
@@ -110,7 +110,7 @@
-
+
{#each quick_knobs as knob (knob.name)}
{title}
-
+
{#each knobs as knob (knob.name)}
(the letter slots - moving these makes the theme palette-tier)
-
+
{#each palette_knobs as knob (knob.name)}
The copyable Theme object, and the CSS it renders — the theme's own footprint.
-
+
-
+
diff --git a/src/routes/RampStrip.svelte b/src/routes/RampStrip.svelte
new file mode 100644
index 000000000..6132c95f9
--- /dev/null
+++ b/src/routes/RampStrip.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
{label}
+
+ {#each numeric_scale_variants as stop (stop)}
+
+ {/each}
+
+
+
+
diff --git a/src/routes/ThemeEditor.svelte b/src/routes/ThemeEditor.svelte
index c17b25502..a8efdd913 100644
--- a/src/routes/ThemeEditor.svelte
+++ b/src/routes/ThemeEditor.svelte
@@ -7,14 +7,21 @@
import type {ThemeState} from '@fuzdev/fuz_ui/theme_state.svelte.ts';
import {render_theme_style} from '$lib/theme.ts';
- import {theme_knobs, type KnobAxis, type ThemeKnob} from '$lib/knobs.ts';
- import type {ColorSchemeVariant} from '$lib/variable_data.ts';
+ import {theme_knobs, knob_axes, type KnobAxis, type ThemeKnob} from '$lib/knobs.ts';
+ import {PALETTE_HUES} from '$lib/ramps.ts';
+ import {
+ palette_variants,
+ intent_variants,
+ type ColorSchemeVariant,
+ type PaletteVariant,
+ } from '$lib/variable_data.ts';
import {
render_theme_ts,
UNSAVED_THEME_NAME,
type ThemeEditorState,
} from '$routes/theme_editor_state.svelte.ts';
import KnobControl from '$routes/KnobControl.svelte';
+ import RampStrip from '$routes/RampStrip.svelte';
// @fuz-classes sm md lg
@@ -37,21 +44,30 @@
: 'light';
});
- const axes: Array<{axis: KnobAxis; title: string}> = [
- {axis: 'color', title: 'Color'},
- {axis: 'shape', title: 'Shape'},
- {axis: 'density', title: 'Density'},
- {axis: 'depth', title: 'Depth'},
- {axis: 'typography', title: 'Typography'},
- {axis: 'motion', title: 'Motion'},
- {axis: 'decoration', title: 'Decoration'},
- ];
-
const semantic_knobs = (axis: KnobAxis): Array
=>
theme_knobs.filter((k) => k.axis === axis && k.tier === 'semantic');
const palette_knobs = theme_knobs.filter((k) => k.tier === 'palette');
- // the highest-leverage knobs duplicated compactly above the granular flow
- const quick_knobs = theme_knobs.filter((k) => k.leverage === 'lg');
+ // the design band: intent/neutral bindings plus the highest-leverage levers,
+ // duplicated compactly above the granular flow - same state, two densities
+ const binding_knobs = theme_knobs.filter((k) => k.bindable);
+ const lever_knobs = theme_knobs.filter((k) => k.leverage === 'lg' && !k.bindable);
+
+ // the sm escape-hatch walls fold behind a disclosure per axis so the levers
+ // stay the visual main flow
+ const sm_details_titles: Partial> = {
+ shape: 'border width & radius tokens',
+ density: 'space tokens',
+ typography: 'line height tokens',
+ motion: 'motion tokens',
+ };
+
+ // resolves a letter's current angle from the same merge the renderer uses,
+ // so binding chips and detachment track theme overrides
+ const resolve_hue = (letter: PaletteVariant): number => {
+ const v = editor.display_value(`hue_${letter}`, editing_scheme);
+ const n = Number(v);
+ return Number.isNaN(n) ? PALETTE_HUES[letter] : n;
+ };
const trimmed_name = $derived(editor.name.trim());
const name_collides = $derived(
@@ -62,6 +78,18 @@
const output_css = $derived(render_theme_style(editor.output));
+{#snippet knob_control(knob: ThemeKnob, compact: boolean)}
+ editor.set_value(knob.name, value, editing_scheme)}
+ onreset={() => editor.reset(knob.name)}
+ />
+{/snippet}
+
@@ -74,7 +102,19 @@
{
- const theme = editor.themes.find((t) => t.name === e.currentTarget.value);
+ const name = e.currentTarget.value;
+ if (
+ editor.dirty &&
+ !confirm(
+ `load "${name}" as the new base? ${
+ editor.overrides.size
+ } edited knob(s) will be discarded`,
+ )
+ ) {
+ e.currentTarget.value = editor.based_on;
+ return;
+ }
+ const theme = editor.themes.find((t) => t.name === name);
if (theme) editor.load_theme(theme);
}}
>
@@ -88,7 +128,7 @@
class="chip"
title={editor.is_palette_tier
? 'moves palette letter hues - exemplar tier, stays out of registries'
- : 'moves only role bindings and levers - safe for theme registries'}
+ : 'moves only intent bindings and levers - safe for theme registries'}
>{editor.is_palette_tier ? 'palette-tier' : 'semantic-tier'}
{#if editor.dirty}
@@ -109,37 +149,62 @@
-
-
- {#each quick_knobs as knob (knob.name)}
-
editor.set_value(knob.name, value, editing_scheme)}
- onreset={() => editor.reset(knob.name)}
- />
- {/each}
+
+
+
intents - what each meaning points at
+
+ {#each binding_knobs as knob (knob.name)}
+ {@render knob_control(knob, true)}
+ {/each}
+
+
+
+
levers - the highest-leverage knobs
+
+ {#each lever_knobs as knob (knob.name)}
+ {@render knob_control(knob, true)}
+ {/each}
+
- {#each axes as { axis, title } (axis)}
- {@const knobs = semantic_knobs(axis)}
- {#if knobs.length}
+ {#each knob_axes as { axis, title } (axis)}
+ {@const inline_knobs = semantic_knobs(axis).filter((k) => k.leverage !== 'sm')}
+ {@const sm_knobs = semantic_knobs(axis).filter((k) => k.leverage === 'sm')}
+ {#if inline_knobs.length || sm_knobs.length}
{title}
-
- {#each knobs as knob (knob.name)}
- editor.set_value(knob.name, value, editing_scheme)}
- onreset={() => editor.reset(knob.name)}
- />
- {/each}
-
+ {#if axis === 'color'}
+
+
+
+
+ {#each intent_variants as intent (intent)}
+
+ {/each}
+
+ {/if}
+ {#if inline_knobs.length}
+
+ {#each inline_knobs as knob (knob.name)}
+ {@render knob_control(knob, false)}
+ {/each}
+
+ {/if}
+ {#if sm_knobs.length}
+
+ {sm_details_titles[axis] ?? 'granular tokens'}
+ (escape hatches - pin individual tokens)
+
+ {#each sm_knobs as knob (knob.name)}
+ {@render knob_control(knob, false)}
+ {/each}
+
+
+ {/if}
{#if axis === 'color'}
(the letter slots - moving these makes the theme palette-tier)
+
+ {#each palette_variants as letter (letter)}
+
+ {/each}
+
{#each palette_knobs as knob (knob.name)}
- editor.set_value(knob.name, value, editing_scheme)}
- onreset={() => editor.reset(knob.name)}
- />
+ {@render knob_control(knob, false)}
{/each}
@@ -195,6 +259,11 @@
.knobs {
row-gap: var(--space_lg);
}
+ .ramp_strips {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: var(--space_sm) var(--space_lg);
+ }
.rendered {
position: relative; /* for the .copy button */
}
diff --git a/src/routes/docs/borders/+page.svelte b/src/routes/docs/borders/+page.svelte
index 1db01c5b3..fea96a2eb 100644
--- a/src/routes/docs/borders/+page.svelte
+++ b/src/routes/docs/borders/+page.svelte
@@ -190,7 +190,7 @@ border-color: var(--shade_30);
Each border utility class has a corresponding outline variant using the same border variables
- (like outline_palette_b_50, outline_width_4, and
+ (like outline_b_50, outline_width_4, and
outline-style:solid), and there are also two special outline variables:
diff --git a/src/routes/docs/classes/+page.svelte b/src/routes/docs/classes/+page.svelte
index 585747784..179557c46 100644
--- a/src/routes/docs/classes/+page.svelte
+++ b/src/routes/docs/classes/+page.svelte
@@ -11,7 +11,7 @@
import {
space_variants,
palette_variants,
- role_variants,
+ intent_variants,
intensity_variants,
shade_variants,
text_variants,
@@ -784,19 +784,27 @@ vite_plugin_fuz_css({
{#each role_variants as role (role)}.{role}_{@render variant_range(intensity_variants)}{#each intent_variants as intent (intent)}.{intent}_{@render variant_range(intensity_variants)}{/each}
{#each role_variants as role (role)}.bg_{role}_{@render variant_range(intensity_variants)}{#each intent_variants as intent (intent)}.bg_{intent}_{@render variant_range(intensity_variants)}{/each}
+
+ A bare scale class applies its family's dominant use — .palette_a_50,
+ .positive_50, and .text_70 set the text color while
+ .shade_50 sets the background — and the bg_ prefix selects the
+ background twin (.bg_a_50, .bg_positive_50). In compound families
+ a letter alone implies the palette: .border_a_50 is the palette family,
+ .border_color_50 the alpha ramp.
+
Color and text classes (.palette_a_50, .text_70, etc.) also set
--text_color, so nested elements like <code> that use
@@ -870,7 +878,7 @@ vite_plugin_fuz_css({
.border_palette_{@render variant_range(palette_variants)}_{@render variant_range(
+ >.border_{@render variant_range(palette_variants)}_{@render variant_range(
intensity_variants,
)}
@@ -927,7 +935,7 @@ vite_plugin_fuz_css({
.outline_palette_{@render variant_range(palette_variants)}_{@render variant_range(
+ >.outline_{@render variant_range(palette_variants)}_{@render variant_range(
intensity_variants,
)}
@@ -976,7 +984,7 @@ vite_plugin_fuz_css({
.shadow_palette_{@render variant_range(palette_variants)}_{@render variant_range(
+ >.shadow_{@render variant_range(palette_variants)}_{@render variant_range(
intensity_variants,
)}
diff --git a/src/routes/docs/colors/+page.svelte b/src/routes/docs/colors/+page.svelte
index 00a2547d5..93be8bc30 100644
--- a/src/routes/docs/colors/+page.svelte
+++ b/src/routes/docs/colors/+page.svelte
@@ -10,7 +10,7 @@
import HueSwatch from './HueSwatch.svelte';
import ColorSwatch from './ColorSwatch.svelte';
- import {palette_variants} from '$lib/variable_data.ts';
+ import {palette_variants, palette_glosses, type PaletteVariant} from '$lib/variable_data.ts';
const LIBRARY_ITEM_NAME = 'colors';
@@ -21,20 +21,11 @@
// TODO button to add an inline hue input for runtime modification of the theme
- // letter glosses: color name plus the default role binding where one exists
- // Note: This array must stay in sync with palette_variants (a-j = 10 elements)
- const descriptions = [
- 'blue · default accent',
- 'green · default positive',
- 'red · default negative',
- 'purple',
- 'yellow',
- 'brown · default neutral',
- 'pink',
- 'orange · default caution',
- 'cyan · default info',
- 'teal',
- ];
+ // letter glosses: color name plus the default intent binding where one exists
+ const describe_letter = (letter: PaletteVariant): string => {
+ const gloss = palette_glosses[letter];
+ return gloss.binding ? `${gloss.color} · default ${gloss.binding}` : gloss.color;
+ };
@@ -51,11 +42,11 @@
Hues use letters so themes can reassign colors without breaking semantics -- "a" is blue by
- default but could be any color. Meaning attaches through the role knobs layered on top:
+ default but could be any color. Meaning attaches through the intent knobs layered on top:
--hue_accent (links, focus, selection, selected states -- what other systems call
the "primary" color, named here for what it communicates) defaults to
--hue_a, --hue_negative to --hue_c, and so on. Retarget
- a role to move just that meaning; rotate a letter to move the palette.
+ an intent to move just that meaning; rotate a letter to move the palette.
@@ -67,7 +58,8 @@
--hue_neutral + --neutral_chroma -- the temperature and strength
- of every surface, text, border, and shadow tint
+ of every surface, text, border, and shadow tint (the neutral intent; its scales are the
+ shade and text ramps)
--chroma_scale -- one multiplier from grayscale (0) through calm (1) to vivid
@@ -78,7 +70,7 @@
warm-light/cool-shadow character (default 0)
- role hues -- --hue_accent, --hue_positive,
+ intent hues -- --hue_accent, --hue_positive,
--hue_negative, --hue_caution, --hue_info -- each
deriving a full 13-stop scale (--accent_00 … --accent_100) with
matching text and background classes (.positive_50,
@@ -125,8 +117,8 @@
Hue variables are the same in both light and dark modes (non-adaptive).
- {#each palette_variants as letter, i (letter)}
-
+ {#each palette_variants as letter (letter)}
+
{/each}
diff --git a/src/routes/docs/shadows/+page.svelte b/src/routes/docs/shadows/+page.svelte
index e55975a65..6d38a2821 100644
--- a/src/routes/docs/shadows/+page.svelte
+++ b/src/routes/docs/shadows/+page.svelte
@@ -88,9 +88,9 @@
- Use shadow_palette_{'{hue}'}_{'{intensity}'} classes to apply colored shadows.
- The intensity controls the color's prominence -- 60 is a fine starting point for visible
- colored shadows.
+ Use shadow_{'{letter}'}_{'{intensity}'} classes to apply colored shadows. The
+ intensity controls the color's prominence -- 60 is a fine starting point for visible colored
+ shadows.
{#each palette_variants as color_variant (color_variant)}
@@ -108,7 +108,7 @@
{@const is_hue =
color_variant && !['umbra', 'highlight', 'glow', 'shroud'].includes(color_variant)}
{@const shadow_color_name = is_hue
- ? `shadow_palette_${color_variant}_${intensity}`
+ ? `shadow_${color_variant}_${intensity}`
: color_variant
? `shadow_color_${color_variant}`
: 'shadow_color_umbra'}
@@ -162,8 +162,8 @@
{/snippet}
{#snippet intensity_selector(color_variant: PaletteVariant)}
- shadow_palette_{color_variant}_{selected_intensity} shadow_{color_variant}_{selected_intensity}
diff --git a/src/test/fixtures/css_classes_fixture.json b/src/test/fixtures/css_classes_fixture.json
index e6cf00147..ecf038180 100644
--- a/src/test/fixtures/css_classes_fixture.json
+++ b/src/test/fixtures/css_classes_fixture.json
@@ -1647,394 +1647,394 @@
"border_color_100": {
"declaration": "border-color: var(--border_color_100); --border_color: var(--border_color_100);"
},
- "border_palette_a_00": {
+ "border_a_00": {
"declaration": "border-color: var(--palette_a_00); --border_color: var(--palette_a_00);"
},
- "border_palette_a_05": {
+ "border_a_05": {
"declaration": "border-color: var(--palette_a_05); --border_color: var(--palette_a_05);"
},
- "border_palette_a_10": {
+ "border_a_10": {
"declaration": "border-color: var(--palette_a_10); --border_color: var(--palette_a_10);"
},
- "border_palette_a_20": {
+ "border_a_20": {
"declaration": "border-color: var(--palette_a_20); --border_color: var(--palette_a_20);"
},
- "border_palette_a_30": {
+ "border_a_30": {
"declaration": "border-color: var(--palette_a_30); --border_color: var(--palette_a_30);"
},
- "border_palette_a_40": {
+ "border_a_40": {
"declaration": "border-color: var(--palette_a_40); --border_color: var(--palette_a_40);"
},
- "border_palette_a_50": {
+ "border_a_50": {
"declaration": "border-color: var(--palette_a_50); --border_color: var(--palette_a_50);"
},
- "border_palette_a_60": {
+ "border_a_60": {
"declaration": "border-color: var(--palette_a_60); --border_color: var(--palette_a_60);"
},
- "border_palette_a_70": {
+ "border_a_70": {
"declaration": "border-color: var(--palette_a_70); --border_color: var(--palette_a_70);"
},
- "border_palette_a_80": {
+ "border_a_80": {
"declaration": "border-color: var(--palette_a_80); --border_color: var(--palette_a_80);"
},
- "border_palette_a_90": {
+ "border_a_90": {
"declaration": "border-color: var(--palette_a_90); --border_color: var(--palette_a_90);"
},
- "border_palette_a_95": {
+ "border_a_95": {
"declaration": "border-color: var(--palette_a_95); --border_color: var(--palette_a_95);"
},
- "border_palette_a_100": {
+ "border_a_100": {
"declaration": "border-color: var(--palette_a_100); --border_color: var(--palette_a_100);"
},
- "border_palette_b_00": {
+ "border_b_00": {
"declaration": "border-color: var(--palette_b_00); --border_color: var(--palette_b_00);"
},
- "border_palette_b_05": {
+ "border_b_05": {
"declaration": "border-color: var(--palette_b_05); --border_color: var(--palette_b_05);"
},
- "border_palette_b_10": {
+ "border_b_10": {
"declaration": "border-color: var(--palette_b_10); --border_color: var(--palette_b_10);"
},
- "border_palette_b_20": {
+ "border_b_20": {
"declaration": "border-color: var(--palette_b_20); --border_color: var(--palette_b_20);"
},
- "border_palette_b_30": {
+ "border_b_30": {
"declaration": "border-color: var(--palette_b_30); --border_color: var(--palette_b_30);"
},
- "border_palette_b_40": {
+ "border_b_40": {
"declaration": "border-color: var(--palette_b_40); --border_color: var(--palette_b_40);"
},
- "border_palette_b_50": {
+ "border_b_50": {
"declaration": "border-color: var(--palette_b_50); --border_color: var(--palette_b_50);"
},
- "border_palette_b_60": {
+ "border_b_60": {
"declaration": "border-color: var(--palette_b_60); --border_color: var(--palette_b_60);"
},
- "border_palette_b_70": {
+ "border_b_70": {
"declaration": "border-color: var(--palette_b_70); --border_color: var(--palette_b_70);"
},
- "border_palette_b_80": {
+ "border_b_80": {
"declaration": "border-color: var(--palette_b_80); --border_color: var(--palette_b_80);"
},
- "border_palette_b_90": {
+ "border_b_90": {
"declaration": "border-color: var(--palette_b_90); --border_color: var(--palette_b_90);"
},
- "border_palette_b_95": {
+ "border_b_95": {
"declaration": "border-color: var(--palette_b_95); --border_color: var(--palette_b_95);"
},
- "border_palette_b_100": {
+ "border_b_100": {
"declaration": "border-color: var(--palette_b_100); --border_color: var(--palette_b_100);"
},
- "border_palette_c_00": {
+ "border_c_00": {
"declaration": "border-color: var(--palette_c_00); --border_color: var(--palette_c_00);"
},
- "border_palette_c_05": {
+ "border_c_05": {
"declaration": "border-color: var(--palette_c_05); --border_color: var(--palette_c_05);"
},
- "border_palette_c_10": {
+ "border_c_10": {
"declaration": "border-color: var(--palette_c_10); --border_color: var(--palette_c_10);"
},
- "border_palette_c_20": {
+ "border_c_20": {
"declaration": "border-color: var(--palette_c_20); --border_color: var(--palette_c_20);"
},
- "border_palette_c_30": {
+ "border_c_30": {
"declaration": "border-color: var(--palette_c_30); --border_color: var(--palette_c_30);"
},
- "border_palette_c_40": {
+ "border_c_40": {
"declaration": "border-color: var(--palette_c_40); --border_color: var(--palette_c_40);"
},
- "border_palette_c_50": {
+ "border_c_50": {
"declaration": "border-color: var(--palette_c_50); --border_color: var(--palette_c_50);"
},
- "border_palette_c_60": {
+ "border_c_60": {
"declaration": "border-color: var(--palette_c_60); --border_color: var(--palette_c_60);"
},
- "border_palette_c_70": {
+ "border_c_70": {
"declaration": "border-color: var(--palette_c_70); --border_color: var(--palette_c_70);"
},
- "border_palette_c_80": {
+ "border_c_80": {
"declaration": "border-color: var(--palette_c_80); --border_color: var(--palette_c_80);"
},
- "border_palette_c_90": {
+ "border_c_90": {
"declaration": "border-color: var(--palette_c_90); --border_color: var(--palette_c_90);"
},
- "border_palette_c_95": {
+ "border_c_95": {
"declaration": "border-color: var(--palette_c_95); --border_color: var(--palette_c_95);"
},
- "border_palette_c_100": {
+ "border_c_100": {
"declaration": "border-color: var(--palette_c_100); --border_color: var(--palette_c_100);"
},
- "border_palette_d_00": {
+ "border_d_00": {
"declaration": "border-color: var(--palette_d_00); --border_color: var(--palette_d_00);"
},
- "border_palette_d_05": {
+ "border_d_05": {
"declaration": "border-color: var(--palette_d_05); --border_color: var(--palette_d_05);"
},
- "border_palette_d_10": {
+ "border_d_10": {
"declaration": "border-color: var(--palette_d_10); --border_color: var(--palette_d_10);"
},
- "border_palette_d_20": {
+ "border_d_20": {
"declaration": "border-color: var(--palette_d_20); --border_color: var(--palette_d_20);"
},
- "border_palette_d_30": {
+ "border_d_30": {
"declaration": "border-color: var(--palette_d_30); --border_color: var(--palette_d_30);"
},
- "border_palette_d_40": {
+ "border_d_40": {
"declaration": "border-color: var(--palette_d_40); --border_color: var(--palette_d_40);"
},
- "border_palette_d_50": {
+ "border_d_50": {
"declaration": "border-color: var(--palette_d_50); --border_color: var(--palette_d_50);"
},
- "border_palette_d_60": {
+ "border_d_60": {
"declaration": "border-color: var(--palette_d_60); --border_color: var(--palette_d_60);"
},
- "border_palette_d_70": {
+ "border_d_70": {
"declaration": "border-color: var(--palette_d_70); --border_color: var(--palette_d_70);"
},
- "border_palette_d_80": {
+ "border_d_80": {
"declaration": "border-color: var(--palette_d_80); --border_color: var(--palette_d_80);"
},
- "border_palette_d_90": {
+ "border_d_90": {
"declaration": "border-color: var(--palette_d_90); --border_color: var(--palette_d_90);"
},
- "border_palette_d_95": {
+ "border_d_95": {
"declaration": "border-color: var(--palette_d_95); --border_color: var(--palette_d_95);"
},
- "border_palette_d_100": {
+ "border_d_100": {
"declaration": "border-color: var(--palette_d_100); --border_color: var(--palette_d_100);"
},
- "border_palette_e_00": {
+ "border_e_00": {
"declaration": "border-color: var(--palette_e_00); --border_color: var(--palette_e_00);"
},
- "border_palette_e_05": {
+ "border_e_05": {
"declaration": "border-color: var(--palette_e_05); --border_color: var(--palette_e_05);"
},
- "border_palette_e_10": {
+ "border_e_10": {
"declaration": "border-color: var(--palette_e_10); --border_color: var(--palette_e_10);"
},
- "border_palette_e_20": {
+ "border_e_20": {
"declaration": "border-color: var(--palette_e_20); --border_color: var(--palette_e_20);"
},
- "border_palette_e_30": {
+ "border_e_30": {
"declaration": "border-color: var(--palette_e_30); --border_color: var(--palette_e_30);"
},
- "border_palette_e_40": {
+ "border_e_40": {
"declaration": "border-color: var(--palette_e_40); --border_color: var(--palette_e_40);"
},
- "border_palette_e_50": {
+ "border_e_50": {
"declaration": "border-color: var(--palette_e_50); --border_color: var(--palette_e_50);"
},
- "border_palette_e_60": {
+ "border_e_60": {
"declaration": "border-color: var(--palette_e_60); --border_color: var(--palette_e_60);"
},
- "border_palette_e_70": {
+ "border_e_70": {
"declaration": "border-color: var(--palette_e_70); --border_color: var(--palette_e_70);"
},
- "border_palette_e_80": {
+ "border_e_80": {
"declaration": "border-color: var(--palette_e_80); --border_color: var(--palette_e_80);"
},
- "border_palette_e_90": {
+ "border_e_90": {
"declaration": "border-color: var(--palette_e_90); --border_color: var(--palette_e_90);"
},
- "border_palette_e_95": {
+ "border_e_95": {
"declaration": "border-color: var(--palette_e_95); --border_color: var(--palette_e_95);"
},
- "border_palette_e_100": {
+ "border_e_100": {
"declaration": "border-color: var(--palette_e_100); --border_color: var(--palette_e_100);"
},
- "border_palette_f_00": {
+ "border_f_00": {
"declaration": "border-color: var(--palette_f_00); --border_color: var(--palette_f_00);"
},
- "border_palette_f_05": {
+ "border_f_05": {
"declaration": "border-color: var(--palette_f_05); --border_color: var(--palette_f_05);"
},
- "border_palette_f_10": {
+ "border_f_10": {
"declaration": "border-color: var(--palette_f_10); --border_color: var(--palette_f_10);"
},
- "border_palette_f_20": {
+ "border_f_20": {
"declaration": "border-color: var(--palette_f_20); --border_color: var(--palette_f_20);"
},
- "border_palette_f_30": {
+ "border_f_30": {
"declaration": "border-color: var(--palette_f_30); --border_color: var(--palette_f_30);"
},
- "border_palette_f_40": {
+ "border_f_40": {
"declaration": "border-color: var(--palette_f_40); --border_color: var(--palette_f_40);"
},
- "border_palette_f_50": {
+ "border_f_50": {
"declaration": "border-color: var(--palette_f_50); --border_color: var(--palette_f_50);"
},
- "border_palette_f_60": {
+ "border_f_60": {
"declaration": "border-color: var(--palette_f_60); --border_color: var(--palette_f_60);"
},
- "border_palette_f_70": {
+ "border_f_70": {
"declaration": "border-color: var(--palette_f_70); --border_color: var(--palette_f_70);"
},
- "border_palette_f_80": {
+ "border_f_80": {
"declaration": "border-color: var(--palette_f_80); --border_color: var(--palette_f_80);"
},
- "border_palette_f_90": {
+ "border_f_90": {
"declaration": "border-color: var(--palette_f_90); --border_color: var(--palette_f_90);"
},
- "border_palette_f_95": {
+ "border_f_95": {
"declaration": "border-color: var(--palette_f_95); --border_color: var(--palette_f_95);"
},
- "border_palette_f_100": {
+ "border_f_100": {
"declaration": "border-color: var(--palette_f_100); --border_color: var(--palette_f_100);"
},
- "border_palette_g_00": {
+ "border_g_00": {
"declaration": "border-color: var(--palette_g_00); --border_color: var(--palette_g_00);"
},
- "border_palette_g_05": {
+ "border_g_05": {
"declaration": "border-color: var(--palette_g_05); --border_color: var(--palette_g_05);"
},
- "border_palette_g_10": {
+ "border_g_10": {
"declaration": "border-color: var(--palette_g_10); --border_color: var(--palette_g_10);"
},
- "border_palette_g_20": {
+ "border_g_20": {
"declaration": "border-color: var(--palette_g_20); --border_color: var(--palette_g_20);"
},
- "border_palette_g_30": {
+ "border_g_30": {
"declaration": "border-color: var(--palette_g_30); --border_color: var(--palette_g_30);"
},
- "border_palette_g_40": {
+ "border_g_40": {
"declaration": "border-color: var(--palette_g_40); --border_color: var(--palette_g_40);"
},
- "border_palette_g_50": {
+ "border_g_50": {
"declaration": "border-color: var(--palette_g_50); --border_color: var(--palette_g_50);"
},
- "border_palette_g_60": {
+ "border_g_60": {
"declaration": "border-color: var(--palette_g_60); --border_color: var(--palette_g_60);"
},
- "border_palette_g_70": {
+ "border_g_70": {
"declaration": "border-color: var(--palette_g_70); --border_color: var(--palette_g_70);"
},
- "border_palette_g_80": {
+ "border_g_80": {
"declaration": "border-color: var(--palette_g_80); --border_color: var(--palette_g_80);"
},
- "border_palette_g_90": {
+ "border_g_90": {
"declaration": "border-color: var(--palette_g_90); --border_color: var(--palette_g_90);"
},
- "border_palette_g_95": {
+ "border_g_95": {
"declaration": "border-color: var(--palette_g_95); --border_color: var(--palette_g_95);"
},
- "border_palette_g_100": {
+ "border_g_100": {
"declaration": "border-color: var(--palette_g_100); --border_color: var(--palette_g_100);"
},
- "border_palette_h_00": {
+ "border_h_00": {
"declaration": "border-color: var(--palette_h_00); --border_color: var(--palette_h_00);"
},
- "border_palette_h_05": {
+ "border_h_05": {
"declaration": "border-color: var(--palette_h_05); --border_color: var(--palette_h_05);"
},
- "border_palette_h_10": {
+ "border_h_10": {
"declaration": "border-color: var(--palette_h_10); --border_color: var(--palette_h_10);"
},
- "border_palette_h_20": {
+ "border_h_20": {
"declaration": "border-color: var(--palette_h_20); --border_color: var(--palette_h_20);"
},
- "border_palette_h_30": {
+ "border_h_30": {
"declaration": "border-color: var(--palette_h_30); --border_color: var(--palette_h_30);"
},
- "border_palette_h_40": {
+ "border_h_40": {
"declaration": "border-color: var(--palette_h_40); --border_color: var(--palette_h_40);"
},
- "border_palette_h_50": {
+ "border_h_50": {
"declaration": "border-color: var(--palette_h_50); --border_color: var(--palette_h_50);"
},
- "border_palette_h_60": {
+ "border_h_60": {
"declaration": "border-color: var(--palette_h_60); --border_color: var(--palette_h_60);"
},
- "border_palette_h_70": {
+ "border_h_70": {
"declaration": "border-color: var(--palette_h_70); --border_color: var(--palette_h_70);"
},
- "border_palette_h_80": {
+ "border_h_80": {
"declaration": "border-color: var(--palette_h_80); --border_color: var(--palette_h_80);"
},
- "border_palette_h_90": {
+ "border_h_90": {
"declaration": "border-color: var(--palette_h_90); --border_color: var(--palette_h_90);"
},
- "border_palette_h_95": {
+ "border_h_95": {
"declaration": "border-color: var(--palette_h_95); --border_color: var(--palette_h_95);"
},
- "border_palette_h_100": {
+ "border_h_100": {
"declaration": "border-color: var(--palette_h_100); --border_color: var(--palette_h_100);"
},
- "border_palette_i_00": {
+ "border_i_00": {
"declaration": "border-color: var(--palette_i_00); --border_color: var(--palette_i_00);"
},
- "border_palette_i_05": {
+ "border_i_05": {
"declaration": "border-color: var(--palette_i_05); --border_color: var(--palette_i_05);"
},
- "border_palette_i_10": {
+ "border_i_10": {
"declaration": "border-color: var(--palette_i_10); --border_color: var(--palette_i_10);"
},
- "border_palette_i_20": {
+ "border_i_20": {
"declaration": "border-color: var(--palette_i_20); --border_color: var(--palette_i_20);"
},
- "border_palette_i_30": {
+ "border_i_30": {
"declaration": "border-color: var(--palette_i_30); --border_color: var(--palette_i_30);"
},
- "border_palette_i_40": {
+ "border_i_40": {
"declaration": "border-color: var(--palette_i_40); --border_color: var(--palette_i_40);"
},
- "border_palette_i_50": {
+ "border_i_50": {
"declaration": "border-color: var(--palette_i_50); --border_color: var(--palette_i_50);"
},
- "border_palette_i_60": {
+ "border_i_60": {
"declaration": "border-color: var(--palette_i_60); --border_color: var(--palette_i_60);"
},
- "border_palette_i_70": {
+ "border_i_70": {
"declaration": "border-color: var(--palette_i_70); --border_color: var(--palette_i_70);"
},
- "border_palette_i_80": {
+ "border_i_80": {
"declaration": "border-color: var(--palette_i_80); --border_color: var(--palette_i_80);"
},
- "border_palette_i_90": {
+ "border_i_90": {
"declaration": "border-color: var(--palette_i_90); --border_color: var(--palette_i_90);"
},
- "border_palette_i_95": {
+ "border_i_95": {
"declaration": "border-color: var(--palette_i_95); --border_color: var(--palette_i_95);"
},
- "border_palette_i_100": {
+ "border_i_100": {
"declaration": "border-color: var(--palette_i_100); --border_color: var(--palette_i_100);"
},
- "border_palette_j_00": {
+ "border_j_00": {
"declaration": "border-color: var(--palette_j_00); --border_color: var(--palette_j_00);"
},
- "border_palette_j_05": {
+ "border_j_05": {
"declaration": "border-color: var(--palette_j_05); --border_color: var(--palette_j_05);"
},
- "border_palette_j_10": {
+ "border_j_10": {
"declaration": "border-color: var(--palette_j_10); --border_color: var(--palette_j_10);"
},
- "border_palette_j_20": {
+ "border_j_20": {
"declaration": "border-color: var(--palette_j_20); --border_color: var(--palette_j_20);"
},
- "border_palette_j_30": {
+ "border_j_30": {
"declaration": "border-color: var(--palette_j_30); --border_color: var(--palette_j_30);"
},
- "border_palette_j_40": {
+ "border_j_40": {
"declaration": "border-color: var(--palette_j_40); --border_color: var(--palette_j_40);"
},
- "border_palette_j_50": {
+ "border_j_50": {
"declaration": "border-color: var(--palette_j_50); --border_color: var(--palette_j_50);"
},
- "border_palette_j_60": {
+ "border_j_60": {
"declaration": "border-color: var(--palette_j_60); --border_color: var(--palette_j_60);"
},
- "border_palette_j_70": {
+ "border_j_70": {
"declaration": "border-color: var(--palette_j_70); --border_color: var(--palette_j_70);"
},
- "border_palette_j_80": {
+ "border_j_80": {
"declaration": "border-color: var(--palette_j_80); --border_color: var(--palette_j_80);"
},
- "border_palette_j_90": {
+ "border_j_90": {
"declaration": "border-color: var(--palette_j_90); --border_color: var(--palette_j_90);"
},
- "border_palette_j_95": {
+ "border_j_95": {
"declaration": "border-color: var(--palette_j_95); --border_color: var(--palette_j_95);"
},
- "border_palette_j_100": {
+ "border_j_100": {
"declaration": "border-color: var(--palette_j_100); --border_color: var(--palette_j_100);"
},
"outline_color_00": {
@@ -2076,394 +2076,394 @@
"outline_color_100": {
"declaration": "outline-color: var(--shade_100);"
},
- "outline_palette_a_00": {
+ "outline_a_00": {
"declaration": "outline-color: var(--palette_a_00); --outline_color: var(--palette_a_00);"
},
- "outline_palette_a_05": {
+ "outline_a_05": {
"declaration": "outline-color: var(--palette_a_05); --outline_color: var(--palette_a_05);"
},
- "outline_palette_a_10": {
+ "outline_a_10": {
"declaration": "outline-color: var(--palette_a_10); --outline_color: var(--palette_a_10);"
},
- "outline_palette_a_20": {
+ "outline_a_20": {
"declaration": "outline-color: var(--palette_a_20); --outline_color: var(--palette_a_20);"
},
- "outline_palette_a_30": {
+ "outline_a_30": {
"declaration": "outline-color: var(--palette_a_30); --outline_color: var(--palette_a_30);"
},
- "outline_palette_a_40": {
+ "outline_a_40": {
"declaration": "outline-color: var(--palette_a_40); --outline_color: var(--palette_a_40);"
},
- "outline_palette_a_50": {
+ "outline_a_50": {
"declaration": "outline-color: var(--palette_a_50); --outline_color: var(--palette_a_50);"
},
- "outline_palette_a_60": {
+ "outline_a_60": {
"declaration": "outline-color: var(--palette_a_60); --outline_color: var(--palette_a_60);"
},
- "outline_palette_a_70": {
+ "outline_a_70": {
"declaration": "outline-color: var(--palette_a_70); --outline_color: var(--palette_a_70);"
},
- "outline_palette_a_80": {
+ "outline_a_80": {
"declaration": "outline-color: var(--palette_a_80); --outline_color: var(--palette_a_80);"
},
- "outline_palette_a_90": {
+ "outline_a_90": {
"declaration": "outline-color: var(--palette_a_90); --outline_color: var(--palette_a_90);"
},
- "outline_palette_a_95": {
+ "outline_a_95": {
"declaration": "outline-color: var(--palette_a_95); --outline_color: var(--palette_a_95);"
},
- "outline_palette_a_100": {
+ "outline_a_100": {
"declaration": "outline-color: var(--palette_a_100); --outline_color: var(--palette_a_100);"
},
- "outline_palette_b_00": {
+ "outline_b_00": {
"declaration": "outline-color: var(--palette_b_00); --outline_color: var(--palette_b_00);"
},
- "outline_palette_b_05": {
+ "outline_b_05": {
"declaration": "outline-color: var(--palette_b_05); --outline_color: var(--palette_b_05);"
},
- "outline_palette_b_10": {
+ "outline_b_10": {
"declaration": "outline-color: var(--palette_b_10); --outline_color: var(--palette_b_10);"
},
- "outline_palette_b_20": {
+ "outline_b_20": {
"declaration": "outline-color: var(--palette_b_20); --outline_color: var(--palette_b_20);"
},
- "outline_palette_b_30": {
+ "outline_b_30": {
"declaration": "outline-color: var(--palette_b_30); --outline_color: var(--palette_b_30);"
},
- "outline_palette_b_40": {
+ "outline_b_40": {
"declaration": "outline-color: var(--palette_b_40); --outline_color: var(--palette_b_40);"
},
- "outline_palette_b_50": {
+ "outline_b_50": {
"declaration": "outline-color: var(--palette_b_50); --outline_color: var(--palette_b_50);"
},
- "outline_palette_b_60": {
+ "outline_b_60": {
"declaration": "outline-color: var(--palette_b_60); --outline_color: var(--palette_b_60);"
},
- "outline_palette_b_70": {
+ "outline_b_70": {
"declaration": "outline-color: var(--palette_b_70); --outline_color: var(--palette_b_70);"
},
- "outline_palette_b_80": {
+ "outline_b_80": {
"declaration": "outline-color: var(--palette_b_80); --outline_color: var(--palette_b_80);"
},
- "outline_palette_b_90": {
+ "outline_b_90": {
"declaration": "outline-color: var(--palette_b_90); --outline_color: var(--palette_b_90);"
},
- "outline_palette_b_95": {
+ "outline_b_95": {
"declaration": "outline-color: var(--palette_b_95); --outline_color: var(--palette_b_95);"
},
- "outline_palette_b_100": {
+ "outline_b_100": {
"declaration": "outline-color: var(--palette_b_100); --outline_color: var(--palette_b_100);"
},
- "outline_palette_c_00": {
+ "outline_c_00": {
"declaration": "outline-color: var(--palette_c_00); --outline_color: var(--palette_c_00);"
},
- "outline_palette_c_05": {
+ "outline_c_05": {
"declaration": "outline-color: var(--palette_c_05); --outline_color: var(--palette_c_05);"
},
- "outline_palette_c_10": {
+ "outline_c_10": {
"declaration": "outline-color: var(--palette_c_10); --outline_color: var(--palette_c_10);"
},
- "outline_palette_c_20": {
+ "outline_c_20": {
"declaration": "outline-color: var(--palette_c_20); --outline_color: var(--palette_c_20);"
},
- "outline_palette_c_30": {
+ "outline_c_30": {
"declaration": "outline-color: var(--palette_c_30); --outline_color: var(--palette_c_30);"
},
- "outline_palette_c_40": {
+ "outline_c_40": {
"declaration": "outline-color: var(--palette_c_40); --outline_color: var(--palette_c_40);"
},
- "outline_palette_c_50": {
+ "outline_c_50": {
"declaration": "outline-color: var(--palette_c_50); --outline_color: var(--palette_c_50);"
},
- "outline_palette_c_60": {
+ "outline_c_60": {
"declaration": "outline-color: var(--palette_c_60); --outline_color: var(--palette_c_60);"
},
- "outline_palette_c_70": {
+ "outline_c_70": {
"declaration": "outline-color: var(--palette_c_70); --outline_color: var(--palette_c_70);"
},
- "outline_palette_c_80": {
+ "outline_c_80": {
"declaration": "outline-color: var(--palette_c_80); --outline_color: var(--palette_c_80);"
},
- "outline_palette_c_90": {
+ "outline_c_90": {
"declaration": "outline-color: var(--palette_c_90); --outline_color: var(--palette_c_90);"
},
- "outline_palette_c_95": {
+ "outline_c_95": {
"declaration": "outline-color: var(--palette_c_95); --outline_color: var(--palette_c_95);"
},
- "outline_palette_c_100": {
+ "outline_c_100": {
"declaration": "outline-color: var(--palette_c_100); --outline_color: var(--palette_c_100);"
},
- "outline_palette_d_00": {
+ "outline_d_00": {
"declaration": "outline-color: var(--palette_d_00); --outline_color: var(--palette_d_00);"
},
- "outline_palette_d_05": {
+ "outline_d_05": {
"declaration": "outline-color: var(--palette_d_05); --outline_color: var(--palette_d_05);"
},
- "outline_palette_d_10": {
+ "outline_d_10": {
"declaration": "outline-color: var(--palette_d_10); --outline_color: var(--palette_d_10);"
},
- "outline_palette_d_20": {
+ "outline_d_20": {
"declaration": "outline-color: var(--palette_d_20); --outline_color: var(--palette_d_20);"
},
- "outline_palette_d_30": {
+ "outline_d_30": {
"declaration": "outline-color: var(--palette_d_30); --outline_color: var(--palette_d_30);"
},
- "outline_palette_d_40": {
+ "outline_d_40": {
"declaration": "outline-color: var(--palette_d_40); --outline_color: var(--palette_d_40);"
},
- "outline_palette_d_50": {
+ "outline_d_50": {
"declaration": "outline-color: var(--palette_d_50); --outline_color: var(--palette_d_50);"
},
- "outline_palette_d_60": {
+ "outline_d_60": {
"declaration": "outline-color: var(--palette_d_60); --outline_color: var(--palette_d_60);"
},
- "outline_palette_d_70": {
+ "outline_d_70": {
"declaration": "outline-color: var(--palette_d_70); --outline_color: var(--palette_d_70);"
},
- "outline_palette_d_80": {
+ "outline_d_80": {
"declaration": "outline-color: var(--palette_d_80); --outline_color: var(--palette_d_80);"
},
- "outline_palette_d_90": {
+ "outline_d_90": {
"declaration": "outline-color: var(--palette_d_90); --outline_color: var(--palette_d_90);"
},
- "outline_palette_d_95": {
+ "outline_d_95": {
"declaration": "outline-color: var(--palette_d_95); --outline_color: var(--palette_d_95);"
},
- "outline_palette_d_100": {
+ "outline_d_100": {
"declaration": "outline-color: var(--palette_d_100); --outline_color: var(--palette_d_100);"
},
- "outline_palette_e_00": {
+ "outline_e_00": {
"declaration": "outline-color: var(--palette_e_00); --outline_color: var(--palette_e_00);"
},
- "outline_palette_e_05": {
+ "outline_e_05": {
"declaration": "outline-color: var(--palette_e_05); --outline_color: var(--palette_e_05);"
},
- "outline_palette_e_10": {
+ "outline_e_10": {
"declaration": "outline-color: var(--palette_e_10); --outline_color: var(--palette_e_10);"
},
- "outline_palette_e_20": {
+ "outline_e_20": {
"declaration": "outline-color: var(--palette_e_20); --outline_color: var(--palette_e_20);"
},
- "outline_palette_e_30": {
+ "outline_e_30": {
"declaration": "outline-color: var(--palette_e_30); --outline_color: var(--palette_e_30);"
},
- "outline_palette_e_40": {
+ "outline_e_40": {
"declaration": "outline-color: var(--palette_e_40); --outline_color: var(--palette_e_40);"
},
- "outline_palette_e_50": {
+ "outline_e_50": {
"declaration": "outline-color: var(--palette_e_50); --outline_color: var(--palette_e_50);"
},
- "outline_palette_e_60": {
+ "outline_e_60": {
"declaration": "outline-color: var(--palette_e_60); --outline_color: var(--palette_e_60);"
},
- "outline_palette_e_70": {
+ "outline_e_70": {
"declaration": "outline-color: var(--palette_e_70); --outline_color: var(--palette_e_70);"
},
- "outline_palette_e_80": {
+ "outline_e_80": {
"declaration": "outline-color: var(--palette_e_80); --outline_color: var(--palette_e_80);"
},
- "outline_palette_e_90": {
+ "outline_e_90": {
"declaration": "outline-color: var(--palette_e_90); --outline_color: var(--palette_e_90);"
},
- "outline_palette_e_95": {
+ "outline_e_95": {
"declaration": "outline-color: var(--palette_e_95); --outline_color: var(--palette_e_95);"
},
- "outline_palette_e_100": {
+ "outline_e_100": {
"declaration": "outline-color: var(--palette_e_100); --outline_color: var(--palette_e_100);"
},
- "outline_palette_f_00": {
+ "outline_f_00": {
"declaration": "outline-color: var(--palette_f_00); --outline_color: var(--palette_f_00);"
},
- "outline_palette_f_05": {
+ "outline_f_05": {
"declaration": "outline-color: var(--palette_f_05); --outline_color: var(--palette_f_05);"
},
- "outline_palette_f_10": {
+ "outline_f_10": {
"declaration": "outline-color: var(--palette_f_10); --outline_color: var(--palette_f_10);"
},
- "outline_palette_f_20": {
+ "outline_f_20": {
"declaration": "outline-color: var(--palette_f_20); --outline_color: var(--palette_f_20);"
},
- "outline_palette_f_30": {
+ "outline_f_30": {
"declaration": "outline-color: var(--palette_f_30); --outline_color: var(--palette_f_30);"
},
- "outline_palette_f_40": {
+ "outline_f_40": {
"declaration": "outline-color: var(--palette_f_40); --outline_color: var(--palette_f_40);"
},
- "outline_palette_f_50": {
+ "outline_f_50": {
"declaration": "outline-color: var(--palette_f_50); --outline_color: var(--palette_f_50);"
},
- "outline_palette_f_60": {
+ "outline_f_60": {
"declaration": "outline-color: var(--palette_f_60); --outline_color: var(--palette_f_60);"
},
- "outline_palette_f_70": {
+ "outline_f_70": {
"declaration": "outline-color: var(--palette_f_70); --outline_color: var(--palette_f_70);"
},
- "outline_palette_f_80": {
+ "outline_f_80": {
"declaration": "outline-color: var(--palette_f_80); --outline_color: var(--palette_f_80);"
},
- "outline_palette_f_90": {
+ "outline_f_90": {
"declaration": "outline-color: var(--palette_f_90); --outline_color: var(--palette_f_90);"
},
- "outline_palette_f_95": {
+ "outline_f_95": {
"declaration": "outline-color: var(--palette_f_95); --outline_color: var(--palette_f_95);"
},
- "outline_palette_f_100": {
+ "outline_f_100": {
"declaration": "outline-color: var(--palette_f_100); --outline_color: var(--palette_f_100);"
},
- "outline_palette_g_00": {
+ "outline_g_00": {
"declaration": "outline-color: var(--palette_g_00); --outline_color: var(--palette_g_00);"
},
- "outline_palette_g_05": {
+ "outline_g_05": {
"declaration": "outline-color: var(--palette_g_05); --outline_color: var(--palette_g_05);"
},
- "outline_palette_g_10": {
+ "outline_g_10": {
"declaration": "outline-color: var(--palette_g_10); --outline_color: var(--palette_g_10);"
},
- "outline_palette_g_20": {
+ "outline_g_20": {
"declaration": "outline-color: var(--palette_g_20); --outline_color: var(--palette_g_20);"
},
- "outline_palette_g_30": {
+ "outline_g_30": {
"declaration": "outline-color: var(--palette_g_30); --outline_color: var(--palette_g_30);"
},
- "outline_palette_g_40": {
+ "outline_g_40": {
"declaration": "outline-color: var(--palette_g_40); --outline_color: var(--palette_g_40);"
},
- "outline_palette_g_50": {
+ "outline_g_50": {
"declaration": "outline-color: var(--palette_g_50); --outline_color: var(--palette_g_50);"
},
- "outline_palette_g_60": {
+ "outline_g_60": {
"declaration": "outline-color: var(--palette_g_60); --outline_color: var(--palette_g_60);"
},
- "outline_palette_g_70": {
+ "outline_g_70": {
"declaration": "outline-color: var(--palette_g_70); --outline_color: var(--palette_g_70);"
},
- "outline_palette_g_80": {
+ "outline_g_80": {
"declaration": "outline-color: var(--palette_g_80); --outline_color: var(--palette_g_80);"
},
- "outline_palette_g_90": {
+ "outline_g_90": {
"declaration": "outline-color: var(--palette_g_90); --outline_color: var(--palette_g_90);"
},
- "outline_palette_g_95": {
+ "outline_g_95": {
"declaration": "outline-color: var(--palette_g_95); --outline_color: var(--palette_g_95);"
},
- "outline_palette_g_100": {
+ "outline_g_100": {
"declaration": "outline-color: var(--palette_g_100); --outline_color: var(--palette_g_100);"
},
- "outline_palette_h_00": {
+ "outline_h_00": {
"declaration": "outline-color: var(--palette_h_00); --outline_color: var(--palette_h_00);"
},
- "outline_palette_h_05": {
+ "outline_h_05": {
"declaration": "outline-color: var(--palette_h_05); --outline_color: var(--palette_h_05);"
},
- "outline_palette_h_10": {
+ "outline_h_10": {
"declaration": "outline-color: var(--palette_h_10); --outline_color: var(--palette_h_10);"
},
- "outline_palette_h_20": {
+ "outline_h_20": {
"declaration": "outline-color: var(--palette_h_20); --outline_color: var(--palette_h_20);"
},
- "outline_palette_h_30": {
+ "outline_h_30": {
"declaration": "outline-color: var(--palette_h_30); --outline_color: var(--palette_h_30);"
},
- "outline_palette_h_40": {
+ "outline_h_40": {
"declaration": "outline-color: var(--palette_h_40); --outline_color: var(--palette_h_40);"
},
- "outline_palette_h_50": {
+ "outline_h_50": {
"declaration": "outline-color: var(--palette_h_50); --outline_color: var(--palette_h_50);"
},
- "outline_palette_h_60": {
+ "outline_h_60": {
"declaration": "outline-color: var(--palette_h_60); --outline_color: var(--palette_h_60);"
},
- "outline_palette_h_70": {
+ "outline_h_70": {
"declaration": "outline-color: var(--palette_h_70); --outline_color: var(--palette_h_70);"
},
- "outline_palette_h_80": {
+ "outline_h_80": {
"declaration": "outline-color: var(--palette_h_80); --outline_color: var(--palette_h_80);"
},
- "outline_palette_h_90": {
+ "outline_h_90": {
"declaration": "outline-color: var(--palette_h_90); --outline_color: var(--palette_h_90);"
},
- "outline_palette_h_95": {
+ "outline_h_95": {
"declaration": "outline-color: var(--palette_h_95); --outline_color: var(--palette_h_95);"
},
- "outline_palette_h_100": {
+ "outline_h_100": {
"declaration": "outline-color: var(--palette_h_100); --outline_color: var(--palette_h_100);"
},
- "outline_palette_i_00": {
+ "outline_i_00": {
"declaration": "outline-color: var(--palette_i_00); --outline_color: var(--palette_i_00);"
},
- "outline_palette_i_05": {
+ "outline_i_05": {
"declaration": "outline-color: var(--palette_i_05); --outline_color: var(--palette_i_05);"
},
- "outline_palette_i_10": {
+ "outline_i_10": {
"declaration": "outline-color: var(--palette_i_10); --outline_color: var(--palette_i_10);"
},
- "outline_palette_i_20": {
+ "outline_i_20": {
"declaration": "outline-color: var(--palette_i_20); --outline_color: var(--palette_i_20);"
},
- "outline_palette_i_30": {
+ "outline_i_30": {
"declaration": "outline-color: var(--palette_i_30); --outline_color: var(--palette_i_30);"
},
- "outline_palette_i_40": {
+ "outline_i_40": {
"declaration": "outline-color: var(--palette_i_40); --outline_color: var(--palette_i_40);"
},
- "outline_palette_i_50": {
+ "outline_i_50": {
"declaration": "outline-color: var(--palette_i_50); --outline_color: var(--palette_i_50);"
},
- "outline_palette_i_60": {
+ "outline_i_60": {
"declaration": "outline-color: var(--palette_i_60); --outline_color: var(--palette_i_60);"
},
- "outline_palette_i_70": {
+ "outline_i_70": {
"declaration": "outline-color: var(--palette_i_70); --outline_color: var(--palette_i_70);"
},
- "outline_palette_i_80": {
+ "outline_i_80": {
"declaration": "outline-color: var(--palette_i_80); --outline_color: var(--palette_i_80);"
},
- "outline_palette_i_90": {
+ "outline_i_90": {
"declaration": "outline-color: var(--palette_i_90); --outline_color: var(--palette_i_90);"
},
- "outline_palette_i_95": {
+ "outline_i_95": {
"declaration": "outline-color: var(--palette_i_95); --outline_color: var(--palette_i_95);"
},
- "outline_palette_i_100": {
+ "outline_i_100": {
"declaration": "outline-color: var(--palette_i_100); --outline_color: var(--palette_i_100);"
},
- "outline_palette_j_00": {
+ "outline_j_00": {
"declaration": "outline-color: var(--palette_j_00); --outline_color: var(--palette_j_00);"
},
- "outline_palette_j_05": {
+ "outline_j_05": {
"declaration": "outline-color: var(--palette_j_05); --outline_color: var(--palette_j_05);"
},
- "outline_palette_j_10": {
+ "outline_j_10": {
"declaration": "outline-color: var(--palette_j_10); --outline_color: var(--palette_j_10);"
},
- "outline_palette_j_20": {
+ "outline_j_20": {
"declaration": "outline-color: var(--palette_j_20); --outline_color: var(--palette_j_20);"
},
- "outline_palette_j_30": {
+ "outline_j_30": {
"declaration": "outline-color: var(--palette_j_30); --outline_color: var(--palette_j_30);"
},
- "outline_palette_j_40": {
+ "outline_j_40": {
"declaration": "outline-color: var(--palette_j_40); --outline_color: var(--palette_j_40);"
},
- "outline_palette_j_50": {
+ "outline_j_50": {
"declaration": "outline-color: var(--palette_j_50); --outline_color: var(--palette_j_50);"
},
- "outline_palette_j_60": {
+ "outline_j_60": {
"declaration": "outline-color: var(--palette_j_60); --outline_color: var(--palette_j_60);"
},
- "outline_palette_j_70": {
+ "outline_j_70": {
"declaration": "outline-color: var(--palette_j_70); --outline_color: var(--palette_j_70);"
},
- "outline_palette_j_80": {
+ "outline_j_80": {
"declaration": "outline-color: var(--palette_j_80); --outline_color: var(--palette_j_80);"
},
- "outline_palette_j_90": {
+ "outline_j_90": {
"declaration": "outline-color: var(--palette_j_90); --outline_color: var(--palette_j_90);"
},
- "outline_palette_j_95": {
+ "outline_j_95": {
"declaration": "outline-color: var(--palette_j_95); --outline_color: var(--palette_j_95);"
},
- "outline_palette_j_100": {
+ "outline_j_100": {
"declaration": "outline-color: var(--palette_j_100); --outline_color: var(--palette_j_100);"
},
"border_width_1": {
@@ -2772,394 +2772,394 @@
"shadow_alpha_100": {
"declaration": "--shadow_alpha: var(--shadow_alpha_100);"
},
- "shadow_palette_a_00": {
+ "shadow_a_00": {
"declaration": "--shadow_color: var(--palette_a_00);"
},
- "shadow_palette_a_05": {
+ "shadow_a_05": {
"declaration": "--shadow_color: var(--palette_a_05);"
},
- "shadow_palette_a_10": {
+ "shadow_a_10": {
"declaration": "--shadow_color: var(--palette_a_10);"
},
- "shadow_palette_a_20": {
+ "shadow_a_20": {
"declaration": "--shadow_color: var(--palette_a_20);"
},
- "shadow_palette_a_30": {
+ "shadow_a_30": {
"declaration": "--shadow_color: var(--palette_a_30);"
},
- "shadow_palette_a_40": {
+ "shadow_a_40": {
"declaration": "--shadow_color: var(--palette_a_40);"
},
- "shadow_palette_a_50": {
+ "shadow_a_50": {
"declaration": "--shadow_color: var(--palette_a_50);"
},
- "shadow_palette_a_60": {
+ "shadow_a_60": {
"declaration": "--shadow_color: var(--palette_a_60);"
},
- "shadow_palette_a_70": {
+ "shadow_a_70": {
"declaration": "--shadow_color: var(--palette_a_70);"
},
- "shadow_palette_a_80": {
+ "shadow_a_80": {
"declaration": "--shadow_color: var(--palette_a_80);"
},
- "shadow_palette_a_90": {
+ "shadow_a_90": {
"declaration": "--shadow_color: var(--palette_a_90);"
},
- "shadow_palette_a_95": {
+ "shadow_a_95": {
"declaration": "--shadow_color: var(--palette_a_95);"
},
- "shadow_palette_a_100": {
+ "shadow_a_100": {
"declaration": "--shadow_color: var(--palette_a_100);"
},
- "shadow_palette_b_00": {
+ "shadow_b_00": {
"declaration": "--shadow_color: var(--palette_b_00);"
},
- "shadow_palette_b_05": {
+ "shadow_b_05": {
"declaration": "--shadow_color: var(--palette_b_05);"
},
- "shadow_palette_b_10": {
+ "shadow_b_10": {
"declaration": "--shadow_color: var(--palette_b_10);"
},
- "shadow_palette_b_20": {
+ "shadow_b_20": {
"declaration": "--shadow_color: var(--palette_b_20);"
},
- "shadow_palette_b_30": {
+ "shadow_b_30": {
"declaration": "--shadow_color: var(--palette_b_30);"
},
- "shadow_palette_b_40": {
+ "shadow_b_40": {
"declaration": "--shadow_color: var(--palette_b_40);"
},
- "shadow_palette_b_50": {
+ "shadow_b_50": {
"declaration": "--shadow_color: var(--palette_b_50);"
},
- "shadow_palette_b_60": {
+ "shadow_b_60": {
"declaration": "--shadow_color: var(--palette_b_60);"
},
- "shadow_palette_b_70": {
+ "shadow_b_70": {
"declaration": "--shadow_color: var(--palette_b_70);"
},
- "shadow_palette_b_80": {
+ "shadow_b_80": {
"declaration": "--shadow_color: var(--palette_b_80);"
},
- "shadow_palette_b_90": {
+ "shadow_b_90": {
"declaration": "--shadow_color: var(--palette_b_90);"
},
- "shadow_palette_b_95": {
+ "shadow_b_95": {
"declaration": "--shadow_color: var(--palette_b_95);"
},
- "shadow_palette_b_100": {
+ "shadow_b_100": {
"declaration": "--shadow_color: var(--palette_b_100);"
},
- "shadow_palette_c_00": {
+ "shadow_c_00": {
"declaration": "--shadow_color: var(--palette_c_00);"
},
- "shadow_palette_c_05": {
+ "shadow_c_05": {
"declaration": "--shadow_color: var(--palette_c_05);"
},
- "shadow_palette_c_10": {
+ "shadow_c_10": {
"declaration": "--shadow_color: var(--palette_c_10);"
},
- "shadow_palette_c_20": {
+ "shadow_c_20": {
"declaration": "--shadow_color: var(--palette_c_20);"
},
- "shadow_palette_c_30": {
+ "shadow_c_30": {
"declaration": "--shadow_color: var(--palette_c_30);"
},
- "shadow_palette_c_40": {
+ "shadow_c_40": {
"declaration": "--shadow_color: var(--palette_c_40);"
},
- "shadow_palette_c_50": {
+ "shadow_c_50": {
"declaration": "--shadow_color: var(--palette_c_50);"
},
- "shadow_palette_c_60": {
+ "shadow_c_60": {
"declaration": "--shadow_color: var(--palette_c_60);"
},
- "shadow_palette_c_70": {
+ "shadow_c_70": {
"declaration": "--shadow_color: var(--palette_c_70);"
},
- "shadow_palette_c_80": {
+ "shadow_c_80": {
"declaration": "--shadow_color: var(--palette_c_80);"
},
- "shadow_palette_c_90": {
+ "shadow_c_90": {
"declaration": "--shadow_color: var(--palette_c_90);"
},
- "shadow_palette_c_95": {
+ "shadow_c_95": {
"declaration": "--shadow_color: var(--palette_c_95);"
},
- "shadow_palette_c_100": {
+ "shadow_c_100": {
"declaration": "--shadow_color: var(--palette_c_100);"
},
- "shadow_palette_d_00": {
+ "shadow_d_00": {
"declaration": "--shadow_color: var(--palette_d_00);"
},
- "shadow_palette_d_05": {
+ "shadow_d_05": {
"declaration": "--shadow_color: var(--palette_d_05);"
},
- "shadow_palette_d_10": {
+ "shadow_d_10": {
"declaration": "--shadow_color: var(--palette_d_10);"
},
- "shadow_palette_d_20": {
+ "shadow_d_20": {
"declaration": "--shadow_color: var(--palette_d_20);"
},
- "shadow_palette_d_30": {
+ "shadow_d_30": {
"declaration": "--shadow_color: var(--palette_d_30);"
},
- "shadow_palette_d_40": {
+ "shadow_d_40": {
"declaration": "--shadow_color: var(--palette_d_40);"
},
- "shadow_palette_d_50": {
+ "shadow_d_50": {
"declaration": "--shadow_color: var(--palette_d_50);"
},
- "shadow_palette_d_60": {
+ "shadow_d_60": {
"declaration": "--shadow_color: var(--palette_d_60);"
},
- "shadow_palette_d_70": {
+ "shadow_d_70": {
"declaration": "--shadow_color: var(--palette_d_70);"
},
- "shadow_palette_d_80": {
+ "shadow_d_80": {
"declaration": "--shadow_color: var(--palette_d_80);"
},
- "shadow_palette_d_90": {
+ "shadow_d_90": {
"declaration": "--shadow_color: var(--palette_d_90);"
},
- "shadow_palette_d_95": {
+ "shadow_d_95": {
"declaration": "--shadow_color: var(--palette_d_95);"
},
- "shadow_palette_d_100": {
+ "shadow_d_100": {
"declaration": "--shadow_color: var(--palette_d_100);"
},
- "shadow_palette_e_00": {
+ "shadow_e_00": {
"declaration": "--shadow_color: var(--palette_e_00);"
},
- "shadow_palette_e_05": {
+ "shadow_e_05": {
"declaration": "--shadow_color: var(--palette_e_05);"
},
- "shadow_palette_e_10": {
+ "shadow_e_10": {
"declaration": "--shadow_color: var(--palette_e_10);"
},
- "shadow_palette_e_20": {
+ "shadow_e_20": {
"declaration": "--shadow_color: var(--palette_e_20);"
},
- "shadow_palette_e_30": {
+ "shadow_e_30": {
"declaration": "--shadow_color: var(--palette_e_30);"
},
- "shadow_palette_e_40": {
+ "shadow_e_40": {
"declaration": "--shadow_color: var(--palette_e_40);"
},
- "shadow_palette_e_50": {
+ "shadow_e_50": {
"declaration": "--shadow_color: var(--palette_e_50);"
},
- "shadow_palette_e_60": {
+ "shadow_e_60": {
"declaration": "--shadow_color: var(--palette_e_60);"
},
- "shadow_palette_e_70": {
+ "shadow_e_70": {
"declaration": "--shadow_color: var(--palette_e_70);"
},
- "shadow_palette_e_80": {
+ "shadow_e_80": {
"declaration": "--shadow_color: var(--palette_e_80);"
},
- "shadow_palette_e_90": {
+ "shadow_e_90": {
"declaration": "--shadow_color: var(--palette_e_90);"
},
- "shadow_palette_e_95": {
+ "shadow_e_95": {
"declaration": "--shadow_color: var(--palette_e_95);"
},
- "shadow_palette_e_100": {
+ "shadow_e_100": {
"declaration": "--shadow_color: var(--palette_e_100);"
},
- "shadow_palette_f_00": {
+ "shadow_f_00": {
"declaration": "--shadow_color: var(--palette_f_00);"
},
- "shadow_palette_f_05": {
+ "shadow_f_05": {
"declaration": "--shadow_color: var(--palette_f_05);"
},
- "shadow_palette_f_10": {
+ "shadow_f_10": {
"declaration": "--shadow_color: var(--palette_f_10);"
},
- "shadow_palette_f_20": {
+ "shadow_f_20": {
"declaration": "--shadow_color: var(--palette_f_20);"
},
- "shadow_palette_f_30": {
+ "shadow_f_30": {
"declaration": "--shadow_color: var(--palette_f_30);"
},
- "shadow_palette_f_40": {
+ "shadow_f_40": {
"declaration": "--shadow_color: var(--palette_f_40);"
},
- "shadow_palette_f_50": {
+ "shadow_f_50": {
"declaration": "--shadow_color: var(--palette_f_50);"
},
- "shadow_palette_f_60": {
+ "shadow_f_60": {
"declaration": "--shadow_color: var(--palette_f_60);"
},
- "shadow_palette_f_70": {
+ "shadow_f_70": {
"declaration": "--shadow_color: var(--palette_f_70);"
},
- "shadow_palette_f_80": {
+ "shadow_f_80": {
"declaration": "--shadow_color: var(--palette_f_80);"
},
- "shadow_palette_f_90": {
+ "shadow_f_90": {
"declaration": "--shadow_color: var(--palette_f_90);"
},
- "shadow_palette_f_95": {
+ "shadow_f_95": {
"declaration": "--shadow_color: var(--palette_f_95);"
},
- "shadow_palette_f_100": {
+ "shadow_f_100": {
"declaration": "--shadow_color: var(--palette_f_100);"
},
- "shadow_palette_g_00": {
+ "shadow_g_00": {
"declaration": "--shadow_color: var(--palette_g_00);"
},
- "shadow_palette_g_05": {
+ "shadow_g_05": {
"declaration": "--shadow_color: var(--palette_g_05);"
},
- "shadow_palette_g_10": {
+ "shadow_g_10": {
"declaration": "--shadow_color: var(--palette_g_10);"
},
- "shadow_palette_g_20": {
+ "shadow_g_20": {
"declaration": "--shadow_color: var(--palette_g_20);"
},
- "shadow_palette_g_30": {
+ "shadow_g_30": {
"declaration": "--shadow_color: var(--palette_g_30);"
},
- "shadow_palette_g_40": {
+ "shadow_g_40": {
"declaration": "--shadow_color: var(--palette_g_40);"
},
- "shadow_palette_g_50": {
+ "shadow_g_50": {
"declaration": "--shadow_color: var(--palette_g_50);"
},
- "shadow_palette_g_60": {
+ "shadow_g_60": {
"declaration": "--shadow_color: var(--palette_g_60);"
},
- "shadow_palette_g_70": {
+ "shadow_g_70": {
"declaration": "--shadow_color: var(--palette_g_70);"
},
- "shadow_palette_g_80": {
+ "shadow_g_80": {
"declaration": "--shadow_color: var(--palette_g_80);"
},
- "shadow_palette_g_90": {
+ "shadow_g_90": {
"declaration": "--shadow_color: var(--palette_g_90);"
},
- "shadow_palette_g_95": {
+ "shadow_g_95": {
"declaration": "--shadow_color: var(--palette_g_95);"
},
- "shadow_palette_g_100": {
+ "shadow_g_100": {
"declaration": "--shadow_color: var(--palette_g_100);"
},
- "shadow_palette_h_00": {
+ "shadow_h_00": {
"declaration": "--shadow_color: var(--palette_h_00);"
},
- "shadow_palette_h_05": {
+ "shadow_h_05": {
"declaration": "--shadow_color: var(--palette_h_05);"
},
- "shadow_palette_h_10": {
+ "shadow_h_10": {
"declaration": "--shadow_color: var(--palette_h_10);"
},
- "shadow_palette_h_20": {
+ "shadow_h_20": {
"declaration": "--shadow_color: var(--palette_h_20);"
},
- "shadow_palette_h_30": {
+ "shadow_h_30": {
"declaration": "--shadow_color: var(--palette_h_30);"
},
- "shadow_palette_h_40": {
+ "shadow_h_40": {
"declaration": "--shadow_color: var(--palette_h_40);"
},
- "shadow_palette_h_50": {
+ "shadow_h_50": {
"declaration": "--shadow_color: var(--palette_h_50);"
},
- "shadow_palette_h_60": {
+ "shadow_h_60": {
"declaration": "--shadow_color: var(--palette_h_60);"
},
- "shadow_palette_h_70": {
+ "shadow_h_70": {
"declaration": "--shadow_color: var(--palette_h_70);"
},
- "shadow_palette_h_80": {
+ "shadow_h_80": {
"declaration": "--shadow_color: var(--palette_h_80);"
},
- "shadow_palette_h_90": {
+ "shadow_h_90": {
"declaration": "--shadow_color: var(--palette_h_90);"
},
- "shadow_palette_h_95": {
+ "shadow_h_95": {
"declaration": "--shadow_color: var(--palette_h_95);"
},
- "shadow_palette_h_100": {
+ "shadow_h_100": {
"declaration": "--shadow_color: var(--palette_h_100);"
},
- "shadow_palette_i_00": {
+ "shadow_i_00": {
"declaration": "--shadow_color: var(--palette_i_00);"
},
- "shadow_palette_i_05": {
+ "shadow_i_05": {
"declaration": "--shadow_color: var(--palette_i_05);"
},
- "shadow_palette_i_10": {
+ "shadow_i_10": {
"declaration": "--shadow_color: var(--palette_i_10);"
},
- "shadow_palette_i_20": {
+ "shadow_i_20": {
"declaration": "--shadow_color: var(--palette_i_20);"
},
- "shadow_palette_i_30": {
+ "shadow_i_30": {
"declaration": "--shadow_color: var(--palette_i_30);"
},
- "shadow_palette_i_40": {
+ "shadow_i_40": {
"declaration": "--shadow_color: var(--palette_i_40);"
},
- "shadow_palette_i_50": {
+ "shadow_i_50": {
"declaration": "--shadow_color: var(--palette_i_50);"
},
- "shadow_palette_i_60": {
+ "shadow_i_60": {
"declaration": "--shadow_color: var(--palette_i_60);"
},
- "shadow_palette_i_70": {
+ "shadow_i_70": {
"declaration": "--shadow_color: var(--palette_i_70);"
},
- "shadow_palette_i_80": {
+ "shadow_i_80": {
"declaration": "--shadow_color: var(--palette_i_80);"
},
- "shadow_palette_i_90": {
+ "shadow_i_90": {
"declaration": "--shadow_color: var(--palette_i_90);"
},
- "shadow_palette_i_95": {
+ "shadow_i_95": {
"declaration": "--shadow_color: var(--palette_i_95);"
},
- "shadow_palette_i_100": {
+ "shadow_i_100": {
"declaration": "--shadow_color: var(--palette_i_100);"
},
- "shadow_palette_j_00": {
+ "shadow_j_00": {
"declaration": "--shadow_color: var(--palette_j_00);"
},
- "shadow_palette_j_05": {
+ "shadow_j_05": {
"declaration": "--shadow_color: var(--palette_j_05);"
},
- "shadow_palette_j_10": {
+ "shadow_j_10": {
"declaration": "--shadow_color: var(--palette_j_10);"
},
- "shadow_palette_j_20": {
+ "shadow_j_20": {
"declaration": "--shadow_color: var(--palette_j_20);"
},
- "shadow_palette_j_30": {
+ "shadow_j_30": {
"declaration": "--shadow_color: var(--palette_j_30);"
},
- "shadow_palette_j_40": {
+ "shadow_j_40": {
"declaration": "--shadow_color: var(--palette_j_40);"
},
- "shadow_palette_j_50": {
+ "shadow_j_50": {
"declaration": "--shadow_color: var(--palette_j_50);"
},
- "shadow_palette_j_60": {
+ "shadow_j_60": {
"declaration": "--shadow_color: var(--palette_j_60);"
},
- "shadow_palette_j_70": {
+ "shadow_j_70": {
"declaration": "--shadow_color: var(--palette_j_70);"
},
- "shadow_palette_j_80": {
+ "shadow_j_80": {
"declaration": "--shadow_color: var(--palette_j_80);"
},
- "shadow_palette_j_90": {
+ "shadow_j_90": {
"declaration": "--shadow_color: var(--palette_j_90);"
},
- "shadow_palette_j_95": {
+ "shadow_j_95": {
"declaration": "--shadow_color: var(--palette_j_95);"
},
- "shadow_palette_j_100": {
+ "shadow_j_100": {
"declaration": "--shadow_color: var(--palette_j_100);"
},
"width_xs5": {
diff --git a/src/test/modified_class_interpreter.test.ts b/src/test/modified_class_interpreter.test.ts
index 28d1af4fe..ae4355298 100644
--- a/src/test/modified_class_interpreter.test.ts
+++ b/src/test/modified_class_interpreter.test.ts
@@ -642,13 +642,13 @@ describe('modified_class_interpreter', () => {
describe('state modifier ordering for cascade', () => {
test('hover classes come before active classes in output (LVFHA order)', () => {
const result = generate_classes_css({
- class_names: ['active:border_palette_a_50', 'hover:border_palette_b_50'],
+ class_names: ['active:border_a_50', 'hover:border_b_50'],
class_definitions: css_class_definitions,
interpreters: [modified_class_interpreter],
css_properties: null,
});
- assert_css_order(result.css, '.hover\\:border_palette_b_50', '.active\\:border_palette_a_50');
+ assert_css_order(result.css, '.hover\\:border_b_50', '.active\\:border_a_50');
});
test('visited < focus < hover < active ordering', () => {
diff --git a/src/test/vite_plugin_examples.test.ts b/src/test/vite_plugin_examples.test.ts
index 2add9c002..8920b145c 100644
--- a/src/test/vite_plugin_examples.test.ts
+++ b/src/test/vite_plugin_examples.test.ts
@@ -105,14 +105,14 @@ const EXPECTED_CLASSES = [
'flex:1',
// From App - Interactive (hover/active state modifiers)
'row',
- 'hover:border_palette_b_50',
- 'hover:outline_palette_b_50',
- 'active:border_palette_d_50',
- 'active:outline_palette_d_50',
- 'hover:border_palette_g_50',
- 'hover:outline_palette_g_50',
- 'active:border_palette_h_50',
- 'active:outline_palette_h_50',
+ 'hover:border_b_50',
+ 'hover:outline_b_50',
+ 'active:border_d_50',
+ 'active:outline_d_50',
+ 'hover:border_g_50',
+ 'hover:outline_g_50',
+ 'active:border_h_50',
+ 'active:outline_h_50',
].sort();
/**
diff --git a/vite.config.ts b/vite.config.ts
index 1e14ee387..74f4d9d0a 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -29,7 +29,7 @@ const docs_classes: Array = [
...shadow_variant_prefixes.flatMap((p) => shadow_size_variants.map((s) => `${p}${s}`)),
...shadow_alpha_variants.map((v) => `shadow_alpha_${v}`),
...shadow_semantic_values.map((v) => `shadow_color_${v}`),
- ...palette_variants.flatMap((l) => intensity_variants.map((i) => `shadow_palette_${l}_${i}`)),
+ ...palette_variants.flatMap((l) => intensity_variants.map((i) => `shadow_${l}_${i}`)),
...palette_variants.map((l) => `palette_${l}_50`),
];
From 3a987f498b4375dff885f2ed71539b07539606c4 Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Sat, 11 Jul 2026 13:09:14 -0400
Subject: [PATCH 08/33] wip
---
.changeset/dev-initial-load-prescan.md | 14 +
.changeset/quiet-otters-gather.md | 28 +
CLAUDE.md | 18 +-
src/lib/ramps.ts | 66 +-
src/lib/theme_check.ts | 860 ++++++++++++++++++
src/lib/vite_plugin_fuz_css.ts | 380 +++++---
src/routes/ThemeEditor.svelte | 38 +-
src/test/fixtures/vite_dev/extra/widgets.html | 1 +
src/test/fixtures/vite_dev/src/island.html | 1 +
src/test/fixtures/vite_dev/src/page.html | 1 +
src/test/ramps.test.ts | 44 +-
src/test/theme_check.compile.test.ts | 77 ++
src/test/theme_check.test.ts | 215 +++++
src/test/themes.test.ts | 22 +-
src/test/vite_plugin_fuz_css.dev.test.ts | 76 ++
15 files changed, 1670 insertions(+), 171 deletions(-)
create mode 100644 .changeset/dev-initial-load-prescan.md
create mode 100644 .changeset/quiet-otters-gather.md
create mode 100644 src/lib/theme_check.ts
create mode 100644 src/test/fixtures/vite_dev/extra/widgets.html
create mode 100644 src/test/fixtures/vite_dev/src/island.html
create mode 100644 src/test/fixtures/vite_dev/src/page.html
create mode 100644 src/test/theme_check.compile.test.ts
create mode 100644 src/test/theme_check.test.ts
create mode 100644 src/test/vite_plugin_fuz_css.dev.test.ts
diff --git a/.changeset/dev-initial-load-prescan.md b/.changeset/dev-initial-load-prescan.md
new file mode 100644
index 000000000..f90e43c00
--- /dev/null
+++ b/.changeset/dev-initial-load-prescan.md
@@ -0,0 +1,14 @@
+---
+'@fuzdev/fuz_css': patch
+---
+
+fix: complete utility CSS on the first dev page load
+
+The Vite plugin now eagerly pre-scans project sources at dev-server startup
+(new `prescan` option: `true` scans `src` under the Vite root, `false`
+disables, or an array of directories) and resyncs clients whose HMR socket
+connects after a missed CSS update. Previously the first cold-start page
+load could render with incomplete utility classes until a manual refresh:
+extraction state accumulated only from modules Vite had transformed so far,
+and the corrective HMR update was dropped when the browser hadn't connected
+yet.
diff --git a/.changeset/quiet-otters-gather.md b/.changeset/quiet-otters-gather.md
new file mode 100644
index 000000000..401196bff
--- /dev/null
+++ b/.changeset/quiet-otters-gather.md
@@ -0,0 +1,28 @@
+---
+'@fuzdev/fuz_css': minor
+---
+
+feat: add theme validation, numeric-twin accessibility gates, and a compile step
+
+New `theme_check.ts` module with three functions over a shared numeric
+resolution core that turns a theme's authored CSS back into numbers (knob
+defaults from the `ramps.ts` numeric twin, `var(--x)` bindings recursed with a
+cycle guard, and the machine-emitted compiled-cap `min(calc(...), )` form):
+
+- `validate_theme` — the structural lint: non-empty name, `StyleVariable`
+ shape, and known variable names as errors, plus advisory type/range warnings
+ for the knob-tier variables.
+- `check_theme` — evaluates the gamut, ramp-monotonicity, and contrast gates
+ (the same thresholds `ramps.test.ts` asserts for the defaults) against an
+ arbitrary theme, resolved through its bindings. Report-only, never throws.
+- `compile_theme` — recomputes each theme's per-stop worst-hue chroma caps
+ from its own hues, lightness ramp, and hue shift, then emits
+ `palette_chroma_NN` overrides where the baked caps no longer fit (a rotated,
+ monochrome, or dark-only theme), and re-checks the result.
+
+The gate thresholds (`GATE_BODY_TEXT`, `GATE_SUBTLE_TEXT`, `GATE_LINK`,
+`GATE_UI`, `GATE_FILL_TEXT`) move to `theme_check.ts` as exported constants.
+`ramps.ts` gains `ramp_hue_shift_offset` (the numeric twin of the hue-shift
+CSS emitter) and `compute_palette_chroma_caps` (the generalized worst-hue cap
+search behind the baked table and the compile step); `ramp_chroma` and
+`render_chroma_stop_css` take optional knob/cap parameters.
diff --git a/CLAUDE.md b/CLAUDE.md
index f51e87b14..69897bfd1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -114,6 +114,12 @@ combined and only used content is included. In utility-only mode, import
in pure CSS (`calc()`/`pow()`/`oklch()`); the fitted knob constants and CSS
emitters live in [ramps.ts](src/lib/ramps.ts) with design-time gamut and
contrast gates in [oklch.ts](src/lib/oklch.ts)/[wcag.ts](src/lib/wcag.ts)
+- [theme_check.ts](src/lib/theme_check.ts) turns those design-time gates into
+ a theme API: `validate_theme` lints a theme's shape, `check_theme` runs the
+ gamut/monotonicity/contrast gates against an arbitrary theme (resolving its
+ bindings back to numbers), and `compile_theme` recomputes per-theme
+ worst-hue chroma caps so rotated, monochrome, or dark-only themes stay in
+ gamut
### Smart utility class generation
@@ -121,7 +127,10 @@ Two generators available, both using AST-based extraction and per-file caching:
1. **Vite plugin** (preferred) - [vite_plugin_fuz_css.ts](src/lib/vite_plugin_fuz_css.ts)
exposes the generated CSS as `virtual:fuz.css` with HMR; works across
- SvelteKit/Svelte/React/Preact/Solid and needs no committed output file
+ SvelteKit/Svelte/React/Preact/Solid and needs no committed output file.
+ In dev it pre-scans project sources at server startup (see `prescan`) so
+ the first served CSS is complete, and resyncs clients whose HMR socket
+ connects after a missed update
2. **Gro generator** - [gen_fuz_css.ts](src/lib/gen_fuz_css.ts), a SvelteKit
alternative that writes a `fuz.css` genfile
@@ -301,6 +310,9 @@ Use `GenFuzCssOptions` or `VitePluginFuzCssOptions` to customize:
variable that shipped styles still reference)
- `filter_file` - which files get extracted (the default filter includes
node_modules deps)
+- `prescan` (Vite plugin only) - dev-only eager source scan at server
+ startup so the first served CSS is complete (`true` = `src` under the
+ Vite root, `false` disables, or an array of directories)
- `cache_dir` - extraction cache location (default `.fuz/cache/css`)
These are the common options — see
@@ -340,6 +352,10 @@ typography, borders, shading, shadows, layout. See
(kind/axis/leverage/tier/bindable/range) for the knob-tier variables, joined
against `default_variables` by name; includes hook knobs like
`heading_font_weight`
+- [theme_check.ts](src/lib/theme_check.ts) - Theme lint (`validate_theme`),
+ numeric-twin accessibility gates (`check_theme`: gamut, ramp monotonicity,
+ contrast), and the worst-hue chroma-cap compile step (`compile_theme`) over
+ a shared string→number resolution core
- [theme.gen.css.ts](src/lib/theme.gen.css.ts) - Gro generator that produces
`theme.css`
diff --git a/src/lib/ramps.ts b/src/lib/ramps.ts
index b7ba62ef8..bb746fceb 100644
--- a/src/lib/ramps.ts
+++ b/src/lib/ramps.ts
@@ -28,7 +28,7 @@ import {
type ColorSchemeVariant,
type NumericScaleVariant,
} from './variable_data.ts';
-import type {Oklch} from './oklch.ts';
+import {oklch_max_srgb_chroma, type Oklch} from './oklch.ts';
/** The 13 intensity stops in ramp order (00 → 100). */
export const RAMP_STOPS: ReadonlyArray = numeric_scale_variants;
@@ -199,16 +199,35 @@ export const ramp_chroma_shape = (stop: NumericScaleVariant, curve: number): num
/**
* Evaluates the palette chroma at a stop: the knob curve clamped by that
* stop's worst-hue cap. `chroma_scale` multiplies above the clamp.
+ *
+ * @param knobs - chroma-curve knobs; defaults to the fitted `PALETTE_CHROMA_KNOBS`
+ * @param cap - the per-stop gamut clamp; defaults to the baked `PALETTE_CHROMA_CAPS`
*/
export const ramp_chroma = (
scheme: ColorSchemeVariant,
stop: NumericScaleVariant,
chroma_scale = 1,
+ knobs: ChromaRampKnobs = PALETTE_CHROMA_KNOBS[scheme],
+ cap: number = PALETTE_CHROMA_CAPS[scheme][stop],
): number => {
- const knobs = PALETTE_CHROMA_KNOBS[scheme];
const requested =
knobs.chroma_min + (knobs.chroma_max - knobs.chroma_min) * ramp_chroma_shape(stop, knobs.curve);
- return Math.min(requested, PALETTE_CHROMA_CAPS[scheme][stop]) * chroma_scale;
+ return Math.min(requested, cap) * chroma_scale;
+};
+
+/**
+ * Evaluates a stop's hue-shift offset in degrees — the numeric twin of
+ * `render_hue_shift_offset_css`. `hue_shift` is the total rotation across a
+ * ramp, anchored at stop 50; the scheme flip is baked in so positive values
+ * always rotate hue upward toward the dark end.
+ */
+export const ramp_hue_shift_offset = (
+ stop: NumericScaleVariant,
+ scheme: ColorSchemeVariant,
+ hue_shift: number,
+): number => {
+ const centered = ramp_stop_t(stop) - 0.5;
+ return (scheme === 'light' ? centered : -centered) * hue_shift;
};
/**
@@ -242,6 +261,39 @@ export const text_stop_oklch = (stop: NumericScaleVariant, scheme: ColorSchemeVa
NEUTRAL_HUE,
];
+/**
+ * Recomputes the worst-hue safe chroma caps per stop for an arbitrary hue set
+ * and lightness ramp — the generalization of the baked `PALETTE_CHROMA_CAPS`.
+ * For each stop the lightness comes from `ramp_lightness`, each hue is offset
+ * by that stop's `ramp_hue_shift_offset`, and the cap is the minimum
+ * `oklch_max_srgb_chroma` across the hues, floored to 4 decimals to stay
+ * conservative (the browser clips anything past it). A theme's compile step
+ * feeds its own hues, lightness knobs, and hue shift to detect where the baked
+ * worst-hue envelope no longer fits.
+ *
+ * @param hues - OKLCH hue angles the ramp must stay in gamut for
+ * @param lightness_knobs - the palette lightness ramp knobs for this scheme
+ * @param hue_shift - total ramp hue rotation in degrees; defaults to 0
+ */
+export const compute_palette_chroma_caps = (
+ hues: ReadonlyArray,
+ lightness_knobs: LightnessRampKnobs,
+ scheme: ColorSchemeVariant,
+ hue_shift = 0,
+): Record => {
+ const caps = {} as Record;
+ for (const stop of numeric_scale_variants) {
+ const lightness = ramp_lightness(lightness_knobs, stop);
+ let cap = Infinity;
+ for (const hue of hues) {
+ const effective_hue = hue + ramp_hue_shift_offset(stop, scheme, hue_shift);
+ cap = Math.min(cap, oklch_max_srgb_chroma(lightness, effective_hue));
+ }
+ caps[stop] = Math.floor(cap * 1e4) / 1e4;
+ }
+ return caps;
+};
+
/*
CSS emitters — the string twins of the numeric evaluators above. These render
@@ -287,15 +339,19 @@ export const render_chroma_shape_css = (stop: NumericScaleVariant): string => {
* Renders the capped default of a palette chroma ramp stop: the knob curve
* clamped by that stop's worst-hue gamut cap. Per-scheme because the caps
* differ; `--chroma_scale` multiplies outside this clamp, at the color stops.
+ *
+ * @param cap - the clamp value; defaults to the baked `PALETTE_CHROMA_CAPS`,
+ * overridden by the theme compile step with a recomputed worst-hue cap
*/
export const render_chroma_stop_css = (
stop: NumericScaleVariant,
scheme: ColorSchemeVariant,
+ cap: number = PALETTE_CHROMA_CAPS[scheme][stop],
): string => {
- const cap = format_ramp_number(PALETTE_CHROMA_CAPS[scheme][stop]);
+ const cap_str = format_ramp_number(cap);
return `min(calc(var(--palette_chroma_min) + (var(--palette_chroma_max) - var(--palette_chroma_min)) * var(--chroma_shape_${
stop
- })), ${cap})`;
+ })), ${cap_str})`;
};
/**
diff --git a/src/lib/theme_check.ts b/src/lib/theme_check.ts
new file mode 100644
index 000000000..0e862ec5e
--- /dev/null
+++ b/src/lib/theme_check.ts
@@ -0,0 +1,860 @@
+/**
+ * Theme lint, numeric-twin accessibility gates, and the compile step for the
+ * derived OKLCH color system.
+ *
+ * Three functions sit over a shared numeric resolution core:
+ *
+ * - `validate_theme` is the structural lint — non-empty name, `StyleVariable`
+ * shape, known variable names, and advisory type/range warnings for the
+ * knob-tier variables.
+ * - `check_theme` evaluates the gamut, ramp-monotonicity, and contrast gates
+ * (the same ones `src/test/ramps.test.ts` asserts for the defaults) against
+ * an arbitrary theme, reusing the `ramps.ts` numeric twin, `oklch.ts`
+ * conversions, and `wcag.ts` ratios. It is report-only and never throws.
+ * - `compile_theme` recomputes the per-stop worst-hue chroma caps for a
+ * theme's own hues and lightness ramp and emits `palette_chroma_NN`
+ * overrides where the baked caps no longer fit, then re-checks.
+ *
+ * The resolution core turns the CSS strings themes author back into numbers so
+ * the gates can run. Its contract:
+ *
+ * - Knob-tier defaults come from the numeric-twin constants in `ramps.ts`, not
+ * from parsing `default_variables`; intent and neutral hues default to their
+ * palette-letter binding (`hue_accent` → `hue_a`, `hue_neutral` → `hue_f`,
+ * and so on) so they follow an overridden letter.
+ * - Theme-authored values parse as a numeric literal, an exact `var(--x)`
+ * reference (recursed through the same effective-value merge, with a
+ * visited-set cycle guard), or the machine-emitted compiled-cap
+ * `min(calc(...), )` form. Anything else is unresolvable and is
+ * recorded with its variable, value, and reason.
+ * - Derived ramp stops use a pinned numeric value when the theme pins one,
+ * fall back to the `ramps.ts` formulas with the resolved knobs otherwise,
+ * and mark the touching gates `unchecked` when a pin is unresolvable.
+ *
+ * The effective-value merge mirrors the renderer's cascade-layer semantics
+ * (`theme.ts` `render_theme_style`, `theme_editor_state.svelte.ts`
+ * `display_value`): light = `theme.light`; dark = `theme.dark ?? theme.light`,
+ * falling back to the numeric-twin default for the scheme.
+ *
+ * @module
+ */
+
+import {StyleVariable} from './variable.ts';
+import type {Theme} from './theme.ts';
+import {default_variables} from './variables.ts';
+import {theme_knob_by_name, theme_knob_hook_names, type ThemeKnob} from './knobs.ts';
+import {
+ PALETTE_HUES,
+ PALETTE_LIGHTNESS_KNOBS,
+ SHADE_LIGHTNESS_KNOBS,
+ TEXT_LIGHTNESS_KNOBS,
+ PALETTE_CHROMA_KNOBS,
+ PALETTE_CHROMA_CAPS,
+ NEUTRAL_CHROMA,
+ ramp_lightness,
+ ramp_chroma,
+ ramp_chroma_shape,
+ ramp_hue_shift_offset,
+ compute_palette_chroma_caps,
+ render_chroma_stop_css,
+ type LightnessRampKnobs,
+} from './ramps.ts';
+import {
+ numeric_scale_variants,
+ palette_variants,
+ intent_variants,
+ color_scheme_variants,
+ type NumericScaleVariant,
+ type ColorSchemeVariant,
+ type PaletteVariant,
+} from './variable_data.ts';
+import {oklch_to_srgb, oklch_in_srgb_gamut, type Oklch, type RgbUnit} from './oklch.ts';
+import {wcag_contrast_ratio} from './wcag.ts';
+
+//
+// Gate thresholds — the WCAG levels the derived palette is designed to clear.
+//
+
+/** AAA body text: `text_80` on `shade_00`/`05`/`10`. */
+export const GATE_BODY_TEXT = 7;
+/** Disabled/secondary floor: `text_50` on `shade_00`. */
+export const GATE_SUBTLE_TEXT = 3;
+/** AA link default: the accent hue at stop 60 on `shade_00`. */
+export const GATE_LINK = 4.5;
+/** WCAG 1.4.11 non-text: every hue at stop 50 vs `shade_00`. */
+export const GATE_UI = 3;
+/** Large-text floor: `text_max` on every stop-50 fill. */
+export const GATE_FILL_TEXT = 3;
+
+/**
+ * The variable names a theme may set: the declared defaults plus the hook
+ * knobs `style.css` consumes through `var()` fallbacks.
+ */
+export const known_theme_variable_names: Set = new Set([
+ ...default_variables.map((v) => v.name),
+ ...theme_knob_hook_names,
+]);
+
+//
+// Report types.
+//
+
+/**
+ * A structural lint finding. `error` marks a broken theme (bad shape, unknown
+ * variable); `warning` is advisory (value doesn't match the knob's kind or
+ * sits outside its safe range).
+ */
+export interface ThemeIssue {
+ level: 'error' | 'warning';
+ message: string;
+ variable?: string;
+}
+
+/** Which accessibility gate an entry belongs to. */
+export type ThemeGateId = 'gamut' | 'monotonicity' | 'contrast';
+
+/** A single gate measurement against a theme. */
+export interface ThemeGateEntry {
+ gate: ThemeGateId;
+ scheme: ColorSchemeVariant;
+ subject: string;
+ value: number;
+ threshold: number;
+ pass: boolean;
+}
+
+/** A gate input that couldn't be resolved to a number, so its gate was skipped. */
+export interface ThemeUncheckedEntry {
+ variable: string;
+ value: string;
+ reason: string;
+}
+
+/**
+ * The result of `check_theme`. `ok` is true only when every entry passes and
+ * nothing was left unchecked.
+ */
+export interface ThemeCheckReport {
+ ok: boolean;
+ entries: Array;
+ unchecked: Array;
+}
+
+/** The output of `compile_theme`: the emitted theme plus its lint and gate report. */
+export interface CompiledTheme {
+ theme: Theme;
+ report: ThemeCheckReport;
+ issues: Array;
+}
+
+//
+// Resolution core.
+//
+
+/** A resolved numeric value, or the offending variable/value/reason on failure. */
+type Resolved =
+ | {ok: true; value: number}
+ | {ok: false; variable: string; value: string; reason: string};
+
+/** Intent and neutral hues default to a palette-letter binding. */
+const INTENT_HUE_DEFAULT_BINDING: Record = {
+ hue_accent: 'hue_a',
+ hue_positive: 'hue_b',
+ hue_negative: 'hue_c',
+ hue_caution: 'hue_h',
+ hue_info: 'hue_i',
+ hue_neutral: 'hue_f',
+};
+
+const LIGHTNESS_KNOBS_BY_FAMILY: Record<
+ 'palette' | 'shade' | 'text',
+ Record
+> = {
+ palette: PALETTE_LIGHTNESS_KNOBS,
+ shade: SHADE_LIGHTNESS_KNOBS,
+ text: TEXT_LIGHTNESS_KNOBS,
+};
+
+const PALETTE_LETTER_MATCHER = /^hue_([a-j])$/u;
+const LIGHTNESS_KNOB_MATCHER = /^(palette|shade|text)_lightness_(00|100|curve)$/u;
+const LIGHTNESS_STOP_MATCHER =
+ /^(palette|shade|text)_lightness_(05|10|20|30|40|50|60|70|80|90|95)$/u;
+const PALETTE_CHROMA_STOP_MATCHER = /^palette_chroma_(00|05|10|20|30|40|50|60|70|80|90|95|100)$/u;
+const VAR_MATCHER = /^var\(\s*--([a-z][a-z0-9_]*)\s*\)$/u;
+
+/**
+ * The compiled worst-hue cap form emitted by `render_chroma_stop_css`:
+ * `min(calc(var(--palette_chroma_min) + (var(--palette_chroma_max) - var(--palette_chroma_min)) * var(--chroma_shape_NN)), )`.
+ * Recognizing it keeps compiled themes fully checkable.
+ */
+const COMPILED_CAP_MATCHER =
+ /^min\(\s*calc\(\s*var\(--palette_chroma_min\)\s*\+\s*\(\s*var\(--palette_chroma_max\)\s*-\s*var\(--palette_chroma_min\)\s*\)\s*\*\s*var\(--chroma_shape_(00|05|10|20|30|40|50|60|70|80|90|95|100)\)\s*\)\s*,\s*(-?\d*\.?\d+)\s*\)$/u;
+
+/**
+ * Resolves knob-tier and derived-stop variables of a single theme to numbers,
+ * mirroring the renderer's effective-value merge and the `ramps.ts` formulas.
+ */
+class ThemeResolver {
+ readonly #by_name: Map;
+ readonly #memo: Map = new Map();
+
+ constructor(theme: Theme) {
+ this.#by_name = new Map(theme.variables.map((v) => [v.name, v]));
+ }
+
+ /** Whether the theme authors a value for `name` (a pin). */
+ pinned(name: string): boolean {
+ return this.#by_name.has(name);
+ }
+
+ /** Resolves `name` for `scheme`, memoized per name+scheme. */
+ resolve(name: string, scheme: ColorSchemeVariant): Resolved {
+ const key = `${scheme}|${name}`;
+ const cached = this.#memo.get(key);
+ if (cached) return cached;
+ const result = this.#resolve(name, scheme, new Set());
+ this.#memo.set(key, result);
+ return result;
+ }
+
+ // the theme-authored value for a slot, honoring the dark → light fallback
+ #authored(name: string, scheme: ColorSchemeVariant): string | undefined {
+ const v = this.#by_name.get(name);
+ if (!v) return undefined;
+ return scheme === 'light' ? v.light : (v.dark ?? v.light);
+ }
+
+ #resolve(name: string, scheme: ColorSchemeVariant, visited: Set): Resolved {
+ if (visited.has(name)) {
+ return {ok: false, variable: name, value: `var(--${name})`, reason: 'cyclic var() reference'};
+ }
+ const next = new Set(visited);
+ next.add(name);
+ const authored = this.#authored(name, scheme);
+ if (authored !== undefined) return this.#parse(name, authored, scheme, next);
+ return this.#resolve_default(name, scheme, next);
+ }
+
+ #parse(name: string, value: string, scheme: ColorSchemeVariant, visited: Set): Resolved {
+ const trimmed = value.trim();
+ // numeric literal
+ if (trimmed !== '') {
+ const n = Number(trimmed);
+ if (Number.isFinite(n)) return {ok: true, value: n};
+ }
+ // exactly var(--x) — recurse through the same merge
+ const var_match = VAR_MATCHER.exec(trimmed);
+ if (var_match) return this.#resolve(var_match[1]!, scheme, visited);
+ // machine-emitted compiled cap form
+ const cap = this.#parse_compiled_cap(trimmed, scheme, visited);
+ if (cap) return cap;
+ return {ok: false, variable: name, value: trimmed, reason: 'unrecognized value expression'};
+ }
+
+ #parse_compiled_cap(
+ value: string,
+ scheme: ColorSchemeVariant,
+ visited: Set,
+ ): Resolved | null {
+ const m = COMPILED_CAP_MATCHER.exec(value);
+ if (!m) return null;
+ const stop = m[1] as NumericScaleVariant;
+ const literal = Number(m[2]);
+ const chroma_min = this.#resolve('palette_chroma_min', scheme, visited);
+ if (!chroma_min.ok) return chroma_min;
+ const chroma_max = this.#resolve('palette_chroma_max', scheme, visited);
+ if (!chroma_max.ok) return chroma_max;
+ const curve = this.#resolve('palette_chroma_curve', scheme, visited);
+ if (!curve.ok) return curve;
+ const shape = ramp_chroma_shape(stop, curve.value);
+ const requested = chroma_min.value + (chroma_max.value - chroma_min.value) * shape;
+ return {ok: true, value: Math.min(requested, literal)};
+ }
+
+ #resolve_default(name: string, scheme: ColorSchemeVariant, visited: Set): Resolved {
+ // palette letters
+ const letter_match = PALETTE_LETTER_MATCHER.exec(name);
+ if (letter_match) return {ok: true, value: PALETTE_HUES[letter_match[1] as PaletteVariant]};
+ // intent/neutral hues default to a palette-letter binding
+ const binding = INTENT_HUE_DEFAULT_BINDING[name];
+ if (binding) return this.#resolve(binding, scheme, visited);
+ // scalar knobs
+ if (name === 'chroma_scale') return {ok: true, value: 1};
+ if (name === 'hue_shift') return {ok: true, value: 0};
+ if (name === 'neutral_chroma') return {ok: true, value: NEUTRAL_CHROMA[scheme]};
+ if (name === 'palette_chroma_min') {
+ return {ok: true, value: PALETTE_CHROMA_KNOBS[scheme].chroma_min};
+ }
+ if (name === 'palette_chroma_max') {
+ return {ok: true, value: PALETTE_CHROMA_KNOBS[scheme].chroma_max};
+ }
+ if (name === 'palette_chroma_curve') {
+ return {ok: true, value: PALETTE_CHROMA_KNOBS[scheme].curve};
+ }
+ // lightness endpoints and curve
+ const knob_match = LIGHTNESS_KNOB_MATCHER.exec(name);
+ if (knob_match) {
+ const knobs =
+ LIGHTNESS_KNOBS_BY_FAMILY[knob_match[1] as 'palette' | 'shade' | 'text'][scheme];
+ const field = knob_match[2];
+ const value =
+ field === '00' ? knobs.lightness_00 : field === '100' ? knobs.lightness_100 : knobs.curve;
+ return {ok: true, value};
+ }
+ // derived lightness intermediates — compute from the resolved knobs
+ const stop_match = LIGHTNESS_STOP_MATCHER.exec(name);
+ if (stop_match) {
+ const family = stop_match[1] as 'palette' | 'shade' | 'text';
+ const stop = stop_match[2] as NumericScaleVariant;
+ const knobs = this.#lightness_knobs(family, scheme, visited);
+ if (!knobs.ok) return knobs.error;
+ return {ok: true, value: ramp_lightness(knobs.value, stop)};
+ }
+ // derived palette chroma stops — the capped knob curve
+ const chroma_match = PALETTE_CHROMA_STOP_MATCHER.exec(name);
+ if (chroma_match) {
+ const stop = chroma_match[1] as NumericScaleVariant;
+ const knobs = this.#chroma_knobs(scheme, visited);
+ if (!knobs.ok) return knobs.error;
+ const value = ramp_chroma(scheme, stop, 1, knobs.value, PALETTE_CHROMA_CAPS[scheme][stop]);
+ return {ok: true, value};
+ }
+ return {
+ ok: false,
+ variable: name,
+ value: '(default)',
+ reason: 'no numeric default for variable',
+ };
+ }
+
+ #lightness_knobs(
+ family: 'palette' | 'shade' | 'text',
+ scheme: ColorSchemeVariant,
+ visited: Set,
+ ): {ok: true; value: LightnessRampKnobs} | {ok: false; error: Resolved} {
+ const l00 = this.#resolve(`${family}_lightness_00`, scheme, visited);
+ if (!l00.ok) return {ok: false, error: l00};
+ const l100 = this.#resolve(`${family}_lightness_100`, scheme, visited);
+ if (!l100.ok) return {ok: false, error: l100};
+ const curve = this.#resolve(`${family}_lightness_curve`, scheme, visited);
+ if (!curve.ok) return {ok: false, error: curve};
+ return {
+ ok: true,
+ value: {lightness_00: l00.value, lightness_100: l100.value, curve: curve.value},
+ };
+ }
+
+ #chroma_knobs(
+ scheme: ColorSchemeVariant,
+ visited: Set,
+ ):
+ | {ok: true; value: {chroma_min: number; chroma_max: number; curve: number}}
+ | {ok: false; error: Resolved} {
+ const chroma_min = this.#resolve('palette_chroma_min', scheme, visited);
+ if (!chroma_min.ok) return {ok: false, error: chroma_min};
+ const chroma_max = this.#resolve('palette_chroma_max', scheme, visited);
+ if (!chroma_max.ok) return {ok: false, error: chroma_max};
+ const curve = this.#resolve('palette_chroma_curve', scheme, visited);
+ if (!curve.ok) return {ok: false, error: curve};
+ return {
+ ok: true,
+ value: {chroma_min: chroma_min.value, chroma_max: chroma_max.value, curve: curve.value},
+ };
+ }
+}
+
+/**
+ * Resolves a single knob-tier or derived-stop variable of `theme` to a number,
+ * or `null` when it can't be resolved. Exposed for direct tests of the
+ * resolution rules (binding chains, cycles, unresolvable expressions).
+ *
+ * @param name - the variable name (without the leading `--`)
+ */
+export const resolve_theme_knob = (
+ theme: Theme,
+ name: string,
+ scheme: ColorSchemeVariant,
+): number | null => {
+ const r = new ThemeResolver(theme).resolve(name, scheme);
+ return r.ok ? r.value : null;
+};
+
+//
+// validate_theme — the structural lint.
+//
+
+const validate_knob_value = (
+ knob: ThemeKnob,
+ value: string,
+ variable: string,
+ slot: string,
+): Array => {
+ const issues: Array = [];
+ const trimmed = value.trim();
+ const numeric = trimmed !== '' && Number.isFinite(Number(trimmed));
+ const check_range = (): void => {
+ if (knob.range && (Number(trimmed) < knob.range[0] || Number(trimmed) > knob.range[1])) {
+ issues.push({
+ level: 'warning',
+ message: `${variable} ${slot} ${trimmed} is outside the safe range [${knob.range[0]}, ${
+ knob.range[1]
+ }], the design envelope (knowingly exceedable)`,
+ variable,
+ });
+ }
+ };
+ switch (knob.kind) {
+ case 'number':
+ case 'percent': {
+ if (!numeric) {
+ issues.push({
+ level: 'warning',
+ message: `${variable} ${slot} "${value}" is not a numeric ${knob.kind} value`,
+ variable,
+ });
+ } else {
+ check_range();
+ }
+ break;
+ }
+ case 'hue': {
+ // a literal angle, or a var(--hue_X) binding (legal CSS regardless of `bindable`)
+ const is_binding = /^var\(--hue_[a-j]\)$/u.test(trimmed);
+ if (!numeric && !is_binding) {
+ issues.push({
+ level: 'warning',
+ message: `${variable} ${slot} "${value}" is not a hue angle or var(--hue_X) binding`,
+ variable,
+ });
+ } else if (numeric) {
+ check_range();
+ }
+ break;
+ }
+ case 'time': {
+ if (!/^-?\d*\.?\d+s$/u.test(trimmed)) {
+ issues.push({
+ level: 'warning',
+ message: `${variable} ${slot} "${value}" is not a CSS time value like 0.2s`,
+ variable,
+ });
+ }
+ break;
+ }
+ case 'enum': {
+ if (knob.values && !knob.values.includes(trimmed)) {
+ issues.push({
+ level: 'warning',
+ message: `${variable} ${slot} "${value}" is not one of ${knob.values.join(', ')}`,
+ variable,
+ });
+ }
+ break;
+ }
+ default:
+ // length, color, font_stack, shadow, text — freeform, advisory only
+ break;
+ }
+ return issues;
+};
+
+/**
+ * Lints a theme's structure: a non-empty name, valid `StyleVariable` shape and
+ * known name per variable (errors), and advisory type/range warnings for the
+ * knob-tier variables. Value validation is advisory and never an error. An
+ * empty array means the theme is structurally valid.
+ */
+export const validate_theme = (theme: Theme): Array => {
+ const issues: Array = [];
+ if (!theme.name) {
+ issues.push({level: 'error', message: 'theme name must be non-empty'});
+ }
+ for (const variable of theme.variables) {
+ const parsed = StyleVariable.safeParse(variable);
+ const name: unknown = (variable as {name?: unknown}).name;
+ const name_label = typeof name === 'string' ? name : undefined;
+ if (!parsed.success) {
+ for (const issue of parsed.error.issues) {
+ issues.push({
+ level: 'error',
+ message: `invalid variable${name_label ? ` "${name_label}"` : ''}: ${issue.message}`,
+ ...(name_label ? {variable: name_label} : null),
+ });
+ }
+ continue;
+ }
+ const valid = parsed.data;
+ if (!known_theme_variable_names.has(valid.name)) {
+ issues.push({
+ level: 'error',
+ message: `unknown variable "${valid.name}"`,
+ variable: valid.name,
+ });
+ continue;
+ }
+ const knob = theme_knob_by_name.get(valid.name);
+ if (!knob) continue;
+ if (valid.light !== undefined) {
+ issues.push(...validate_knob_value(knob, valid.light, valid.name, 'light'));
+ }
+ if (valid.dark !== undefined) {
+ issues.push(...validate_knob_value(knob, valid.dark, valid.name, 'dark'));
+ }
+ }
+ return issues;
+};
+
+//
+// check_theme — the numeric-twin accessibility gates.
+//
+
+const clamp01 = (n: number): number => Math.min(1, Math.max(0, n));
+
+const clamp_rgb = (rgb: RgbUnit): RgbUnit => [clamp01(rgb[0]), clamp01(rgb[1]), clamp01(rgb[2])];
+
+// max sRGB channel excess outside [0, 1] — 0 when in gamut
+const gamut_excess = ([r, g, b]: RgbUnit): number => Math.max(0, -r, r - 1, -g, g - 1, -b, b - 1);
+
+/**
+ * Runs the gamut, monotonicity, and contrast gates against a theme, resolving
+ * its authored CSS back to numbers through the resolution core. Report-only:
+ * failures land in `entries` (with `pass: false`), inputs that can't be
+ * resolved land in `unchecked`, and `ok` is true only when every entry passes
+ * and nothing is unchecked. Never throws.
+ */
+export const check_theme = (theme: Theme): ThemeCheckReport => {
+ const resolver = new ThemeResolver(theme);
+ const entries: Array = [];
+ const unchecked: Array = [];
+ const seen_unchecked: Set = new Set();
+
+ const record = (r: Extract): null => {
+ const key = `${r.variable}|${r.value}|${r.reason}`;
+ if (!seen_unchecked.has(key)) {
+ seen_unchecked.add(key);
+ unchecked.push({variable: r.variable, value: r.value, reason: r.reason});
+ }
+ return null;
+ };
+
+ const num = (name: string, scheme: ColorSchemeVariant): number | null => {
+ const r = resolver.resolve(name, scheme);
+ return r.ok ? r.value : record(r);
+ };
+
+ // a palette/intent ramp color at a stop for a given hue angle
+ const ramp_color = (
+ hue: number,
+ stop: NumericScaleVariant,
+ scheme: ColorSchemeVariant,
+ ): Oklch | null => {
+ const lightness = num(`palette_lightness_${stop}`, scheme);
+ const chroma_stop = num(`palette_chroma_${stop}`, scheme);
+ const chroma_scale = num('chroma_scale', scheme);
+ const hue_shift = num('hue_shift', scheme);
+ if (lightness === null || chroma_stop === null || chroma_scale === null || hue_shift === null) {
+ return null;
+ }
+ return [
+ lightness,
+ chroma_stop * chroma_scale,
+ hue + ramp_hue_shift_offset(stop, scheme, hue_shift),
+ ];
+ };
+
+ // a neutral (shade/text) ramp color at a stop
+ const neutral_color = (
+ family: 'shade' | 'text',
+ stop: NumericScaleVariant,
+ scheme: ColorSchemeVariant,
+ ): Oklch | null => {
+ const lightness = num(`${family}_lightness_${stop}`, scheme);
+ const neutral_c = num('neutral_chroma', scheme);
+ const curve = num('palette_chroma_curve', scheme);
+ const neutral_hue = num('hue_neutral', scheme);
+ const hue_shift = num('hue_shift', scheme);
+ if (
+ lightness === null ||
+ neutral_c === null ||
+ curve === null ||
+ neutral_hue === null ||
+ hue_shift === null
+ ) {
+ return null;
+ }
+ return [
+ lightness,
+ neutral_c * ramp_chroma_shape(stop, curve),
+ neutral_hue + ramp_hue_shift_offset(stop, scheme, hue_shift),
+ ];
+ };
+
+ const contrast = (a: RgbUnit, b: RgbUnit): number =>
+ wcag_contrast_ratio(clamp_rgb(a), clamp_rgb(b));
+
+ const push_gamut = (subject: string, color: Oklch, scheme: ColorSchemeVariant): void => {
+ entries.push({
+ gate: 'gamut',
+ scheme,
+ subject,
+ value: gamut_excess(oklch_to_srgb(color)),
+ threshold: 1e-4,
+ pass: oklch_in_srgb_gamut(color, 1e-4),
+ });
+ };
+
+ const push_monotonicity = (
+ family: 'palette' | 'shade' | 'text',
+ scheme: ColorSchemeVariant,
+ ): void => {
+ const lightnesses: Array = [];
+ for (const stop of numeric_scale_variants) {
+ const l = num(`${family}_lightness_${stop}`, scheme);
+ if (l === null) return; // input unchecked; skip this family+scheme
+ lightnesses.push(l);
+ }
+ const direction = Math.sign(lightnesses[lightnesses.length - 1]! - lightnesses[0]!);
+ let min_step = Infinity;
+ for (let i = 1; i < lightnesses.length; i++) {
+ min_step = Math.min(min_step, (lightnesses[i]! - lightnesses[i - 1]!) * direction);
+ }
+ // direction 0 (degenerate ramp) yields min_step 0 and fails, as it should
+ const value = direction === 0 ? 0 : min_step;
+ entries.push({
+ gate: 'monotonicity',
+ scheme,
+ subject: `${family}_lightness`,
+ value,
+ threshold: 0,
+ pass: value > 0,
+ });
+ };
+
+ for (const scheme of color_scheme_variants) {
+ // gamut: palette letters × 13 stops
+ const letter_hues: Array = [];
+ for (const letter of palette_variants) {
+ const hue = num(`hue_${letter}`, scheme);
+ if (hue === null) continue;
+ letter_hues.push(hue);
+ for (const stop of numeric_scale_variants) {
+ const color = ramp_color(hue, stop, scheme);
+ if (color) push_gamut(`palette_${letter}_${stop}`, color, scheme);
+ }
+ }
+ // gamut: each intent hue that resolves to a literal angle distinct from all letters
+ for (const intent of intent_variants) {
+ const hue = num(`hue_${intent}`, scheme);
+ if (hue === null) continue;
+ if (letter_hues.some((h) => Math.abs(h - hue) < 1e-9)) continue;
+ for (const stop of numeric_scale_variants) {
+ const color = ramp_color(hue, stop, scheme);
+ if (color) push_gamut(`${intent}_${stop}`, color, scheme);
+ }
+ }
+ // gamut: the neutral (shade/text) scales
+ for (const family of ['shade', 'text'] as const) {
+ for (const stop of numeric_scale_variants) {
+ const color = neutral_color(family, stop, scheme);
+ if (color) push_gamut(`${family}_${stop}`, color, scheme);
+ }
+ }
+
+ // monotonicity: each lightness family
+ for (const family of ['palette', 'shade', 'text'] as const) {
+ push_monotonicity(family, scheme);
+ }
+
+ // contrast: body text — text_80 on shade_00/05/10
+ const text_80 = neutral_color('text', '80', scheme);
+ for (const stop of ['00', '05', '10'] as const) {
+ const surface = neutral_color('shade', stop, scheme);
+ if (!text_80 || !surface) continue;
+ const ratio = contrast(oklch_to_srgb(text_80), oklch_to_srgb(surface));
+ entries.push({
+ gate: 'contrast',
+ scheme,
+ subject: `text_80 on shade_${stop}`,
+ value: ratio,
+ threshold: GATE_BODY_TEXT,
+ pass: ratio >= GATE_BODY_TEXT,
+ });
+ }
+
+ const shade_00 = neutral_color('shade', '00', scheme);
+
+ // contrast: subtle text — text_50 on shade_00
+ const text_50 = neutral_color('text', '50', scheme);
+ if (text_50 && shade_00) {
+ const ratio = contrast(oklch_to_srgb(text_50), oklch_to_srgb(shade_00));
+ entries.push({
+ gate: 'contrast',
+ scheme,
+ subject: 'text_50 on shade_00',
+ value: ratio,
+ threshold: GATE_SUBTLE_TEXT,
+ pass: ratio >= GATE_SUBTLE_TEXT,
+ });
+ }
+
+ // contrast: link — the accent hue at stop 60 on shade_00 (resolved through bindings)
+ const accent_hue = num('hue_accent', scheme);
+ if (accent_hue !== null && shade_00) {
+ const link = ramp_color(accent_hue, '60', scheme);
+ if (link) {
+ const ratio = contrast(oklch_to_srgb(link), oklch_to_srgb(shade_00));
+ entries.push({
+ gate: 'contrast',
+ scheme,
+ subject: 'accent_60 on shade_00',
+ value: ratio,
+ threshold: GATE_LINK,
+ pass: ratio >= GATE_LINK,
+ });
+ }
+ }
+
+ // contrast: UI affordances — every letter and intent fill at stop 50
+ const text_max: RgbUnit = scheme === 'light' ? [0, 0, 0] : [1, 1, 1];
+ const fills: Array<[string, number]> = [];
+ for (const letter of palette_variants) {
+ const hue = num(`hue_${letter}`, scheme);
+ if (hue !== null) fills.push([`palette_${letter}`, hue]);
+ }
+ for (const intent of intent_variants) {
+ const hue = num(`hue_${intent}`, scheme);
+ if (hue !== null) fills.push([intent, hue]);
+ }
+ if (shade_00) {
+ for (const [label, hue] of fills) {
+ const fill = ramp_color(hue, '50', scheme);
+ if (!fill) continue;
+ const fill_rgb = oklch_to_srgb(fill);
+ const ui = contrast(fill_rgb, oklch_to_srgb(shade_00));
+ entries.push({
+ gate: 'contrast',
+ scheme,
+ subject: `${label}_50 vs shade_00`,
+ value: ui,
+ threshold: GATE_UI,
+ pass: ui >= GATE_UI,
+ });
+ const on_fill = contrast(text_max, fill_rgb);
+ entries.push({
+ gate: 'contrast',
+ scheme,
+ subject: `text_max on ${label}_50`,
+ value: on_fill,
+ threshold: GATE_FILL_TEXT,
+ pass: on_fill >= GATE_FILL_TEXT,
+ });
+ }
+ }
+ }
+
+ const ok = unchecked.length === 0 && entries.every((e) => e.pass);
+ return {ok, entries, unchecked};
+};
+
+//
+// compile_theme — recompute worst-hue caps for the theme's own hues.
+//
+
+// the difference at which a recomputed cap is worth emitting; below this the
+// baked table's ~1e-3 search slack would produce no-op overrides, and sub-JND
+// chroma isn't worth an override
+const CAP_EMIT_EPSILON = 0.002;
+
+// the theme's effective hue set for cap recomputation: 10 letters plus any
+// intent that resolves to a literal angle distinct from every letter
+const collect_hues = (resolver: ThemeResolver, scheme: ColorSchemeVariant): Array => {
+ const hues: Array = [];
+ for (const letter of palette_variants) {
+ const r = resolver.resolve(`hue_${letter}`, scheme);
+ if (r.ok) hues.push(r.value);
+ }
+ for (const intent of intent_variants) {
+ const r = resolver.resolve(`hue_${intent}`, scheme);
+ if (r.ok && !hues.some((h) => Math.abs(h - r.value) < 1e-9)) hues.push(r.value);
+ }
+ return hues;
+};
+
+const resolve_or = (
+ resolver: ThemeResolver,
+ name: string,
+ scheme: ColorSchemeVariant,
+ fallback: number,
+): number => {
+ const r = resolver.resolve(name, scheme);
+ return r.ok ? r.value : fallback;
+};
+
+const resolve_lightness_knobs = (
+ resolver: ThemeResolver,
+ scheme: ColorSchemeVariant,
+): LightnessRampKnobs => {
+ const l00 = resolver.resolve('palette_lightness_00', scheme);
+ const l100 = resolver.resolve('palette_lightness_100', scheme);
+ const curve = resolver.resolve('palette_lightness_curve', scheme);
+ const fallback = PALETTE_LIGHTNESS_KNOBS[scheme];
+ return {
+ lightness_00: l00.ok ? l00.value : fallback.lightness_00,
+ lightness_100: l100.ok ? l100.value : fallback.lightness_100,
+ curve: curve.ok ? curve.value : fallback.curve,
+ };
+};
+
+/**
+ * Recomputes a theme's per-stop worst-hue chroma caps from its own hues,
+ * palette lightness ramp, and hue shift, then emits `palette_chroma_NN`
+ * overrides wherever the baked caps no longer fit — the fix for a theme
+ * (rotated hues, a monochrome collapse, a dark-only mirror) whose gamut
+ * headroom the baked worst-hue table misjudges.
+ *
+ * A stop is emitted only when either scheme's recomputed cap drifts from the
+ * baked value by more than the emit epsilon and the theme doesn't already pin
+ * that stop. Both slots are always emitted together so a one-scheme override
+ * can't silently kill the base default's other slot by cascade-layer order.
+ * The input theme is never mutated. The report re-checks the emitted theme,
+ * whose compiled-cap overrides the resolution core recognizes.
+ */
+export const compile_theme = (theme: Theme): CompiledTheme => {
+ const issues = validate_theme(theme);
+ const resolver = new ThemeResolver(theme);
+
+ const recomputed: Record> = {
+ light: compute_palette_chroma_caps(
+ collect_hues(resolver, 'light'),
+ resolve_lightness_knobs(resolver, 'light'),
+ 'light',
+ resolve_or(resolver, 'hue_shift', 'light', 0),
+ ),
+ dark: compute_palette_chroma_caps(
+ collect_hues(resolver, 'dark'),
+ resolve_lightness_knobs(resolver, 'dark'),
+ 'dark',
+ resolve_or(resolver, 'hue_shift', 'dark', 0),
+ ),
+ };
+
+ const cap_overrides: Array = [];
+ for (const stop of numeric_scale_variants) {
+ if (resolver.pinned(`palette_chroma_${stop}`)) continue; // respect the pin
+ const light_cap = recomputed.light[stop];
+ const dark_cap = recomputed.dark[stop];
+ const light_drift = Math.abs(light_cap - PALETTE_CHROMA_CAPS.light[stop]);
+ const dark_drift = Math.abs(dark_cap - PALETTE_CHROMA_CAPS.dark[stop]);
+ if (light_drift > CAP_EMIT_EPSILON || dark_drift > CAP_EMIT_EPSILON) {
+ cap_overrides.push({
+ name: `palette_chroma_${stop}`,
+ // always emit both slots — the dual-slot rule
+ light: render_chroma_stop_css(stop, 'light', light_cap),
+ dark: render_chroma_stop_css(stop, 'dark', dark_cap),
+ });
+ }
+ }
+
+ const compiled: Theme = {name: theme.name, variables: [...theme.variables, ...cap_overrides]};
+ return {theme: compiled, report: check_theme(compiled), issues};
+};
diff --git a/src/lib/vite_plugin_fuz_css.ts b/src/lib/vite_plugin_fuz_css.ts
index 01a2ad744..30ead7d4b 100644
--- a/src/lib/vite_plugin_fuz_css.ts
+++ b/src/lib/vite_plugin_fuz_css.ts
@@ -5,7 +5,10 @@
* as they're processed, including node_modules dependencies. In bundled
* mode (the default) it also resolves the base reset and theme variables,
* emitting only the rules, elements, and variables the source actually uses.
- * Generates CSS on-demand via virtual module with HMR support.
+ * Generates CSS on-demand via virtual module with HMR support. In dev it
+ * eagerly pre-scans project sources at server startup so the first served
+ * CSS is complete (see `prescan`), and resyncs clients whose HMR socket
+ * connects after a missed update.
*
* @example
* ```ts
@@ -26,9 +29,11 @@
* @module
*/
-import type {Logger as ViteLogger, Plugin, ViteDevServer} from 'vite';
-import {join} from 'node:path';
+import {normalizePath, type Logger as ViteLogger, type Plugin, type ViteDevServer} from 'vite';
+import {isAbsolute, join} from 'node:path';
import {hash_blake3} from '@fuzdev/fuz_util/hash_blake3.ts';
+import {fs_search} from '@fuzdev/fuz_util/fs.ts';
+import {each_concurrent} from '@fuzdev/fuz_util/async.ts';
import {format_diagnostic, CssGenerationError} from './diagnostics.ts';
import {generate_css} from './generate_css.ts';
@@ -113,11 +118,42 @@ const is_ci = !!process.env.CI;
const HMR_DEBOUNCE_MS = 10;
/**
- * Options for the fuz_css Vite plugin.
- * Extends the shared base options (no additional Vite-specific options currently needed).
+ * Concurrency for the dev pre-scan (file read + cache read + extract).
+ * Matches the Gro generator's default — the value controls I/O interleaving,
+ * not CPU parallelism (AST parsing is synchronous on the main thread).
*/
+const PRESCAN_CONCURRENCY = 8;
-export interface VitePluginFuzCssOptions extends CssGeneratorBaseOptions {}
+/** Default directories scanned by the dev pre-scan, relative to the Vite root. */
+const PRESCAN_ROOTS_DEFAULT = ['src'];
+
+/**
+ * Options for the fuz_css Vite plugin.
+ * Extends the shared base options with Vite-specific options.
+ */
+export interface VitePluginFuzCssOptions extends CssGeneratorBaseOptions {
+ /**
+ * Dev-only eager pre-scan of source files at server startup.
+ *
+ * Extraction state accumulates from Vite's transform hook, so without a
+ * pre-scan the first CSS served on a cold start includes only the files
+ * the module graph happened to visit first — classes from modules the SSR
+ * walk never reaches (client-only branches, dynamic imports) are missing
+ * until a corrective HMR update, which is dropped when the browser hasn't
+ * connected yet. The pre-scan extracts every file passing `filter_file`
+ * under the given directories before the virtual module is first served,
+ * so the initial CSS is complete. Warm starts are cheap via the extraction
+ * cache.
+ *
+ * `true` scans `src` under the Vite root; `false` disables; an array gives
+ * custom directories (resolved against the Vite root unless absolute).
+ * node_modules deps are never pre-scanned — they stream in via transform
+ * and are covered by the connect-time resync.
+ *
+ * @default true
+ */
+ prescan?: boolean | Array;
+}
/**
* Creates the fuz_css Vite plugin.
@@ -144,6 +180,7 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug
exclude_elements,
exclude_variables,
deps = default_cache_deps,
+ prescan = true,
} = options;
// Derive include flags from null check
@@ -184,9 +221,13 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug
let hmr_timeout: ReturnType | null = null;
let last_generated_css: string | null = null;
let pending_css: string | null = null; // CSS generated during HMR, reused by load()
+ let last_served_css: string | null = null; // last CSS returned by load(); connect-time resync diffs against it
+ let prescan_promise: Promise | null = null; // load() awaits this so the first served CSS is complete
+ /** Suppresses per-file HMR invalidation while the pre-scan runs (it invalidates once at the end). */
+ let prescan_active = false;
/**
- * Per-file epoch guarding against a deletion (or newer transform) racing an
- * in-flight transform. `transform()` awaits the disk cache read; an `unlink`
+ * Per-file epoch guarding against a deletion (or newer ingest) racing an
+ * in-flight ingest. `ingest_file` awaits the disk cache read; an `unlink`
* arriving during that await deletes the entry, but the resolved await would
* otherwise re-add it and resurrect the deleted file. Captured before the
* await, re-checked after.
@@ -199,8 +240,9 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug
* `render_css` narrows to theme variables once `variable_graph` is loaded.
*
* Filtering here (the old behavior) silently dropped detections from any file
- * transformed before the graph loads — and the graph loads lazily on the first
- * `load()`. In SvelteKit dev, route nodes (e.g. `+page.svelte`) are resolved, and
+ * transformed before the graph loads — and the graph loads asynchronously
+ * (eagerly at `configureServer` in dev, on the first `load()` in build).
+ * In SvelteKit dev, route nodes (e.g. `+page.svelte`) are resolved, and
* thus transformed, during SSR *before* the layout's `virtual:fuz.css` import
* triggers that first `load()`; a theme `var()` used only there was dropped, and
* the cached content hash meant it never re-scanned until the file was edited.
@@ -214,7 +256,8 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug
}
};
- // Bundled CSS resources (loaded lazily on first CSS generation when bundled mode is enabled)
+ // Bundled CSS resources (when bundled mode is enabled: started eagerly at
+ // configureServer in dev, lazily on first load() in build)
let style_rule_index: StyleRuleIndex | null = null;
let variable_graph: VariableDependencyGraph | null = null;
let class_variable_index: CssClassVariableIndex | null = null;
@@ -259,6 +302,14 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug
}
};
+ const log_info = (msg: string): void => {
+ if (logger) {
+ logger.info(msg);
+ } else {
+ console.log(msg);
+ }
+ };
+
/**
* Generates banner-wrapped CSS from the current classes, dispatching
* diagnostics per the `on_error`/`on_warning` settings.
@@ -345,6 +396,48 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug
return `${FUZ_CSS_BANNER}\n\n${final_css}\n\n${FUZ_CSS_BANNER}`;
};
+ /**
+ * Invalidates every served virtual-module variant and pushes the js-update
+ * that makes connected clients refetch. Shared by the debounced HMR path
+ * and the connect-time resync.
+ */
+ const invalidate_and_push = (new_css: string): void => {
+ last_generated_css = new_css;
+ pending_css = new_css; // Store for reuse in load() to avoid regenerating
+
+ // Invalidate every served variant, not just the bare id. The bare
+ // `/__fuz.css` backs the client `
diff --git a/src/routes/docs/themes/+page.svelte b/src/routes/docs/themes/+page.svelte
index a40bc81e6..60a890720 100644
--- a/src/routes/docs/themes/+page.svelte
+++ b/src/routes/docs/themes/+page.svelte
@@ -15,7 +15,7 @@
import {necromancer_theme} from '$lib/themes/necromancer.ts';
import {sunset_ember_theme} from '$lib/themes/sunset_ember.ts';
import {brutalish_theme} from '$lib/themes/brutalish.ts';
- import {create_terminal_theme, terminal_green_theme} from '$lib/themes/terminal.ts';
+ import {terminal_green_theme} from '$lib/themes/terminal.ts';
import type {Theme} from '$lib/theme.ts';
import UnfinishedImplementationWarning from '$routes/docs/UnfinishedImplementationWarning.svelte';
import ThemeEditor from '$routes/ThemeEditor.svelte';
@@ -34,18 +34,11 @@
const theme_state = get_theme_state();
const themes = default_themes.slice();
- // amber runs modestly denser than green phosphor - plain data composition
- const terminal_amber_base = create_terminal_theme(70, 'terminal amber');
- const terminal_amber_theme: Theme = {
- ...terminal_amber_base,
- variables: [...terminal_amber_base.variables, {name: 'scale_factor', light: '0.9'}],
- };
const exemplar_themes = [
necromancer_theme,
sunset_ember_theme,
brutalish_theme,
terminal_green_theme,
- terminal_amber_theme,
];
const editor = new ThemeEditorState([...themes, ...exemplar_themes]);
From 9263f076e8b23c64564a2236b886279d1b42d626 Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Thu, 16 Jul 2026 13:08:09 -0400
Subject: [PATCH 15/33] wip
---
CLAUDE.md | 6 ++--
src/lib/themes/terminal.ts | 51 +++++++++++++++++++++--------
src/routes/docs/themes/+page.svelte | 9 ++---
src/test/theme_check.test.ts | 10 +++---
src/test/themes.test.ts | 2 +-
5 files changed, 50 insertions(+), 28 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 15c0a3739..08b78c7b5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -353,8 +353,10 @@ typography, borders, shading, shadows, layout. See
- `src/lib/themes/` - One module per theme. The registry (base, low/high
contrast) is semantic-tier: intent bindings + levers only, palette hues
untouched. Unregistered exemplars: necromancer, sunset ember, brutalish,
- and `terminal.ts` (a `create_terminal_theme(hue)` factory; terminal green
- = 145); necromancer and terminal are dark-only via `scheme: 'dark'`
+ and `terminal.ts` (the `terminal_theme` flagship — dark mono terminal chrome
+ with a green cast but functional palette — plus a `create_terminal_theme(hue)`
+ factory for pure single-phosphor monochrome terminals); necromancer and
+ terminal are dark-only via `scheme: 'dark'`
- [knobs.ts](src/lib/knobs.ts) - The theme knob catalog: typed metadata
(kind/axis/leverage/tier/bindable/range) for the knob-tier variables, joined
against `default_variables` by name; includes hook knobs like
diff --git a/src/lib/themes/terminal.ts b/src/lib/themes/terminal.ts
index a963f680c..ee34fef86 100644
--- a/src/lib/themes/terminal.ts
+++ b/src/lib/themes/terminal.ts
@@ -1,13 +1,43 @@
import type {Theme} from '../theme.ts';
/**
- * Creates a monochrome terminal theme at any OKLCH hue — green phosphor at
- * 145, amber at 70, cool blue at 250. Every palette slot and the neutral
- * collapse onto the one hue (a palette-tier move, which is why terminal
- * themes live outside the semantic-only registry); expressiveness comes from
- * the ramps doing the work — a slight hue slant blooms the bright end warm and
- * the dim end cool, phosphor-style, so the monochrome reads lit rather than
- * flat. Dark-only via the `scheme` stance, mono type, sharp corners, no shadows.
+ * A practical terminal theme: the dark, monospace, sharp-cornered, flat-depth
+ * terminal chrome with a green-phosphor cast in the surfaces and accent, but
+ * the palette slots keep their own hues so status colors still read — negative
+ * stays red, caution amber, info cyan. The usable twist on the pure
+ * single-phosphor collapse of `create_terminal_theme`. Dark-only via the
+ * `scheme` stance.
+ */
+export const terminal_theme: Theme = {
+ name: 'terminal',
+ scheme: 'dark',
+ variables: [
+ // green-phosphor surfaces and text — the terminal cast, kept low-chroma so
+ // the palette colors read over it
+ {name: 'hue_neutral', light: 'var(--hue_b)'},
+ {name: 'neutral_chroma', light: '0.05'},
+ // links, focus, selection glow phosphor green
+ {name: 'hue_accent', light: 'var(--hue_b)'},
+ // a whisper of phosphor bloom across every ramp
+ {name: 'hue_shift', light: '12'},
+ // mono type everywhere
+ {name: 'font_family_sans', light: 'var(--font_family_mono)'},
+ // sharp: one knob zeroes every radius tier
+ {name: 'radius_scale', light: '0'},
+ // flat: one knob zeroes the whole alpha ramp, button shadows included
+ {name: 'shadow_alpha_scale', light: '0'},
+ ],
+};
+
+/**
+ * Creates a pure single-phosphor terminal at any OKLCH hue — green at 145,
+ * amber at 70, cool blue at 250. Every palette slot and the neutral collapse
+ * onto the one hue (a palette-tier move, which is why terminal themes live
+ * outside the semantic-only registry); expressiveness comes from the ramps
+ * doing the work — a hue slant blooms the bright end warm and the dim end cool,
+ * phosphor-style, so the monochrome reads lit rather than flat. The strict
+ * monochrome counterpart to `terminal_theme`. Dark-only via the `scheme`
+ * stance, mono type, sharp corners, no shadows.
*
* Declared subversions: negative/caution/info all render in the terminal hue
* — status legibility is traded for the monochrome premise.
@@ -21,7 +51,7 @@ export const create_terminal_theme = (
hue: number,
options: {name?: string; hue_shift?: number} = {},
): Theme => {
- const {name = `terminal ${hue}`, hue_shift = 15} = options;
+ const {name = `terminal ${hue}`, hue_shift = 45} = options;
const hue_value = String(hue);
return {
name,
@@ -53,8 +83,3 @@ export const create_terminal_theme = (
],
};
};
-
-/**
- * The classic green phosphor terminal.
- */
-export const terminal_green_theme: Theme = create_terminal_theme(145, {name: 'terminal green'});
diff --git a/src/routes/docs/themes/+page.svelte b/src/routes/docs/themes/+page.svelte
index 60a890720..58dd0d64c 100644
--- a/src/routes/docs/themes/+page.svelte
+++ b/src/routes/docs/themes/+page.svelte
@@ -15,7 +15,7 @@
import {necromancer_theme} from '$lib/themes/necromancer.ts';
import {sunset_ember_theme} from '$lib/themes/sunset_ember.ts';
import {brutalish_theme} from '$lib/themes/brutalish.ts';
- import {terminal_green_theme} from '$lib/themes/terminal.ts';
+ import {terminal_theme} from '$lib/themes/terminal.ts';
import type {Theme} from '$lib/theme.ts';
import UnfinishedImplementationWarning from '$routes/docs/UnfinishedImplementationWarning.svelte';
import ThemeEditor from '$routes/ThemeEditor.svelte';
@@ -34,12 +34,7 @@
const theme_state = get_theme_state();
const themes = default_themes.slice();
- const exemplar_themes = [
- necromancer_theme,
- sunset_ember_theme,
- brutalish_theme,
- terminal_green_theme,
- ];
+ const exemplar_themes = [necromancer_theme, sunset_ember_theme, brutalish_theme, terminal_theme];
const editor = new ThemeEditorState([...themes, ...exemplar_themes]);
diff --git a/src/test/theme_check.test.ts b/src/test/theme_check.test.ts
index 4a2b0428e..e0f0c77e4 100644
--- a/src/test/theme_check.test.ts
+++ b/src/test/theme_check.test.ts
@@ -8,7 +8,7 @@ import {high_contrast_theme} from '$lib/themes/high_contrast.ts';
import {necromancer_theme} from '$lib/themes/necromancer.ts';
import {sunset_ember_theme} from '$lib/themes/sunset_ember.ts';
import {brutalish_theme} from '$lib/themes/brutalish.ts';
-import {terminal_green_theme, create_terminal_theme} from '$lib/themes/terminal.ts';
+import {terminal_theme, create_terminal_theme} from '$lib/themes/terminal.ts';
import {
PALETTE_HUES,
PALETTE_CHROMA_KNOBS,
@@ -28,7 +28,7 @@ describe('validate_theme', () => {
necromancer_theme,
sunset_ember_theme,
brutalish_theme,
- terminal_green_theme,
+ terminal_theme,
create_terminal_theme(70), // amber, exercises the factory
];
for (const theme of themes) {
@@ -213,8 +213,8 @@ describe('check_theme', () => {
assert.isTrue(check_theme(brutalish_theme).ok);
});
- test('terminal green keeps its contrast gates', () => {
- const contrast = check_theme(terminal_green_theme).entries.filter((e) => e.gate === 'contrast');
+ test('terminal keeps its contrast gates', () => {
+ const contrast = check_theme(terminal_theme).entries.filter((e) => e.gate === 'contrast');
assert.isTrue(contrast.every((e) => e.pass));
});
@@ -287,7 +287,7 @@ describe('scheme stance', () => {
});
test('the dark-stanced exemplars pass their contrast gates in both schemes', () => {
- for (const theme of [necromancer_theme, terminal_green_theme]) {
+ for (const theme of [necromancer_theme, terminal_theme]) {
const contrast = check_theme(theme).entries.filter((e) => e.gate === 'contrast');
assert.isTrue(
contrast.every((e) => e.pass),
diff --git a/src/test/themes.test.ts b/src/test/themes.test.ts
index 2b96194c6..5e18f5839 100644
--- a/src/test/themes.test.ts
+++ b/src/test/themes.test.ts
@@ -100,7 +100,7 @@ describe('shipped themes', () => {
assert.include(names, 'necromancer');
assert.include(names, 'sunset ember');
assert.include(names, 'brutalish');
- assert.include(names, 'terminal green');
+ assert.include(names, 'terminal');
});
test('all exemplar variables validate and exist in default_variables', () => {
From a5c6037190047330483f3869e95a3bef8dae2989 Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Wed, 22 Jul 2026 15:01:45 -0400
Subject: [PATCH 16/33] wip
---
CLAUDE.md | 2 +-
package.json | 27 ++++-----------------------
2 files changed, 5 insertions(+), 24 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 08b78c7b5..759a489fd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -476,7 +476,7 @@ Integration: `vite_plugin_examples.test.ts` (skip with
- TypeScript strict mode
- Svelte 5 with runes API (for docs site)
-- Prettier with tabs, 100 char width
+- tsv with tabs, 100 char width
- Node >= 24.14
- Tests in `src/test/` (not co-located)
diff --git a/package.json b/package.json
index 94778f841..47ad77e22 100644
--- a/package.json
+++ b/package.json
@@ -69,12 +69,12 @@
"devDependencies": {
"@changesets/changelog-git": "^0.2.1",
"@fuzdev/blake3_wasm": "^0.1.1",
- "@fuzdev/fuz_code": "^0.47.0",
+ "@fuzdev/fuz_code": "^0.48.0",
"@fuzdev/fuz_ui": "^0.206.7",
"@fuzdev/fuz_util": "^0.65.2",
- "@fuzdev/gro": "^0.206.0",
+ "@fuzdev/gro": "^0.207.0",
"@fuzdev/mdz": "^0.3.0",
- "@ryanatkn/eslint-config": "^0.12.1",
+ "@ryanatkn/eslint-config": "^0.12.2",
"@sveltejs/acorn-typescript": "^1.0.9",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.66.0",
@@ -88,11 +88,9 @@
"eslint-plugin-svelte": "^3.13.1",
"esm-env": "^1.2.2",
"magic-string": "^0.30.21",
- "prettier": "^3.7.4",
- "prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.56.3",
"svelte-check": "^4.4.6",
- "svelte-docinfo": "^0.5.1",
+ "svelte-docinfo": "^0.5.4",
"svelte2tsx": "^0.7.51",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
@@ -102,23 +100,6 @@
"zimmerframe": "^1.1.4",
"zod": "^4.3.6"
},
- "prettier": {
- "plugins": [
- "prettier-plugin-svelte"
- ],
- "useTabs": true,
- "printWidth": 100,
- "singleQuote": true,
- "bracketSpacing": false,
- "overrides": [
- {
- "files": "package.json",
- "options": {
- "useTabs": false
- }
- }
- ]
- },
"sideEffects": [
"**/*.css"
],
From 187375d464e5ddcab231a39de6dff731c0b8d886 Mon Sep 17 00:00:00 2001
From: Ryan Atkinson
Date: Fri, 24 Jul 2026 09:46:17 -0400
Subject: [PATCH 17/33] wip
---
CLAUDE.md | 6 +-
eslint.config.js | 4 +-
examples/vite-preact/src/App.tsx | 57 +-
examples/vite-preact/src/main.tsx | 4 +-
examples/vite-preact/vite.config.ts | 10 +-
examples/vite-react/src/App.tsx | 57 +-
examples/vite-react/src/main.tsx | 4 +-
examples/vite-react/vite.config.ts | 10 +-
examples/vite-solid/src/App.tsx | 57 +-
examples/vite-solid/src/main.tsx | 4 +-
examples/vite-solid/vite.config.ts | 10 +-
examples/vite-svelte/src/App.svelte | 8 +-
examples/vite-svelte/src/main.ts | 4 +-
examples/vite-svelte/vite.config.ts | 10 +-
package-lock.json | 83 +-
package.json | 22 +-
src/app.d.ts | 2 +-
src/app.html | 2 +-
src/lib/bundled_resources.ts | 21 +-
src/lib/class_variable_index.ts | 14 +-
src/lib/css_bundled_resolution.ts | 36 +-
src/lib/css_cache.ts | 30 +-
src/lib/css_class_composites.ts | 48 +-
src/lib/css_class_definitions.ts | 108 +-
src/lib/css_class_extractor.ts | 182 +--
src/lib/css_class_generation.ts | 50 +-
src/lib/css_class_generators.ts | 70 +-
src/lib/css_class_interpreters.ts | 26 +-
src/lib/css_class_resolution.ts | 68 +-
src/lib/css_classes.ts | 8 +-
src/lib/css_literal.ts | 124 +-
src/lib/css_plugin_options.ts | 10 +-
src/lib/css_ruleset_parser.ts | 34 +-
src/lib/deps.ts | 15 +-
src/lib/deps_defaults.ts | 26 +-
src/lib/diagnostics.ts | 4 +-
src/lib/example_class_utilities.ts | 2 +-
src/lib/extract_file_cached.ts | 16 +-
src/lib/gen_fuz_css.ts | 72 +-
src/lib/generate_css.ts | 30 +-
src/lib/knobs.ts | 219 ++--
src/lib/modifiers.ts | 164 +--
src/lib/oklch.ts | 4 +-
src/lib/ramps.ts | 56 +-
src/lib/style_rule_parser.ts | 79 +-
src/lib/theme.gen.css.ts | 10 +-
src/lib/theme.ts | 14 +-
src/lib/theme_check.ts | 164 +--
src/lib/themes.ts | 10 +-
src/lib/themes/base.ts | 4 +-
src/lib/themes/brutalish.ts | 22 +-
src/lib/themes/high_contrast.ts | 8 +-
src/lib/themes/low_contrast.ts | 8 +-
src/lib/themes/necromancer.ts | 36 +-
src/lib/themes/sunset_ember.ts | 16 +-
src/lib/themes/terminal.ts | 56 +-
src/lib/variable.ts | 8 +-
src/lib/variable_data.ts | 36 +-
src/lib/variable_graph.ts | 32 +-
src/lib/variables.ts | 1134 +++++++++--------
src/lib/vite_plugin_fuz_css.ts | 84 +-
src/lib/wcag.ts | 2 +-
src/routes/+layout.svelte | 14 +-
src/routes/+page.svelte | 10 +-
src/routes/FileLink.svelte | 17 +-
src/routes/FontSizeControl.svelte | 6 +-
src/routes/FontWeightControl.svelte | 4 +-
src/routes/Introduction.svelte | 6 +-
src/routes/KnobControl.svelte | 13 +-
src/routes/RampStrip.svelte | 4 +-
src/routes/SourceFileLink.svelte | 2 +-
src/routes/StyleVariableDetail.svelte | 4 +-
src/routes/ThemeEditor.svelte | 20 +-
src/routes/about/+page.svelte | 10 +-
src/routes/docs/+layout.svelte | 14 +-
src/routes/docs/+page.svelte | 4 +-
.../UnfinishedImplementationWarning.svelte | 4 +-
src/routes/docs/api/+page.svelte | 4 +-
.../docs/api/[...module_path]/+page.svelte | 2 +-
src/routes/docs/borders/+page.svelte | 31 +-
src/routes/docs/buttons/+page.svelte | 24 +-
src/routes/docs/chips/+page.svelte | 8 +-
src/routes/docs/classes/+page.svelte | 233 ++--
src/routes/docs/colors/+page.svelte | 36 +-
src/routes/docs/colors/ColorSwatch.svelte | 4 +-
src/routes/docs/colors/ColorSwatchItem.svelte | 10 +-
src/routes/docs/colors/HueSwatch.svelte | 4 +-
src/routes/docs/elements/+page.svelte | 18 +-
src/routes/docs/examples/+page.svelte | 7 +-
src/routes/docs/forms/+page.svelte | 18 +-
src/routes/docs/introduction/+page.svelte | 17 +-
src/routes/docs/layout/+page.svelte | 6 +-
src/routes/docs/menuitem/todo.svelte | 4 +-
src/routes/docs/semantic/+page.svelte | 26 +-
src/routes/docs/shading/+page.svelte | 37 +-
src/routes/docs/shadows/+page.svelte | 15 +-
src/routes/docs/themes/+page.svelte | 71 +-
src/routes/docs/tomes.ts | 38 +-
src/routes/docs/typography/+page.svelte | 10 +-
src/routes/docs/typography/IconSizes.svelte | 2 +-
src/routes/docs/variables/+page.svelte | 11 +-
src/routes/library.ts | 4 +-
src/routes/theme_editor_state.svelte.ts | 34 +-
src/test/class_variable_index.test.ts | 50 +-
...css_bundled_resolution.diagnostics.test.ts | 180 +--
src/test/css_bundled_resolution.test.ts | 298 ++---
.../css_bundled_resolution.variables.test.ts | 206 +--
src/test/css_bundled_resolution_fixtures.ts | 16 +-
src/test/css_cache.test.ts | 191 +--
src/test/css_class_extractor.elements.test.ts | 30 +-
.../css_class_extractor.expressions.test.ts | 44 +-
src/test/css_class_extractor.jsx.test.ts | 8 +-
.../css_class_extractor.locations.test.ts | 25 +-
src/test/css_class_extractor.test.ts | 112 +-
.../css_class_extractor.tracked_vars.test.ts | 86 +-
.../css_class_extractor.typescript.test.ts | 28 +-
.../css_class_extractor.utilities.test.ts | 44 +-
src/test/css_class_extractor_test_helpers.ts | 24 +-
src/test/css_class_generators.test.ts | 41 +-
.../css_class_resolution.literals.test.ts | 62 +-
src/test/css_class_resolution.test.ts | 262 ++--
src/test/css_classes.test.ts | 235 ++--
src/test/css_literal.test.ts | 174 +--
src/test/css_plugin_options.test.ts | 34 +-
.../css_ruleset_parser.generation.test.ts | 66 +-
src/test/css_ruleset_parser.modifiers.test.ts | 82 +-
src/test/css_ruleset_parser.parse.test.ts | 4 +-
src/test/css_ruleset_parser.selectors.test.ts | 8 +-
src/test/css_variable_utils.test.ts | 10 +-
src/test/diagnostics.test.ts | 44 +-
src/test/escape_css_selector.test.ts | 8 +-
src/test/explicit_classes.test.ts | 40 +-
src/test/file_filter.test.ts | 4 +-
.../fixtures/css_classes_fixture.gen.json.ts | 6 +-
src/test/fixtures/mock_deps.ts | 22 +-
src/test/fuz_comments.test.ts | 24 +-
src/test/generate_bundled_css.test.ts | 24 +-
src/test/generate_classes_css.test.ts | 334 ++---
src/test/generate_css.test.ts | 84 +-
src/test/knobs.test.ts | 12 +-
src/test/modified_class_interpreter.test.ts | 186 +--
src/test/modifiers.test.ts | 18 +-
src/test/oklch.test.ts | 10 +-
src/test/ramps.test.ts | 40 +-
src/test/style_rule_parser.custom.test.ts | 4 +-
src/test/style_rule_parser.test.ts | 30 +-
src/test/styles.test.ts | 10 +-
src/test/test_helpers.ts | 48 +-
src/test/theme.test.ts | 46 +-
src/test/theme_check.compile.test.ts | 30 +-
src/test/theme_check.test.ts | 116 +-
src/test/themes.test.ts | 20 +-
src/test/variable.test.ts | 16 +-
src/test/variable_graph.test.ts | 239 ++--
src/test/variables.test.ts | 16 +-
src/test/vite_plugin_examples.test.ts | 24 +-
src/test/vite_plugin_fuz_css.dev.test.ts | 26 +-
svelte.config.js | 30 +-
tsconfig.json | 4 +-
vite.config.ts | 18 +-
160 files changed, 4170 insertions(+), 4003 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 759a489fd..11071a2de 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -263,8 +263,8 @@ your code uses:
```ts
// vite.config.ts
-import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
-export default defineConfig({plugins: [vite_plugin_fuz_css()]});
+import { vite_plugin_fuz_css } from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
+export default defineConfig({ plugins: [vite_plugin_fuz_css()] });
// main.ts (or your SvelteKit root layout)
import 'virtual:fuz.css';
@@ -285,7 +285,7 @@ declare module 'virtual:fuz.css' {
```ts
// src/routes/fuz.gen.css.ts
-import {gen_fuz_css} from '@fuzdev/fuz_css/gen_fuz_css.ts';
+import { gen_fuz_css } from '@fuzdev/fuz_css/gen_fuz_css.ts';
export const gen = gen_fuz_css();
```
diff --git a/eslint.config.js b/eslint.config.js
index c59655eb8..68331fad6 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,6 +1,6 @@
-import {configs, ts_config} from '@ryanatkn/eslint-config';
+import { configs, ts_config } from '@ryanatkn/eslint-config';
ts_config.rules['no-console'] = 1;
// Ignore examples directory - each example has its own tsconfig
-export default [{ignores: ['examples/**']}, ...configs];
+export default [{ ignores: ['examples/**'] }, ...configs];
diff --git a/examples/vite-preact/src/App.tsx b/examples/vite-preact/src/App.tsx
index cf44759da..5af4d7668 100644
--- a/examples/vite-preact/src/App.tsx
+++ b/examples/vite-preact/src/App.tsx
@@ -1,4 +1,4 @@
-import {useState} from 'preact/hooks';
+import { useState } from 'preact/hooks';
// Import from node_modules to verify extraction works for dependencies
import {
@@ -22,7 +22,7 @@ import {
arrayClasses,
objectClasses,
// Comment hint examples
- fromComment,
+ fromComment
} from '@fuzdev/fuz_css/example_class_utilities.ts';
export const App = () => {
@@ -33,7 +33,11 @@ export const App = () => {
fuz_css + Preact
- Utility classes generated on-demand via Vite plugin (docs , source )
+
+ Utility classes generated on-demand via Vite plugin (
+ docs ,{' '}
+ source )
+
{/* Class types */}
@@ -50,7 +54,7 @@ export const App = () => {
Composite classes
.box
-
+
.ellipsis -- this text truncates with ellipsis when it overflows
@@ -59,7 +63,9 @@ export const App = () => {
Literal classes
.opacity:60%
.color:var(--palette_j_50)
-
.box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
+ .box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
@@ -71,28 +77,42 @@ export const App = () => {
Responsive
-
.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens
+
+ .column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens
+
Resize the window to see the layout switch from column to row
-
.min-width(543px):font_size_lg -- arbitrary breakpoint
+
+ .min-width(543px):font_size_lg -- arbitrary breakpoint
+
Interactive
- setCount((c) => c + 1)}>
+ setCount((c) => c + 1)}
+ >
count: {count}
- .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
+
+ .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
+
- setCount(0)}>
+ setCount(0)}
+ >
reset
- .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
+
+ .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
+
@@ -100,7 +120,10 @@ export const App = () => {
{/* Extraction */}
Extraction
- Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning)
+
+ Classes detected via naming conventions, expressions, and comments (examples imported
+ from node_modules to verify dependency scanning)
+
Naming patterns
@@ -144,7 +167,15 @@ export const App = () => {
- This demos a subset of features. See the docs and source code for more.
+
+ This demos a subset of features.
+
+ See the docs and{' '}
+
+ source code
+ {' '}
+ for more.
+
diff --git a/examples/vite-preact/src/main.tsx b/examples/vite-preact/src/main.tsx
index 524746d96..7638a6c24 100644
--- a/examples/vite-preact/src/main.tsx
+++ b/examples/vite-preact/src/main.tsx
@@ -1,6 +1,6 @@
import 'virtual:fuz.css';
-import {render} from 'preact';
-import {App} from './App.tsx';
+import { render } from 'preact';
+import { App } from './App.tsx';
render( , document.getElementById('root')!);
diff --git a/examples/vite-preact/vite.config.ts b/examples/vite-preact/vite.config.ts
index ada615546..438cc4200 100644
--- a/examples/vite-preact/vite.config.ts
+++ b/examples/vite-preact/vite.config.ts
@@ -1,14 +1,14 @@
-import {defineConfig} from 'vite';
+import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import jsx from 'acorn-jsx';
-import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
+import { vite_plugin_fuz_css } from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
export default defineConfig({
plugins: [
// Plugin order doesn't matter for preact (fuz_css uses enforce: 'pre')
preact(),
vite_plugin_fuz_css({
- acorn_plugins: [jsx()],
- }),
- ],
+ acorn_plugins: [jsx()]
+ })
+ ]
});
diff --git a/examples/vite-react/src/App.tsx b/examples/vite-react/src/App.tsx
index e35c8afff..25d3bc246 100644
--- a/examples/vite-react/src/App.tsx
+++ b/examples/vite-react/src/App.tsx
@@ -1,4 +1,4 @@
-import {useState} from 'react';
+import { useState } from 'react';
// Import from node_modules to verify extraction works for dependencies
import {
@@ -22,7 +22,7 @@ import {
arrayClasses,
objectClasses,
// Comment hint examples
- fromComment,
+ fromComment
} from '@fuzdev/fuz_css/example_class_utilities.ts';
export const App = () => {
@@ -33,7 +33,11 @@ export const App = () => {
fuz_css + React
- Utility classes generated on-demand via Vite plugin (docs , source )
+
+ Utility classes generated on-demand via Vite plugin (
+ docs ,{' '}
+ source )
+
{/* Class types */}
@@ -50,7 +54,7 @@ export const App = () => {
Composite classes
.box
-
+
.ellipsis -- this text truncates with ellipsis when it overflows
@@ -59,7 +63,9 @@ export const App = () => {
Literal classes
.opacity:60%
.color:var(--palette_j_50)
-
.box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
+ .box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
@@ -71,28 +77,42 @@ export const App = () => {
Responsive
-
.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens
+
+ .column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens
+
Resize the window to see the layout switch from column to row
-
.min-width(543px):font_size_lg -- arbitrary breakpoint
+
+ .min-width(543px):font_size_lg -- arbitrary breakpoint
+
Interactive
- setCount((c) => c + 1)}>
+ setCount((c) => c + 1)}
+ >
count: {count}
- .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
+
+ .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
+
- setCount(0)}>
+ setCount(0)}
+ >
reset
- .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
+
+ .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
+
@@ -100,7 +120,10 @@ export const App = () => {
{/* Extraction */}
Extraction
- Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning)
+
+ Classes detected via naming conventions, expressions, and comments (examples imported
+ from node_modules to verify dependency scanning)
+
Naming patterns
@@ -144,7 +167,15 @@ export const App = () => {
- This demos a subset of features. See the docs and source code for more.
+
+ This demos a subset of features.
+
+ See the docs and{' '}
+
+ source code
+ {' '}
+ for more.
+
diff --git a/examples/vite-react/src/main.tsx b/examples/vite-react/src/main.tsx
index 6d6b70c21..2f20aa28a 100644
--- a/examples/vite-react/src/main.tsx
+++ b/examples/vite-react/src/main.tsx
@@ -1,6 +1,6 @@
import 'virtual:fuz.css';
-import {createRoot} from 'react-dom/client';
-import {App} from './App.tsx';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.tsx';
createRoot(document.getElementById('root')!).render( );
diff --git a/examples/vite-react/vite.config.ts b/examples/vite-react/vite.config.ts
index 38eddc178..276b4f3b4 100644
--- a/examples/vite-react/vite.config.ts
+++ b/examples/vite-react/vite.config.ts
@@ -1,14 +1,14 @@
-import {defineConfig} from 'vite';
+import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import jsx from 'acorn-jsx';
-import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
+import { vite_plugin_fuz_css } from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
export default defineConfig({
plugins: [
// Plugin order doesn't matter for react (fuz_css uses enforce: 'pre')
react(),
vite_plugin_fuz_css({
- acorn_plugins: [jsx()],
- }),
- ],
+ acorn_plugins: [jsx()]
+ })
+ ]
});
diff --git a/examples/vite-solid/src/App.tsx b/examples/vite-solid/src/App.tsx
index 052297a21..f047d32d8 100644
--- a/examples/vite-solid/src/App.tsx
+++ b/examples/vite-solid/src/App.tsx
@@ -1,4 +1,4 @@
-import {createSignal} from 'solid-js';
+import { createSignal } from 'solid-js';
// Import from node_modules to verify extraction works for dependencies
import {
@@ -22,7 +22,7 @@ import {
arrayClasses,
objectClasses,
// Comment hint examples
- fromComment,
+ fromComment
} from '@fuzdev/fuz_css/example_class_utilities.ts';
export const App = () => {
@@ -33,7 +33,11 @@ export const App = () => {
fuz_css + Solid
- Utility classes generated on-demand via Vite plugin (docs , source )
+
+ Utility classes generated on-demand via Vite plugin (
+ docs ,{' '}
+ source )
+
{/* Class types */}
@@ -50,7 +54,7 @@ export const App = () => {
Composite classes
.box
-
+
.ellipsis -- this text truncates with ellipsis when it overflows
@@ -59,7 +63,9 @@ export const App = () => {
Literal classes
.opacity:60%
.color:var(--palette_j_50)
-
.box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
+ .box-shadow:0~4px~8px~rgb(0,0,0,0.2) (~ encodes spaces)
+
@@ -71,28 +77,42 @@ export const App = () => {
Responsive
-
.column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens
+
+ .column .gap_md on mobile, .md:flex-direction:row .md:gap_lg on medium+ screens
+
Resize the window to see the layout switch from column to row
-
.min-width(543px):font_size_lg -- arbitrary breakpoint
+
+ .min-width(543px):font_size_lg -- arbitrary breakpoint
+
Interactive
- setCount((c) => c + 1)}>
+ setCount((c) => c + 1)}
+ >
count: {count()}
- .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
+
+ .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
+
- setCount(0)}>
+ setCount(0)}
+ >
reset
- .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
+
+ .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
+
@@ -100,7 +120,10 @@ export const App = () => {
{/* Extraction */}
Extraction
- Classes detected via naming conventions, expressions, and comments (examples imported from node_modules to verify dependency scanning)
+
+ Classes detected via naming conventions, expressions, and comments (examples imported
+ from node_modules to verify dependency scanning)
+
Naming patterns
@@ -144,7 +167,15 @@ export const App = () => {
- This demos a subset of features. See the docs and source code for more.
+
+ This demos a subset of features.
+
+ See the docs and{' '}
+
+ source code
+ {' '}
+ for more.
+
diff --git a/examples/vite-solid/src/main.tsx b/examples/vite-solid/src/main.tsx
index 7b3815d60..6a2b5d30a 100644
--- a/examples/vite-solid/src/main.tsx
+++ b/examples/vite-solid/src/main.tsx
@@ -1,6 +1,6 @@
import 'virtual:fuz.css';
-import {render} from 'solid-js/web';
-import {App} from './App.tsx';
+import { render } from 'solid-js/web';
+import { App } from './App.tsx';
render(() => , document.getElementById('root')!);
diff --git a/examples/vite-solid/vite.config.ts b/examples/vite-solid/vite.config.ts
index de604732c..b36c015d6 100644
--- a/examples/vite-solid/vite.config.ts
+++ b/examples/vite-solid/vite.config.ts
@@ -1,14 +1,14 @@
-import {defineConfig} from 'vite';
+import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
import jsx from 'acorn-jsx';
-import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
+import { vite_plugin_fuz_css } from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
export default defineConfig({
plugins: [
// fuz_css must be listed first for solid (enforce: 'pre' alone isn't sufficient)
vite_plugin_fuz_css({
- acorn_plugins: [jsx()],
+ acorn_plugins: [jsx()]
}),
- solid(),
- ],
+ solid()
+ ]
});
diff --git a/examples/vite-svelte/src/App.svelte b/examples/vite-svelte/src/App.svelte
index 0cab482bb..d92d472b1 100644
--- a/examples/vite-svelte/src/App.svelte
+++ b/examples/vite-svelte/src/App.svelte
@@ -21,7 +21,7 @@
arrayClasses,
objectClasses,
// Comment hint examples
- fromComment,
+ fromComment
} from '@fuzdev/fuz_css/example_class_utilities.ts';
let count = $state(0);
@@ -96,8 +96,7 @@
count: {count}
.hover:border_b_50 .hover:outline_b_50 .active:border_d_50
- .active:outline_d_50 .hover:border_b_50 .hover:outline_b_50 .active:border_d_50 .active:outline_d_50
@@ -108,8 +107,7 @@
reset
.hover:border_g_50 .hover:outline_g_50 .active:border_h_50
- .active:outline_h_50 .hover:border_g_50 .hover:outline_g_50 .active:border_h_50 .active:outline_h_50
diff --git a/examples/vite-svelte/src/main.ts b/examples/vite-svelte/src/main.ts
index 86ce8fd08..c5f73e540 100644
--- a/examples/vite-svelte/src/main.ts
+++ b/examples/vite-svelte/src/main.ts
@@ -1,6 +1,6 @@
import 'virtual:fuz.css';
-import {mount} from 'svelte';
+import { mount } from 'svelte';
import App from './App.svelte';
-mount(App, {target: document.getElementById('root')!});
+mount(App, { target: document.getElementById('root')! });
diff --git a/examples/vite-svelte/vite.config.ts b/examples/vite-svelte/vite.config.ts
index 2268fd808..ee788d189 100644
--- a/examples/vite-svelte/vite.config.ts
+++ b/examples/vite-svelte/vite.config.ts
@@ -1,11 +1,11 @@
-import {defineConfig} from 'vite';
-import {svelte} from '@sveltejs/vite-plugin-svelte';
-import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
+import { defineConfig } from 'vite';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { vite_plugin_fuz_css } from '@fuzdev/fuz_css/vite_plugin_fuz_css.ts';
export default defineConfig({
plugins: [
// fuz_css must be listed first for svelte (enforce: 'pre' alone isn't sufficient)
vite_plugin_fuz_css(),
- svelte(),
- ],
+ svelte()
+ ]
});
diff --git a/package-lock.json b/package-lock.json
index 4b6d59c39..f51365307 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,12 +11,12 @@
"devDependencies": {
"@changesets/changelog-git": "^0.2.1",
"@fuzdev/blake3_wasm": "^0.1.1",
- "@fuzdev/fuz_code": "^0.47.0",
+ "@fuzdev/fuz_code": "^0.48.0",
"@fuzdev/fuz_ui": "^0.206.7",
"@fuzdev/fuz_util": "^0.65.2",
- "@fuzdev/gro": "^0.206.0",
+ "@fuzdev/gro": "^0.205.1",
"@fuzdev/mdz": "^0.3.0",
- "@ryanatkn/eslint-config": "^0.12.1",
+ "@ryanatkn/eslint-config": "^0.12.2",
"@sveltejs/acorn-typescript": "^1.0.9",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.66.0",
@@ -30,11 +30,11 @@
"eslint-plugin-svelte": "^3.13.1",
"esm-env": "^1.2.2",
"magic-string": "^0.30.21",
- "prettier": "^3.7.4",
- "prettier-plugin-svelte": "^3.4.1",
+ "prettier": "^3.9.6",
+ "prettier-plugin-svelte": "^4.1.1",
"svelte": "^5.56.3",
"svelte-check": "^4.4.6",
- "svelte-docinfo": "^0.5.1",
+ "svelte-docinfo": "^0.5.4",
"svelte2tsx": "^0.7.51",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
@@ -755,9 +755,9 @@
}
},
"node_modules/@fuzdev/fuz_code": {
- "version": "0.47.0",
- "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.47.0.tgz",
- "integrity": "sha512-ZVA4gswHigMDp62F2JKIjIJsgglF14vzgAI48QBafusoj5JIUvOEGYvajXhEMQdb2cobOQjZUxbaaST7UqGKXg==",
+ "version": "0.48.0",
+ "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.48.0.tgz",
+ "integrity": "sha512-uzcAkElwu73UULfd2Vij0U9oCL0ySZEYHd8CxYGSglxLZc4JzTIxDpFkY67glPBmAVR36CgyXpdWGGCTW3bF7A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -920,17 +920,18 @@
}
},
"node_modules/@fuzdev/gro": {
- "version": "0.206.0",
- "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.206.0.tgz",
- "integrity": "sha512-11nkTAEWDOvkgzynRChQS+bj3tc7R0NduB+eHLXzXZJqz66nc/oop8Ttvz0UkjTBPX/zzgtU8QuDDNyA7buf5A==",
+ "version": "0.205.1",
+ "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.205.1.tgz",
+ "integrity": "sha512-o5WBXWSH/UlN1BsDnpBNIi0WHR7lj7UFGltH/JctSP7oSB2ojvmoXD5OLCEeVdRVcDaSQKl48xcXKUwHKqVSmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@fuzdev/tsv_wasm": "^0.1.0",
"chokidar": "^5.0.0",
"dotenv": "^17.2.3",
"esm-env": "^1.2.2",
"oxc-parser": "^0.99.0",
+ "prettier": "^3.7.4",
+ "prettier-plugin-svelte": "^3.5.1",
"ts-blank-space": "^0.6.2",
"tslib": "^2.8.1"
},
@@ -985,6 +986,17 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@fuzdev/gro/node_modules/prettier-plugin-svelte": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz",
+ "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "prettier": "^3.0.0",
+ "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
+ }
+ },
"node_modules/@fuzdev/gro/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
@@ -1031,22 +1043,6 @@
}
}
},
- "node_modules/@fuzdev/tsv_wasm": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@fuzdev/tsv_wasm/-/tsv_wasm-0.1.0.tgz",
- "integrity": "sha512-hlZxdL1c5jiMyLnMv3NWONxNR64JChNekxlsKEJxXvgr/bmcRsgF1XuyabOTtppW9C+9HUvCpklJgJeU4aZbKw==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "tsv": "cli.js"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://www.ryanatkn.com/funding"
- }
- },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1830,9 +1826,9 @@
]
},
"node_modules/@ryanatkn/eslint-config": {
- "version": "0.12.1",
- "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.12.1.tgz",
- "integrity": "sha512-rur/pPhaaGyBlFjsPs1wIjyHnlPI5UT617TibNSAq6B3fialaI/YqsaDx/DMU5yRXlyhU6esYm+aOmXnckOZOA==",
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.12.2.tgz",
+ "integrity": "sha512-p5yIpW0ka5Gev3VM2a7X8kEnVqORaF8JccU79D7BxEL+erTMlsn0aFWtMaxPXaIxBwL3yqJ+42uFc4AKDIn/aA==",
"dev": true,
"license": "Unlicense",
"dependencies": {
@@ -3822,9 +3818,9 @@
}
},
"node_modules/prettier": {
- "version": "3.7.4",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
- "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3838,14 +3834,17 @@
}
},
"node_modules/prettier-plugin-svelte": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
- "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-4.1.1.tgz",
+ "integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
"peerDependencies": {
"prettier": "^3.0.0",
- "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
+ "svelte": "^5.0.0"
}
},
"node_modules/punycode": {
@@ -4115,9 +4114,9 @@
}
},
"node_modules/svelte-docinfo": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/svelte-docinfo/-/svelte-docinfo-0.5.1.tgz",
- "integrity": "sha512-/+THIRDddz5QeTBBmgK69lL9jXUS5jdL08VAD8BV1JT0kYEjPK9T+wWHCVpcLX09NSyPGCX2H6jgBMF1XbspEg==",
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/svelte-docinfo/-/svelte-docinfo-0.5.4.tgz",
+ "integrity": "sha512-9XONWxu//kRmThbJBpDuNu3jSP6lSPgMy4T9G2GYHWWrHDiowJFn8ov5acvhBvAogBinBATylXk1TsvyQlI0iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 47ad77e22..d13e250b6 100644
--- a/package.json
+++ b/package.json
@@ -72,7 +72,7 @@
"@fuzdev/fuz_code": "^0.48.0",
"@fuzdev/fuz_ui": "^0.206.7",
"@fuzdev/fuz_util": "^0.65.2",
- "@fuzdev/gro": "^0.207.0",
+ "@fuzdev/gro": "^0.205.1",
"@fuzdev/mdz": "^0.3.0",
"@ryanatkn/eslint-config": "^0.12.2",
"@sveltejs/acorn-typescript": "^1.0.9",
@@ -88,6 +88,8 @@
"eslint-plugin-svelte": "^3.13.1",
"esm-env": "^1.2.2",
"magic-string": "^0.30.21",
+ "prettier": "^3.9.6",
+ "prettier-plugin-svelte": "^4.1.1",
"svelte": "^5.56.3",
"svelte-check": "^4.4.6",
"svelte-docinfo": "^0.5.4",
@@ -100,6 +102,24 @@
"zimmerframe": "^1.1.4",
"zod": "^4.3.6"
},
+ "prettier": {
+ "plugins": [
+ "prettier-plugin-svelte"
+ ],
+ "useTabs": true,
+ "printWidth": 100,
+ "singleQuote": true,
+ "bracketSpacing": true,
+ "trailingComma": "none",
+ "overrides": [
+ {
+ "files": "package.json",
+ "options": {
+ "useTabs": false
+ }
+ }
+ ]
+ },
"sideEffects": [
"**/*.css"
],
diff --git a/src/app.d.ts b/src/app.d.ts
index a917e8990..e7cd05ab3 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -7,7 +7,7 @@ declare module 'virtual:fuz.css' {
export default css;
}
declare module 'virtual:pkg.json' {
- import type {PkgJson} from '@fuzdev/fuz_util/pkg_json.ts';
+ import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.ts';
const pkg_json: PkgJson;
export default pkg_json;
}
diff --git a/src/app.html b/src/app.html
index e3061bc77..06d95cd29 100644
--- a/src/app.html
+++ b/src/app.html
@@ -11,7 +11,7 @@
((c) =>
c === 'dark' || (c !== 'light' && matchMedia('(prefers-color-scheme:dark)').matches)
? 'dark'
- : 'light')(localStorage.getItem('fuz:color-scheme')),
+ : 'light')(localStorage.getItem('fuz:color-scheme'))
);
diff --git a/src/lib/bundled_resources.ts b/src/lib/bundled_resources.ts
index 3571d3d92..0af765ef2 100644
--- a/src/lib/bundled_resources.ts
+++ b/src/lib/bundled_resources.ts
@@ -14,13 +14,16 @@ import {
type StyleRuleIndex,
load_style_rule_index,
create_style_rule_index,
- load_default_style_css,
+ load_default_style_css
} from './style_rule_parser.ts';
-import {type VariableDependencyGraph, build_variable_graph_from_options} from './variable_graph.ts';
-import {type CssClassVariableIndex, build_class_variable_index} from './class_variable_index.ts';
-import type {CssClassDefinition} from './css_class_generation.ts';
-import type {BaseCssOption, VariablesOption} from './css_plugin_options.ts';
-import type {CacheDeps} from './deps.ts';
+import {
+ type VariableDependencyGraph,
+ build_variable_graph_from_options
+} from './variable_graph.ts';
+import { type CssClassVariableIndex, build_class_variable_index } from './class_variable_index.ts';
+import type { CssClassDefinition } from './css_class_generation.ts';
+import type { BaseCssOption, VariablesOption } from './css_plugin_options.ts';
+import type { CacheDeps } from './deps.ts';
/**
* Bundled CSS resources needed to emit base styles and theme variables.
@@ -49,9 +52,9 @@ export interface CreateBundledResourcesOptions {
* generators' prior behavior.
*/
export const create_bundled_resources = async (
- options: CreateBundledResourcesOptions,
+ options: CreateBundledResourcesOptions
): Promise => {
- const {base_css, variables, class_definitions, deps} = options;
+ const { base_css, variables, class_definitions, deps } = options;
let style_rule_index: StyleRuleIndex;
if (typeof base_css === 'string') {
@@ -69,6 +72,6 @@ export const create_bundled_resources = async (
return {
style_rule_index,
variable_graph: build_variable_graph_from_options(variables),
- class_variable_index: build_class_variable_index(class_definitions),
+ class_variable_index: build_class_variable_index(class_definitions)
};
};
diff --git a/src/lib/class_variable_index.ts b/src/lib/class_variable_index.ts
index e98054502..cac7614c5 100644
--- a/src/lib/class_variable_index.ts
+++ b/src/lib/class_variable_index.ts
@@ -8,8 +8,8 @@
* @module
*/
-import type {CssClassDefinition} from './css_class_generation.ts';
-import {extract_css_variables} from './css_variable_utils.ts';
+import type { CssClassDefinition } from './css_class_generation.ts';
+import { extract_css_variables } from './css_variable_utils.ts';
/**
* Index mapping class names to their CSS variable dependencies.
@@ -26,7 +26,7 @@ export interface CssClassVariableIndex {
* @returns `CssClassVariableIndex` with variable lookups
*/
export const build_class_variable_index = (
- definitions: Record,
+ definitions: Record
): CssClassVariableIndex => {
const by_class: Map> = new Map();
@@ -39,7 +39,7 @@ export const build_class_variable_index = (
}
}
- return {by_class};
+ return { by_class };
};
/**
@@ -80,7 +80,7 @@ const extract_variables_from_definition = (definition: CssClassDefinition): Set<
*/
export const get_class_variables = (
index: CssClassVariableIndex,
- class_name: string,
+ class_name: string
): Set | null => index.by_class.get(class_name) ?? null;
/**
@@ -92,7 +92,7 @@ export const get_class_variables = (
*/
export const collect_class_variables = (
index: CssClassVariableIndex,
- class_names: Iterable,
+ class_names: Iterable
): Set => {
const variables: Set = new Set();
@@ -117,7 +117,7 @@ export const collect_class_variables = (
*/
export const get_classes_using_variable = (
index: CssClassVariableIndex,
- variable_name: string,
+ variable_name: string
): Array => {
const classes: Array = [];
diff --git a/src/lib/css_bundled_resolution.ts b/src/lib/css_bundled_resolution.ts
index 62da3df09..8336d4a0a 100644
--- a/src/lib/css_bundled_resolution.ts
+++ b/src/lib/css_bundled_resolution.ts
@@ -13,22 +13,22 @@
* @module
*/
-import type {GenerationDiagnostic} from './diagnostics.ts';
+import type { GenerationDiagnostic } from './diagnostics.ts';
import {
type StyleRuleIndex,
get_matching_rules,
generate_base_css,
- collect_rule_variables,
+ collect_rule_variables
} from './style_rule_parser.ts';
import {
type VariableDependencyGraph,
resolve_variables_transitive,
generate_theme_css,
get_all_variable_names,
- find_similar_variable,
+ find_similar_variable
} from './variable_graph.ts';
-import {type CssClassVariableIndex, collect_class_variables} from './class_variable_index.ts';
-import {FUZ_LAYER_ORDER_STATEMENT} from './theme.ts';
+import { type CssClassVariableIndex, collect_class_variables } from './class_variable_index.ts';
+import { FUZ_LAYER_ORDER_STATEMENT } from './theme.ts';
/**
* Threshold for string similarity to suggest typo corrections.
@@ -203,7 +203,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
exclude_elements: raw_exclude_elements,
exclude_variables: raw_exclude_variables,
explicit_elements,
- explicit_variables,
+ explicit_variables
} = options;
const diagnostics: Array = [];
@@ -242,7 +242,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
included_rule_indices = get_matching_rules(
style_rule_index,
included_elements,
- detected_classes,
+ detected_classes
);
}
@@ -258,7 +258,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
suggestion:
'Element will use browser defaults. Add to additional_elements if intentional.',
identifier: element,
- locations: null,
+ locations: null
});
}
}
@@ -282,7 +282,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
? `Check spelling. Similar element: ${similar}`
: 'Element has no fuz_css styles. Remove from @fuz-elements or add custom styles.',
identifier: element,
- locations: null,
+ locations: null
});
}
}
@@ -305,7 +305,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
? `Check spelling. Similar variable: ${similar}`
: 'Variable does not exist in the theme. Remove from @fuz-variables.',
identifier: variable,
- locations: null,
+ locations: null
});
}
}
@@ -371,7 +371,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
}" is referenced by included styles but excluded via exclude_variables - references to it will be undefined`,
suggestion: 'Remove it from exclude_variables, or define the variable elsewhere.',
identifier: v,
- locations: null,
+ locations: null
});
}
all_variables.delete(v);
@@ -390,7 +390,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
message: warning,
suggestion: null,
identifier: 'variable_resolution',
- locations: null,
+ locations: null
});
}
@@ -405,14 +405,14 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
message: `CSS variable "--${name}" not found - did you mean "--${similar}"?`,
suggestion: `Check spelling. Similar theme variable: --${similar}`,
identifier: `var(--${name})`,
- locations: null,
+ locations: null
});
}
// Variables not similar to any theme variable are assumed to be user-defined
}
// Step 5: Generate theme CSS
- const {light_css, dark_css} = generate_theme_css(variable_graph, resolved_variables);
+ const { light_css, dark_css } = generate_theme_css(variable_graph, resolved_variables);
const theme_css = [light_css, dark_css].filter(Boolean).join('\n\n');
// Step 6: Generate base CSS from matched rules
@@ -425,7 +425,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
elements: [...included_elements],
included_rules: included_rule_indices.size,
total_rules: style_rule_index.rules.length,
- variable_count: resolved_variables.size,
+ variable_count: resolved_variables.size
}
: undefined;
@@ -436,7 +436,7 @@ export const resolve_css = (options: CssResolutionOptions): CssResolutionResult
included_rule_indices,
included_elements,
diagnostics,
- stats,
+ stats
};
};
@@ -463,9 +463,9 @@ export interface GenerateBundledCssOptions {
export const generate_bundled_css = (
result: CssResolutionResult,
utility_css: string,
- options: GenerateBundledCssOptions = {},
+ options: GenerateBundledCssOptions = {}
): string => {
- const {include_theme = true, include_base = true, include_utilities = true} = options;
+ const { include_theme = true, include_base = true, include_utilities = true } = options;
const parts: Array = [];
diff --git a/src/lib/css_cache.ts b/src/lib/css_cache.ts
index 06ce3d0fb..d3689622b 100644
--- a/src/lib/css_cache.ts
+++ b/src/lib/css_cache.ts
@@ -7,12 +7,12 @@
* @module
*/
-import {join} from 'node:path';
-import {hash_insecure} from '@fuzdev/fuz_util/hash.ts';
+import { join } from 'node:path';
+import { hash_insecure } from '@fuzdev/fuz_util/hash.ts';
-import type {SourceLocation, ExtractionDiagnostic} from './diagnostics.ts';
-import type {ExtractionData} from './css_class_extractor.ts';
-import type {CacheDeps} from './deps.ts';
+import type { SourceLocation, ExtractionDiagnostic } from './diagnostics.ts';
+import type { ExtractionData } from './css_class_extractor.ts';
+import type { CacheDeps } from './deps.ts';
/**
* Default cache directory relative to project root.
@@ -73,7 +73,7 @@ export interface CachedExtraction {
export const get_cache_path = (
source_path: string,
cache_dir: string,
- project_root: string,
+ project_root: string
): string => {
if (!source_path.startsWith(project_root)) {
throw new Error(`Source path "${source_path}" is not under project root "${project_root}"`);
@@ -94,7 +94,7 @@ export const get_cache_path = (
export const get_file_cache_path = (
file_id: string,
cache_dir: string,
- project_root: string,
+ project_root: string
): string => {
const is_internal = file_id.startsWith(project_root);
return is_internal
@@ -112,10 +112,10 @@ export const get_file_cache_path = (
*/
export const load_cached_extraction = async (
deps: CacheDeps,
- cache_path: string,
+ cache_path: string
): Promise => {
try {
- const r = await deps.read_text({path: cache_path});
+ const r = await deps.read_text({ path: cache_path });
if (!r.ok) return null;
const cached = JSON.parse(r.value) as CachedExtraction;
@@ -147,7 +147,7 @@ export const save_cached_extraction = async (
deps: CacheDeps,
cache_path: string,
content_hash: string,
- extraction: ExtractionData,
+ extraction: ExtractionData
): Promise => {
// Convert to null if empty to save allocation on load
const classes_array =
@@ -179,10 +179,10 @@ export const save_cached_extraction = async (
diagnostics: diagnostics_array,
elements: elements_array,
explicit_elements: explicit_elements_array,
- explicit_variables: explicit_variables_array,
+ explicit_variables: explicit_variables_array
};
- await deps.write_text_atomic({path: cache_path, content: JSON.stringify(data)});
+ await deps.write_text_atomic({ path: cache_path, content: JSON.stringify(data) });
};
/**
@@ -194,9 +194,9 @@ export const save_cached_extraction = async (
*/
export const delete_cached_extraction = async (
deps: CacheDeps,
- cache_path: string,
+ cache_path: string
): Promise => {
- await deps.unlink({path: cache_path});
+ await deps.unlink({ path: cache_path });
};
/**
@@ -211,5 +211,5 @@ export const from_cached_extraction = (cached: CachedExtraction): ExtractionData
diagnostics: cached.diagnostics,
elements: cached.elements ? new Set(cached.elements) : null,
explicit_elements: cached.explicit_elements ? new Set(cached.explicit_elements) : null,
- explicit_variables: cached.explicit_variables ? new Set(cached.explicit_variables) : null,
+ explicit_variables: cached.explicit_variables ? new Set(cached.explicit_variables) : null
});
diff --git a/src/lib/css_class_composites.ts b/src/lib/css_class_composites.ts
index 176a92b4f..903b31e6e 100644
--- a/src/lib/css_class_composites.ts
+++ b/src/lib/css_class_composites.ts
@@ -1,4 +1,4 @@
-import type {CssClassDefinition} from './css_class_generation.ts';
+import type { CssClassDefinition } from './css_class_generation.ts';
export const css_class_composites: Record = {
pixelated: {
@@ -7,10 +7,10 @@ export const css_class_composites: Record `var(--line_height_${v})`,
+ (v) => `var(--line_height_${v})`
),
...generate_property_classes(
'font-size',
font_size_variants,
- (v) => `var(--font_size_${v}); --font_size: var(--font_size_${v})`,
+ (v) => `var(--font_size_${v}); --font_size: var(--font_size_${v})`
),
...generate_property_classes(
'font-size',
icon_size_variants,
(v) => `var(--icon_size_${v}); --font_size: var(--icon_size_${v})`,
- 'icon_size',
+ 'icon_size'
),
/*
@@ -86,22 +86,22 @@ export const css_class_definitions: Record `var(--text_${v}); --text_color: var(--text_${v})`,
- 'text',
+ 'text'
),
// Shade scale (tinted backgrounds)
...generate_property_classes(
'background-color',
shade_scale_variants,
(v) => `var(--shade_${v})`,
- 'shade',
+ 'shade'
),
// Hue classes
...generate_classes(
(letter: string) => ({
name: `hue_${letter}`,
- css: `--hue: var(--hue_${letter});`,
+ css: `--hue: var(--hue_${letter});`
}),
- palette_variants,
+ palette_variants
),
// Palette intensity classes (text color)
...generate_classes(
@@ -109,63 +109,63 @@ export const css_class_definitions: Record ({
name: `bg_${letter}_${intensity}`,
- css: `background-color: var(--palette_${letter}_${intensity});`,
+ css: `background-color: var(--palette_${letter}_${intensity});`
}),
palette_variants,
- intensity_variants,
+ intensity_variants
),
// Intent intensity classes (text color) - meaning-first twins of the palette classes
...generate_classes(
(intent: string, intensity: string) => ({
name: `${intent}_${intensity}`,
- css: `color: var(--${intent}_${intensity}); --text_color: var(--${intent}_${intensity});`,
+ css: `color: var(--${intent}_${intensity}); --text_color: var(--${intent}_${intensity});`
}),
intent_variants,
- intensity_variants,
+ intensity_variants
),
// Intent intensity classes (background color)
...generate_classes(
(intent: string, intensity: string) => ({
name: `bg_${intent}_${intensity}`,
- css: `background-color: var(--${intent}_${intensity});`,
+ css: `background-color: var(--${intent}_${intensity});`
}),
intent_variants,
- intensity_variants,
+ intensity_variants
),
// Darken/lighten overlays (non-adaptive, alpha-based)
...generate_property_classes(
'background-color',
darken_lighten_variants,
(v) => `var(--darken_${v})`,
- 'darken',
+ 'darken'
),
...generate_property_classes(
'background-color',
darken_lighten_variants,
(v) => `var(--lighten_${v})`,
- 'lighten',
+ 'lighten'
),
// Adaptive alpha overlays (fg = toward foreground, bg = toward background)
...generate_property_classes(
'background-color',
darken_lighten_variants,
(v) => `var(--fg_${v})`,
- 'fg',
+ 'fg'
),
...generate_property_classes(
'background-color',
darken_lighten_variants,
(v) => `var(--bg_${v})`,
- 'bg',
+ 'bg'
),
/*
@@ -179,7 +179,7 @@ export const css_class_definitions: Record `var(--border_color_${v}); --border_color: var(--border_color_${v})`,
- 'border_color',
+ 'border_color'
),
// Border colors using palette hue + intensity (sets both property and contextual variable);
// the letter alone implies the palette - the alpha-ramp family is `border_color_NN`
@@ -188,10 +188,10 @@ export const css_class_definitions: Record `var(--shade_${v})`),
@@ -202,29 +202,29 @@ export const css_class_definitions: Record `var(--border_width_${v})`,
+ (v) => `var(--border_width_${v})`
),
...generate_property_classes(
'outline-width',
border_width_variants.map(String),
- (v) => `var(--border_width_${v})`,
+ (v) => `var(--border_width_${v})`
),
- outline_width_focus: {declaration: 'outline-width: var(--outline_width_focus);'},
- outline_width_active: {declaration: 'outline-width: var(--outline_width_active);'},
+ outline_width_focus: { declaration: 'outline-width: var(--outline_width_focus);' },
+ outline_width_active: { declaration: 'outline-width: var(--outline_width_active);' },
...generate_property_classes(
'border-radius',
border_radius_variants,
- (v) => `var(--border_radius_${v})`,
+ (v) => `var(--border_radius_${v})`
),
...generate_border_radius_corners(border_radius_variants, (v) => `var(--border_radius_${v})`),
@@ -238,31 +238,31 @@ export const css_class_definitions: Record ({
name: `shadow_color_${value}`,
- css: `--shadow_color: var(--shadow_color_${value});`,
+ css: `--shadow_color: var(--shadow_color_${value});`
}),
- shadow_semantic_values,
+ shadow_semantic_values
),
...generate_classes(
(alpha: string) => ({
name: `shadow_alpha_${alpha}`,
- css: `--shadow_alpha: var(--shadow_alpha_${alpha});`,
+ css: `--shadow_alpha: var(--shadow_alpha_${alpha});`
}),
- shadow_alpha_variants,
+ shadow_alpha_variants
),
// Shadow colors using palette hue + intensity (sets contextual variable only);
// the letter alone implies the palette - semantic colors are `shadow_color_umbra` etc.
...generate_classes(
(letter: string, intensity: string) => ({
name: `shadow_${letter}_${intensity}`,
- css: `--shadow_color: var(--palette_${letter}_${intensity});`,
+ css: `--shadow_color: var(--palette_${letter}_${intensity});`
}),
palette_variants,
- intensity_variants,
+ intensity_variants
),
/*
@@ -276,30 +276,30 @@ export const css_class_definitions: Record ({
name: `width_atmost_${v}`,
- css: `width: 100%; max-width: var(--distance_${v});`,
+ css: `width: 100%; max-width: var(--distance_${v});`
}),
- distance_variants,
+ distance_variants
),
...generate_classes(
(v: string) => ({
name: `width_atleast_${v}`,
- css: `width: 100%; min-width: var(--distance_${v});`,
+ css: `width: 100%; min-width: var(--distance_${v});`
}),
- distance_variants,
+ distance_variants
),
...generate_classes(
(v: string) => ({
name: `height_atmost_${v}`,
- css: `height: 100%; max-height: var(--distance_${v});`,
+ css: `height: 100%; max-height: var(--distance_${v});`
}),
- distance_variants,
+ distance_variants
),
...generate_classes(
(v: string) => ({
name: `height_atleast_${v}`,
- css: `height: 100%; min-height: var(--distance_${v});`,
+ css: `height: 100%; min-height: var(--distance_${v});`
}),
- distance_variants,
+ distance_variants
),
...generate_property_classes('top', space_variants, format_spacing_value),
@@ -312,7 +312,7 @@ export const css_class_definitions: Record | undefined,
- include_defaults: boolean,
+ include_defaults: boolean
): Record => {
if (!include_defaults && !user_definitions) {
throw new Error('class_definitions is required when include_default_classes is false');
}
return include_defaults
? user_definitions
- ? {...css_class_definitions, ...user_definitions}
+ ? { ...css_class_definitions, ...user_definitions }
: css_class_definitions
: user_definitions!;
};
diff --git a/src/lib/css_class_extractor.ts b/src/lib/css_class_extractor.ts
index cf8da509a..3c5ff7d12 100644
--- a/src/lib/css_class_extractor.ts
+++ b/src/lib/css_class_extractor.ts
@@ -15,13 +15,13 @@
* @module
*/
-import {parse as parse_svelte, type AST} from 'svelte/compiler';
-import {walk, type Visitors} from 'zimmerframe';
-import {Parser, type Node} from 'acorn';
-import {tsPlugin} from '@sveltejs/acorn-typescript';
-import {to_error_message} from '@fuzdev/fuz_util/error.ts';
+import { parse as parse_svelte, type AST } from 'svelte/compiler';
+import { walk, type Visitors } from 'zimmerframe';
+import { Parser, type Node } from 'acorn';
+import { tsPlugin } from '@sveltejs/acorn-typescript';
+import { to_error_message } from '@fuzdev/fuz_util/error.ts';
-import {type SourceLocation, type ExtractionDiagnostic} from './diagnostics.ts';
+import { type SourceLocation, type ExtractionDiagnostic } from './diagnostics.ts';
//
// Types
@@ -153,7 +153,7 @@ export class SourceIndex {
if (this.line_starts[mid]! <= offset) low = mid;
else high = mid - 1;
}
- return {file, line: low + 1, column: offset - this.line_starts[low]! + 1};
+ return { file, line: low + 1, column: offset - this.line_starts[low]! + 1 };
}
}
@@ -168,7 +168,7 @@ export class SourceIndex {
const add_class_with_location = (
classes: Map>,
class_name: string,
- location: SourceLocation,
+ location: SourceLocation
): void => {
const existing = classes.get(class_name);
if (existing) {
@@ -184,7 +184,7 @@ const CLASS_UTILITY_FUNCTIONS = new Set([
'cn', // common alias (shadcn/ui convention)
'classNames', // classnames package
'classnames', // lowercase variant
- 'cx', // emotion and other libs
+ 'cx' // emotion and other libs
]);
// Svelte 5 runes that wrap expressions we should extract from
@@ -235,7 +235,7 @@ const create_walk_state = (file: string, source_index: SourceIndex | null): Walk
diagnostics: [],
elements: new Set(),
explicit_elements: new Set(),
- explicit_variables: new Set(),
+ explicit_variables: new Set()
});
/**
@@ -248,7 +248,7 @@ const finalize_extraction_result = (state: WalkState): ExtractionResult => ({
diagnostics: state.diagnostics.length > 0 ? state.diagnostics : null,
elements: state.elements.size > 0 ? state.elements : null,
explicit_elements: state.explicit_elements.size > 0 ? state.explicit_elements : null,
- explicit_variables: state.explicit_variables.size > 0 ? state.explicit_variables : null,
+ explicit_variables: state.explicit_variables.size > 0 ? state.explicit_variables : null
});
/**
@@ -262,7 +262,7 @@ const empty_extraction_result = (diagnostics: Array): Extr
diagnostics: diagnostics.length > 0 ? diagnostics : null,
elements: null,
explicit_elements: null,
- explicit_variables: null,
+ explicit_variables: null
});
/**
@@ -280,7 +280,7 @@ const location_from_offset = (state: WalkState, offset: number): SourceLocation
return state.source_index.get_location(offset, state.file);
}
// Fallback for script context (should have line/column from AST)
- return {file: state.file, line: 1, column: 1};
+ return { file: state.file, line: 1, column: 1 };
};
/**
@@ -295,7 +295,7 @@ export const extract_from_svelte = (source: string, file = ''): Extract
let ast: AST.Root;
try {
- ast = parse_svelte(source, {modern: true});
+ ast = parse_svelte(source, { modern: true });
} catch (err) {
return empty_extraction_result([
{
@@ -303,8 +303,8 @@ export const extract_from_svelte = (source: string, file = ''): Extract
level: 'warning',
message: `Failed to parse Svelte file: ${to_error_message(err, 'unknown error')}`,
suggestion: 'Check for syntax errors in the file',
- location: {file, line: 1, column: 1},
- },
+ location: { file, line: 1, column: 1 }
+ }
]);
}
@@ -340,11 +340,11 @@ export const extract_from_svelte = (source: string, file = ''): Extract
* and returns the space-separated list of names.
*/
const create_fuz_comment_parser = (
- tag: string,
+ tag: string
): ((
content: string,
location: SourceLocation,
- diagnostics: Array,
+ diagnostics: Array
) => Array | null) => {
const pattern = new RegExp(`^\\s*@fuz-${tag}(:?)\\s+(.+?)\\s*$`);
return (content, location, diagnostics) => {
@@ -356,7 +356,7 @@ const create_fuz_comment_parser = (
level: 'warning',
message: `@fuz-${tag}: colon is unnecessary`,
suggestion: `Use @fuz-${tag} without the colon`,
- location,
+ location
});
}
return match[2]!.split(/\s+/).filter(Boolean);
@@ -406,9 +406,9 @@ const process_fuz_comment = (content: string, location: SourceLocation, state: W
*/
const extract_fuz_comments_from_svelte = (ast: AST.Root, state: WalkState): void => {
const visitors: Visitors = {
- Comment(node, {state}) {
+ Comment(node, { state }) {
process_fuz_comment(node.data, location_from_offset(state, node.start), state);
- },
+ }
};
walk(ast.fragment as AST.SvelteNode, state, visitors);
};
@@ -421,11 +421,11 @@ const extract_fuz_comments_from_svelte = (ast: AST.Root, state: WalkState): void
const extract_fuz_comments_from_script = (
script: AST.Script,
source: string,
- state: WalkState,
+ state: WalkState
): void => {
// Extract the script source using start/end positions
// Svelte AST nodes have start/end but the TypeScript types don't include them
- const content = script.content as unknown as {start: number; end: number};
+ const content = script.content as unknown as { start: number; end: number };
const script_source = source.slice(content.start, content.end);
// Calculate the line offset for proper location reporting
@@ -436,7 +436,7 @@ const extract_fuz_comments_from_script = (
}
// Collect comments via acorn's onComment callback
- const comments: Array<{value: string; loc: {start: {line: number; column: number}}}> = [];
+ const comments: Array<{ value: string; loc: { start: { line: number; column: number } } }> = [];
try {
const parser = Parser.extend(tsPlugin());
@@ -449,12 +449,12 @@ const extract_fuz_comments_from_script = (
text: string,
_start: number,
_end: number,
- startLoc?: {line: number; column: number},
+ startLoc?: { line: number; column: number }
) => {
if (startLoc) {
- comments.push({value: text, loc: {start: startLoc}});
+ comments.push({ value: text, loc: { start: startLoc } });
}
- },
+ }
});
} catch {
// If parsing fails, we can't extract comments
@@ -466,7 +466,7 @@ const extract_fuz_comments_from_script = (
const location: SourceLocation = {
file: state.file,
line: comment.loc.start.line + line_offset,
- column: comment.loc.start.column + 1,
+ column: comment.loc.start.column + 1
};
process_fuz_comment(comment.value, location, state);
}
@@ -483,10 +483,10 @@ const extract_fuz_comments_from_script = (
export const extract_from_ts = (
source: string,
file = '',
- acorn_plugins?: Array,
+ acorn_plugins?: Array
): ExtractionResult => {
// Collect comments via acorn's onComment callback
- const comments: Array<{value: string; loc: {start: {line: number; column: number}}}> = [];
+ const comments: Array<{ value: string; loc: { start: { line: number; column: number } } }> = [];
let ast: Node;
try {
@@ -502,12 +502,12 @@ export const extract_from_ts = (
text: string,
_start: number,
_end: number,
- startLoc?: {line: number; column: number},
+ startLoc?: { line: number; column: number }
) => {
if (startLoc) {
- comments.push({value: text, loc: {start: startLoc}});
+ comments.push({ value: text, loc: { start: startLoc } });
}
- },
+ }
});
} catch (err) {
return empty_extraction_result([
@@ -516,8 +516,8 @@ export const extract_from_ts = (
level: 'warning',
message: `Failed to parse TypeScript/JS file: ${to_error_message(err, 'unknown error')}`,
suggestion: 'Check for syntax errors in the file',
- location: {file, line: 1, column: 1},
- },
+ location: { file, line: 1, column: 1 }
+ }
]);
}
@@ -528,7 +528,7 @@ export const extract_from_ts = (
const location: SourceLocation = {
file,
line: comment.loc.start.line,
- column: comment.loc.start.column + 1,
+ column: comment.loc.start.column + 1
};
process_fuz_comment(comment.value, location, state);
}
@@ -554,7 +554,7 @@ export const extract_from_ts = (
*/
export const extract_css_classes = (
source: string,
- options: ExtractCssClassesOptions = {},
+ options: ExtractCssClassesOptions = {}
): Set | null => {
const result = extract_css_classes_with_locations(source, options);
return result.classes ? new Set(result.classes.keys()) : null;
@@ -570,9 +570,9 @@ export const extract_css_classes = (
*/
export const extract_css_classes_with_locations = (
source: string,
- options: ExtractCssClassesOptions = {},
+ options: ExtractCssClassesOptions = {}
): ExtractionResult => {
- const {filename, acorn_plugins} = options;
+ const { filename, acorn_plugins } = options;
const ext = filename ? filename.slice(filename.lastIndexOf('.')) : '';
const file = filename ?? '';
@@ -598,56 +598,56 @@ export const extract_css_classes_with_locations = (
const walk_template = (fragment: AST.Fragment, state: WalkState): void => {
const visitors: Visitors = {
// Handle regular elements - capture element name
- RegularElement(node, {state, next}) {
+ RegularElement(node, { state, next }) {
// Track HTML element names (not components, not svelte:* elements)
state.elements.add(node.name);
process_element_attributes(node.attributes, state);
next();
},
// SvelteElement has dynamic tag, can't statically determine element
- SvelteElement(node, {state, next}) {
+ SvelteElement(node, { state, next }) {
process_element_attributes(node.attributes, state);
next();
},
// Components are PascalCase - don't add to elements
- SvelteComponent(node, {state, next}) {
+ SvelteComponent(node, { state, next }) {
process_element_attributes(node.attributes, state);
next();
},
- Component(node, {state, next}) {
+ Component(node, { state, next }) {
process_element_attributes(node.attributes, state);
next();
},
- SvelteFragment(node, {state, next}) {
+ SvelteFragment(node, { state, next }) {
process_element_attributes(node.attributes, state);
next();
},
// SvelteHead injects children into document - walk its fragment
- SvelteHead(_node, {next}) {
+ SvelteHead(_node, { next }) {
next();
},
// TitleElement is a special element type for inside svelte:head
- TitleElement(node, {state, next}) {
+ TitleElement(node, { state, next }) {
state.elements.add(node.name);
process_element_attributes(node.attributes, state);
next();
},
// SlotElement is - can have fallback content with elements
- SlotElement(node, {state, next}) {
+ SlotElement(node, { state, next }) {
state.elements.add(node.name);
process_element_attributes(node.attributes, state);
next();
},
// SvelteBody, SvelteWindow, SvelteDocument don't have element children
- SvelteBody(_node, {next}) {
+ SvelteBody(_node, { next }) {
next();
},
- SvelteWindow(_node, {next}) {
+ SvelteWindow(_node, { next }) {
next();
},
- SvelteDocument(_node, {next}) {
+ SvelteDocument(_node, { next }) {
next();
- },
+ }
};
walk(fragment as AST.SvelteNode, state, visitors);
@@ -658,7 +658,7 @@ const walk_template = (fragment: AST.Fragment, state: WalkState): void => {
*/
const process_element_attributes = (
attributes: Array,
- state: WalkState,
+ state: WalkState
): void => {
for (const attr of attributes) {
// Handle class attribute
@@ -716,20 +716,23 @@ const extract_from_attribute_value = (value: AST.Attribute['value'], state: Walk
const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void => {
// Get location from the expression's start offset
const get_location = (): SourceLocation => {
- const node = expr as unknown as {start?: number; loc?: {start: {line: number; column: number}}};
+ const node = expr as unknown as {
+ start?: number;
+ loc?: { start: { line: number; column: number } };
+ };
if (node.loc) {
- return {file: state.file, line: node.loc.start.line, column: node.loc.start.column + 1};
+ return { file: state.file, line: node.loc.start.line, column: node.loc.start.column + 1 };
}
if (node.start !== undefined) {
return location_from_offset(state, node.start);
}
- return {file: state.file, line: 1, column: 1};
+ return { file: state.file, line: 1, column: 1 };
};
switch (expr.type) {
case 'Literal': {
// String literal
- const node = expr as unknown as {value: unknown};
+ const node = expr as unknown as { value: unknown };
if (typeof node.value === 'string') {
const location = get_location();
for (const cls of node.value.split(/\s+/).filter(Boolean)) {
@@ -744,9 +747,9 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
// Only extract tokens that are whitespace-bounded (not fragments like `icon-` from `icon-${size}`)
const node = expr as unknown as {
quasis: Array<{
- value: {raw: string};
+ value: { raw: string };
start?: number;
- loc?: {start: {line: number; column: number}};
+ loc?: { start: { line: number; column: number } };
}>;
expressions: Array;
};
@@ -758,7 +761,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
if (!quasi.value.raw) continue;
const location = quasi.loc
- ? {file: state.file, line: quasi.loc.start.line, column: quasi.loc.start.column + 1}
+ ? { file: state.file, line: quasi.loc.start.line, column: quasi.loc.start.column + 1 }
: quasi.start !== undefined
? location_from_offset(state, quasi.start)
: get_location();
@@ -816,7 +819,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'ArrayExpression': {
// Array: extract from each element
- const node = expr as unknown as {elements: Array};
+ const node = expr as unknown as { elements: Array };
for (const element of node.elements) {
if (element) {
extract_from_expression(element as AST.SvelteNode, state);
@@ -833,15 +836,15 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
key: unknown;
computed: boolean;
start?: number;
- loc?: {start: {line: number; column: number}};
+ loc?: { start: { line: number; column: number } };
}>;
};
for (const prop of node.properties) {
if (prop.type === 'Property' && !prop.computed) {
// Non-computed key - extract the key as a class name
- const key = prop.key as {type: string; name?: string; value?: string};
+ const key = prop.key as { type: string; name?: string; value?: string };
const location = prop.loc
- ? {file: state.file, line: prop.loc.start.line, column: prop.loc.start.column + 1}
+ ? { file: state.file, line: prop.loc.start.line, column: prop.loc.start.column + 1 }
: prop.start !== undefined
? location_from_offset(state, prop.start)
: get_location();
@@ -860,7 +863,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'ConditionalExpression': {
// Ternary: extract from both branches
- const node = expr as unknown as {consequent: unknown; alternate: unknown};
+ const node = expr as unknown as { consequent: unknown; alternate: unknown };
extract_from_expression(node.consequent as AST.SvelteNode, state);
extract_from_expression(node.alternate as AST.SvelteNode, state);
break;
@@ -868,7 +871,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'LogicalExpression': {
// && or ||: extract from both sides
- const node = expr as unknown as {left: unknown; right: unknown};
+ const node = expr as unknown as { left: unknown; right: unknown };
extract_from_expression(node.left as AST.SvelteNode, state);
extract_from_expression(node.right as AST.SvelteNode, state);
break;
@@ -877,7 +880,12 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'CallExpression': {
// Function call: check if it's a class utility function or Svelte rune
const node = expr as unknown as {
- callee: {type: string; name?: string; object?: {name?: string}; property?: {name?: string}};
+ callee: {
+ type: string;
+ name?: string;
+ object?: { name?: string };
+ property?: { name?: string };
+ };
arguments: Array;
};
@@ -905,7 +913,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'Identifier': {
// Variable reference: track it for later extraction
- const node = expr as unknown as {name: string};
+ const node = expr as unknown as { name: string };
if (state.in_class_context) {
state.tracked_vars.add(node.name);
}
@@ -920,7 +928,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'TaggedTemplateExpression': {
// Tagged template literal like css`class-name` - extract from the template
- const node = expr as unknown as {quasi: unknown};
+ const node = expr as unknown as { quasi: unknown };
if (node.quasi) {
extract_from_expression(node.quasi as AST.SvelteNode, state);
}
@@ -931,7 +939,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'FunctionExpression': {
// Function expression: extract from the body
// Handles $derived.by(() => cond ? 'a' : 'b')
- const node = expr as unknown as {body: unknown};
+ const node = expr as unknown as { body: unknown };
if (node.body) {
extract_from_expression(node.body as AST.SvelteNode, state);
}
@@ -940,7 +948,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'BlockStatement': {
// Block statement: walk all statements looking for return statements
- const node = expr as unknown as {body: Array};
+ const node = expr as unknown as { body: Array };
for (const stmt of node.body) {
extract_from_expression(stmt as AST.SvelteNode, state);
}
@@ -949,7 +957,7 @@ const extract_from_expression = (expr: AST.SvelteNode, state: WalkState): void =
case 'ReturnStatement': {
// Return statement: extract from the argument
- const node = expr as unknown as {argument: unknown};
+ const node = expr as unknown as { argument: unknown };
if (node.argument) {
extract_from_expression(node.argument as AST.SvelteNode, state);
}
@@ -974,14 +982,14 @@ const extract_from_jsx_attribute_value = (value: unknown, state: WalkState): voi
type: string;
value?: string;
expression?: unknown;
- loc?: {start: {line: number; column: number}};
+ loc?: { start: { line: number; column: number } };
};
if (node.type === 'Literal' && typeof node.value === 'string') {
// Static className="foo bar"
const location: SourceLocation = node.loc
- ? {file: state.file, line: node.loc.start.line, column: node.loc.start.column + 1}
- : {file: state.file, line: 1, column: 1};
+ ? { file: state.file, line: node.loc.start.line, column: node.loc.start.column + 1 }
+ : { file: state.file, line: 1, column: 1 };
for (const cls of node.value.split(/\s+/).filter(Boolean)) {
add_class(state, cls, location);
}
@@ -1000,8 +1008,8 @@ const extract_from_jsx_attribute_value = (value: unknown, state: WalkState): voi
const walk_script = (ast: unknown, state: WalkState): void => {
const visitors: Visitors = {
// Variable declarations
- VariableDeclarator(node, {state, next}) {
- const declarator = node as unknown as {id: {type: string; name: string}; init: unknown};
+ VariableDeclarator(node, { state, next }) {
+ const declarator = node as unknown as { id: { type: string; name: string }; init: unknown };
if (declarator.id.type === 'Identifier') {
const name = declarator.id.name;
// Check if variable name matches class pattern
@@ -1020,9 +1028,9 @@ const walk_script = (ast: unknown, state: WalkState): void => {
},
// Call expressions (for clsx/cn calls outside of class attributes)
- CallExpression(node, {state, next}) {
+ CallExpression(node, { state, next }) {
const call = node as unknown as {
- callee: {type: string; name?: string};
+ callee: { type: string; name?: string };
arguments: Array;
};
if (call.callee.type === 'Identifier' && CLASS_UTILITY_FUNCTIONS.has(call.callee.name!)) {
@@ -1034,9 +1042,9 @@ const walk_script = (ast: unknown, state: WalkState): void => {
},
// Object properties with class-related keys
- Property(node, {state, next}) {
+ Property(node, { state, next }) {
const prop = node as unknown as {
- key: {type: string; name?: string; value?: string};
+ key: { type: string; name?: string; value?: string };
value: unknown;
computed: boolean;
};
@@ -1060,13 +1068,13 @@ const walk_script = (ast: unknown, state: WalkState): void => {
// JSX elements (React/Preact/Solid) - extract class-related attributes and element names
// These are only present when acorn-jsx plugin is used
- JSXElement(node, {state, next}) {
+ JSXElement(node, { state, next }) {
const element = node as unknown as {
openingElement: {
- name?: {type: string; name?: string};
+ name?: { type: string; name?: string };
attributes: Array<{
type: string;
- name?: {type: string; name?: string};
+ name?: { type: string; name?: string };
value?: unknown;
}>;
};
@@ -1092,7 +1100,7 @@ const walk_script = (ast: unknown, state: WalkState): void => {
}
}
next();
- },
+ }
};
walk(ast as Node, state, visitors);
@@ -1115,8 +1123,8 @@ const extract_from_tracked_vars = (ast: AST.Root, state: WalkState): void => {
*/
const extract_from_tracked_vars_in_script = (ast: Node, state: WalkState): void => {
const find_visitors: Visitors = {
- VariableDeclarator(node, {state}) {
- const declarator = node as unknown as {id: {type: string; name: string}; init: unknown};
+ VariableDeclarator(node, { state }) {
+ const declarator = node as unknown as { id: { type: string; name: string }; init: unknown };
if (
declarator.id.type === 'Identifier' &&
state.tracked_vars.has(declarator.id.name) &&
@@ -1125,7 +1133,7 @@ const extract_from_tracked_vars_in_script = (ast: Node, state: WalkState): void
) {
extract_from_expression(declarator.init as AST.SvelteNode, state);
}
- },
+ }
};
walk(ast, state, find_visitors);
diff --git a/src/lib/css_class_generation.ts b/src/lib/css_class_generation.ts
index cb9fb1edc..979eb3f35 100644
--- a/src/lib/css_class_generation.ts
+++ b/src/lib/css_class_generation.ts
@@ -7,23 +7,23 @@
* @module
*/
-import type {Logger} from '@fuzdev/fuz_util/log.ts';
-import {to_error_message} from '@fuzdev/fuz_util/error.ts';
+import type { Logger } from '@fuzdev/fuz_util/log.ts';
+import { to_error_message } from '@fuzdev/fuz_util/error.ts';
import {
type SourceLocation,
type InterpreterDiagnostic,
type GenerationDiagnostic,
- create_generation_diagnostic,
+ create_generation_diagnostic
} from './diagnostics.ts';
import {
parse_ruleset,
is_single_selector_ruleset,
- ruleset_contains_class,
+ ruleset_contains_class
} from './css_ruleset_parser.ts';
-import {resolve_class_definition} from './css_class_resolution.ts';
-import {get_modifier} from './modifiers.ts';
-import {extract_css_variables} from './css_variable_utils.ts';
+import { resolve_class_definition } from './css_class_resolution.ts';
+import { get_modifier } from './modifiers.ts';
+import { extract_css_variables } from './css_variable_utils.ts';
//
// CSS Utilities
@@ -74,7 +74,7 @@ export const extract_primary_property = (declaration: string): string | null =>
const get_sort_index = (
class_name: string,
indexes: Map,
- property_to_last_index: Map,
+ property_to_last_index: Map
): number => {
const direct = indexes.get(class_name);
if (direct !== undefined) return direct;
@@ -145,9 +145,7 @@ export interface CssClassDefinitionRuleset extends CssClassDefinitionBase {
/** Static definitions (not interpreter-based). */
export type CssClassDefinitionStatic =
- | CssClassDefinitionComposition
- | CssClassDefinitionDeclaration
- | CssClassDefinitionRuleset;
+ CssClassDefinitionComposition | CssClassDefinitionDeclaration | CssClassDefinitionRuleset;
/** Full union including interpreters. */
export type CssClassDefinition = CssClassDefinitionStatic | CssClassDefinitionInterpreter;
@@ -206,7 +204,7 @@ export interface GenerateClassesCssOptions {
}
export const generate_classes_css = (
- options: GenerateClassesCssOptions,
+ options: GenerateClassesCssOptions
): GenerateClassesCssResult => {
const {
class_names,
@@ -215,7 +213,7 @@ export const generate_classes_css = (
css_properties,
log,
class_locations,
- explicit_classes,
+ explicit_classes
} = options;
const interpreter_diagnostics: Array = [];
const diagnostics: Array = [];
@@ -226,7 +224,7 @@ export const generate_classes_css = (
log,
diagnostics: interpreter_diagnostics,
class_definitions,
- css_properties,
+ css_properties
};
// Build index maps in a single pass:
@@ -258,7 +256,7 @@ export const generate_classes_css = (
const order_b = get_state_modifier_order(b);
if (order_a !== order_b) return order_a - order_b;
return a.localeCompare(b); // alphabetic tiebreaker for stable sort
- },
+ }
);
let css = '';
@@ -281,10 +279,10 @@ export const generate_classes_css = (
// or just a declaration
if (result.includes('{')) {
// Full ruleset - use as-is
- v = {ruleset: result, comment: interpreter.comment};
+ v = { ruleset: result, comment: interpreter.comment };
} else {
// Simple declaration
- v = {declaration: result, comment: interpreter.comment};
+ v = { declaration: result, comment: interpreter.comment };
}
break;
}
@@ -302,7 +300,7 @@ export const generate_classes_css = (
const is_css_property_error =
diag.level === 'error' && diag.message.startsWith('Unknown CSS property');
const level = is_css_property_error && !is_explicit ? 'warning' : diag.level;
- diagnostics.push(create_generation_diagnostic({...diag, level}, locations));
+ diagnostics.push(create_generation_diagnostic({ ...diag, level }, locations));
}
if (!v) {
@@ -316,13 +314,13 @@ export const generate_classes_css = (
message: 'No matching class definition found',
identifier: c,
suggestion: 'Check spelling or add a custom class definition',
- locations,
+ locations
});
}
continue;
}
- const {comment} = v;
+ const { comment } = v;
if (comment) {
const trimmed = comment.trim();
@@ -346,7 +344,7 @@ export const generate_classes_css = (
message: resolution_result.error.message,
identifier: c,
suggestion: resolution_result.error.suggestion,
- locations: class_locations?.get(c) ?? null,
+ locations: class_locations?.get(c) ?? null
});
continue;
}
@@ -372,7 +370,7 @@ export const generate_classes_css = (
message: `Ruleset "${c}" is empty`,
identifier: c,
suggestion: `Add CSS rules or remove the empty ruleset definition`,
- locations: class_locations?.get(c) ?? null,
+ locations: class_locations?.get(c) ?? null
});
continue;
}
@@ -397,7 +395,7 @@ export const generate_classes_css = (
message: `Ruleset "${c}" has no selectors containing ".${c}"`,
identifier: c,
suggestion: `Ensure at least one selector uses ".${c}" so the class works when applied`,
- locations: class_locations?.get(c) ?? null,
+ locations: class_locations?.get(c) ?? null
});
}
@@ -422,7 +420,7 @@ export const generate_classes_css = (
suggestion: `Convert to: { declaration: '${parsed.rules[0]?.declarations
.replace(/\s+/g, ' ')
.trim()}' }`,
- locations: class_locations?.get(c) ?? null,
+ locations: class_locations?.get(c) ?? null
});
}
} catch (e) {
@@ -434,12 +432,12 @@ export const generate_classes_css = (
message: `Failed to parse ruleset for "${c}": ${error_message}`,
identifier: c,
suggestion: 'Check for CSS syntax errors in the ruleset',
- locations: class_locations?.get(c) ?? null,
+ locations: class_locations?.get(c) ?? null
});
}
}
// Note: Interpreted types are converted to declaration above, so no else clause needed
}
- return {css, diagnostics, variables_used};
+ return { css, diagnostics, variables_used };
};
diff --git a/src/lib/css_class_generators.ts b/src/lib/css_class_generators.ts
index abc23238d..8458bdf01 100644
--- a/src/lib/css_class_generators.ts
+++ b/src/lib/css_class_generators.ts
@@ -1,4 +1,4 @@
-import type {CssClassDefinition} from './css_class_generation.ts';
+import type { CssClassDefinition } from './css_class_generation.ts';
export type GeneratedClassResult = {
name: string;
@@ -42,20 +42,20 @@ export const generate_classes = (
template: ClassTemplateFn,
values: Iterable,
secondary?: Iterable,
- tertiary?: Iterable,
+ tertiary?: Iterable
): Record => {
const result: Record = {};
if (!secondary) {
for (const v1 of values) {
const generated = (template as any)(v1);
- if (generated) result[generated.name] = {declaration: generated.css};
+ if (generated) result[generated.name] = { declaration: generated.css };
}
} else if (!tertiary) {
for (const v1 of values) {
for (const v2 of secondary) {
const generated = (template as any)(v1, v2);
- if (generated) result[generated.name] = {declaration: generated.css};
+ if (generated) result[generated.name] = { declaration: generated.css };
}
}
} else {
@@ -63,7 +63,7 @@ export const generate_classes = (
for (const v2 of secondary) {
for (const v3 of tertiary) {
const generated = (template as any)(v1, v2, v3);
- if (generated) result[generated.name] = {declaration: generated.css};
+ if (generated) result[generated.name] = { declaration: generated.css };
}
}
}
@@ -122,14 +122,14 @@ export const generate_property_classes = (
property: string,
values: Iterable,
formatter?: (value: string) => string,
- prefix: string = format_variable_name(property),
+ prefix: string = format_variable_name(property)
): Record => {
return generate_classes(
(value: string) => ({
name: `${prefix}_${format_variable_name(value)}`,
- css: `${property}: ${formatter?.(value) ?? value};`,
+ css: `${property}: ${formatter?.(value) ?? value};`
}),
- values,
+ values
);
};
@@ -144,7 +144,7 @@ export const generate_property_classes = (
export const generate_directional_classes = (
property: string,
values: Iterable,
- formatter?: (v: string) => string,
+ formatter?: (v: string) => string
): Record => {
const prefix = property[0]; // 'm' for margin, 'p' for padding
@@ -153,38 +153,38 @@ export const generate_directional_classes = (
const formatted = formatter?.(value) ?? value;
// Map variants to their configurations
- const configs: Record = {
- '': {name: `${prefix}_${format_variable_name(value)}`, css: `${property}: ${formatted};`},
+ const configs: Record = {
+ '': { name: `${prefix}_${format_variable_name(value)}`, css: `${property}: ${formatted};` },
t: {
name: `${prefix}t_${format_variable_name(value)}`,
- css: `${property}-top: ${formatted};`,
+ css: `${property}-top: ${formatted};`
},
r: {
name: `${prefix}r_${format_variable_name(value)}`,
- css: `${property}-right: ${formatted};`,
+ css: `${property}-right: ${formatted};`
},
b: {
name: `${prefix}b_${format_variable_name(value)}`,
- css: `${property}-bottom: ${formatted};`,
+ css: `${property}-bottom: ${formatted};`
},
l: {
name: `${prefix}l_${format_variable_name(value)}`,
- css: `${property}-left: ${formatted};`,
+ css: `${property}-left: ${formatted};`
},
x: {
name: `${prefix}x_${format_variable_name(value)}`,
- css: `${property}-left: ${formatted};\t${property}-right: ${formatted};`,
+ css: `${property}-left: ${formatted};\t${property}-right: ${formatted};`
},
y: {
name: `${prefix}y_${format_variable_name(value)}`,
- css: `${property}-top: ${formatted};\t${property}-bottom: ${formatted};`,
- },
+ css: `${property}-top: ${formatted};\t${property}-bottom: ${formatted};`
+ }
};
return configs[variant] || null;
},
['', 't', 'r', 'b', 'l', 'x', 'y'],
- values,
+ values
);
};
@@ -197,22 +197,22 @@ export const generate_directional_classes = (
*/
export const generate_border_radius_corners = (
values: Iterable,
- formatter?: (value: string) => string,
+ formatter?: (value: string) => string
): Record => {
const corners = [
- {prop: 'border-top-left-radius', name: 'border_top_left_radius'},
- {prop: 'border-top-right-radius', name: 'border_top_right_radius'},
- {prop: 'border-bottom-left-radius', name: 'border_bottom_left_radius'},
- {prop: 'border-bottom-right-radius', name: 'border_bottom_right_radius'},
+ { prop: 'border-top-left-radius', name: 'border_top_left_radius' },
+ { prop: 'border-top-right-radius', name: 'border_top_right_radius' },
+ { prop: 'border-bottom-left-radius', name: 'border_bottom_left_radius' },
+ { prop: 'border-bottom-right-radius', name: 'border_bottom_right_radius' }
];
return generate_classes(
(corner: (typeof corners)[0], value: string) => ({
name: `${corner.name}_${format_variable_name(value)}`,
- css: `${corner.prop}: ${formatter?.(value) ?? value};`,
+ css: `${corner.prop}: ${formatter?.(value) ?? value};`
}),
corners,
- values,
+ values
);
};
@@ -226,15 +226,15 @@ export const generate_border_radius_corners = (
*/
export const generate_shadow_classes = (
sizes: Iterable,
- alpha_mapping: Record,
+ alpha_mapping: Record
): Record => {
const shadow_types = [
- {prefix: 'shadow', var_prefix: 'shadow'},
- {prefix: 'shadow_top', var_prefix: 'shadow_top'},
- {prefix: 'shadow_bottom', var_prefix: 'shadow_bottom'},
- {prefix: 'shadow_inset', var_prefix: 'shadow_inset'},
- {prefix: 'shadow_inset_top', var_prefix: 'shadow_inset_top'},
- {prefix: 'shadow_inset_bottom', var_prefix: 'shadow_inset_bottom'},
+ { prefix: 'shadow', var_prefix: 'shadow' },
+ { prefix: 'shadow_top', var_prefix: 'shadow_top' },
+ { prefix: 'shadow_bottom', var_prefix: 'shadow_bottom' },
+ { prefix: 'shadow_inset', var_prefix: 'shadow_inset' },
+ { prefix: 'shadow_inset_top', var_prefix: 'shadow_inset_top' },
+ { prefix: 'shadow_inset_bottom', var_prefix: 'shadow_inset_bottom' }
];
return generate_classes(
@@ -244,9 +244,9 @@ export const generate_shadow_classes = (
size
}) color-mix(in oklab, var(--shadow_color, var(--shadow_color_umbra)) var(--shadow_alpha, var(--shadow_alpha_${
alpha_mapping[size]
- })), transparent);`,
+ })), transparent);`
}),
shadow_types,
- sizes,
+ sizes
);
};
diff --git a/src/lib/css_class_interpreters.ts b/src/lib/css_class_interpreters.ts
index f419e5802..e3fb2bb97 100644
--- a/src/lib/css_class_interpreters.ts
+++ b/src/lib/css_class_interpreters.ts
@@ -1,14 +1,14 @@
-import {escape_css_selector, type CssClassDefinitionInterpreter} from './css_class_generation.ts';
+import { escape_css_selector, type CssClassDefinitionInterpreter } from './css_class_generation.ts';
import {
is_possible_css_literal,
interpret_css_literal,
generate_css_literal_simple,
extract_segments,
extract_and_validate_modifiers,
- type CssLiteralOutput,
+ type CssLiteralOutput
} from './css_literal.ts';
-import {generate_modified_ruleset} from './css_ruleset_parser.ts';
-import {resolve_class_definition} from './css_class_resolution.ts';
+import { generate_modified_ruleset } from './css_ruleset_parser.ts';
+import { resolve_class_definition } from './css_class_resolution.ts';
/**
* Interpreter for modified token/composite classes (e.g., `hover:p_md`, `md:box`, `dark:hover:panel`).
@@ -38,7 +38,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
return null;
}
- const {modifiers, remaining} = result;
+ const { modifiers, remaining } = result;
// All preceding segments must be valid modifiers (none left over)
if (remaining.length !== 0) {
@@ -76,7 +76,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
base_class,
base_class_name,
ctx.class_definitions,
- ctx.css_properties,
+ ctx.css_properties
);
if (!resolution_result.ok) {
ctx.diagnostics.push(resolution_result.error);
@@ -102,7 +102,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
declaration: resolution_result.declaration,
selector,
media_wrapper: modifiers.media?.css ?? null,
- ancestor_wrapper: modifiers.ancestor?.css ?? null,
+ ancestor_wrapper: modifiers.ancestor?.css ?? null
};
const css = generate_css_literal_simple(output);
@@ -118,7 +118,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
state_css,
pseudo_element_css,
modifiers.media?.css ?? null,
- modifiers.ancestor?.css ?? null,
+ modifiers.ancestor?.css ?? null
);
// Emit warnings for skipped modifiers
@@ -131,7 +131,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
message: `Rule "${skipped.selector}" already contains a pseudo-element; "${
skipped.conflicting_modifier
}" modifier was not applied`,
- suggestion: `The rule is included with just the class renamed`,
+ suggestion: `The rule is included with just the class renamed`
});
} else {
ctx.diagnostics.push({
@@ -140,7 +140,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
message: `Rule "${skipped.selector}" already contains "${
skipped.conflicting_modifier
}"; modifier was not applied to avoid redundancy`,
- suggestion: `The rule is included with just the class renamed`,
+ suggestion: `The rule is included with just the class renamed`
});
}
}
@@ -150,7 +150,7 @@ export const modified_class_interpreter: CssClassDefinitionInterpreter = {
}
return null;
- },
+ }
};
/**
@@ -186,7 +186,7 @@ export const css_literal_interpreter: CssClassDefinitionInterpreter = {
// Return the CSS but strip trailing newline for consistency
return css.trimEnd();
- },
+ }
};
/**
@@ -196,5 +196,5 @@ export const css_literal_interpreter: CssClassDefinitionInterpreter = {
*/
export const css_class_interpreters: Array = [
modified_class_interpreter,
- css_literal_interpreter,
+ css_literal_interpreter
];
diff --git a/src/lib/css_class_resolution.ts b/src/lib/css_class_resolution.ts
index 40b044c65..0fffb2199 100644
--- a/src/lib/css_class_resolution.ts
+++ b/src/lib/css_class_resolution.ts
@@ -7,24 +7,24 @@
* @module
*/
-import {levenshtein_distance} from '@fuzdev/fuz_util/string.ts';
+import { levenshtein_distance } from '@fuzdev/fuz_util/string.ts';
-import type {InterpreterDiagnostic} from './diagnostics.ts';
-import type {CssClassDefinition, CssClassDefinitionStatic} from './css_class_generation.ts';
+import type { InterpreterDiagnostic } from './diagnostics.ts';
+import type { CssClassDefinition, CssClassDefinitionStatic } from './css_class_generation.ts';
import {
try_resolve_literal,
extract_segments,
extract_and_validate_modifiers,
has_extracted_modifiers,
- suggest_modifier,
+ suggest_modifier
} from './css_literal.ts';
/**
* Result from resolving a `composes` array to combined declarations.
*/
export type ResolveComposesResult =
- | {ok: true; declaration: string; warnings: Array | null}
- | {ok: false; error: InterpreterDiagnostic};
+ | { ok: true; declaration: string; warnings: Array | null }
+ | { ok: false; error: InterpreterDiagnostic };
/**
* Resolves a class definition's declaration, handling `composes` composition.
@@ -43,7 +43,7 @@ export const resolve_class_definition = (
def: CssClassDefinitionStatic,
class_name: string,
definitions: Record,
- css_properties: Set | null = null,
+ css_properties: Set | null = null
): ResolveComposesResult => {
let warnings: Array | null = null;
@@ -55,7 +55,7 @@ export const resolve_class_definition = (
new Set([class_name]),
new Set(),
class_name,
- css_properties,
+ css_properties
);
if (!result.ok) return result;
@@ -72,14 +72,14 @@ export const resolve_class_definition = (
level: 'warning',
identifier: class_name,
message: `Class "${class_name}" has an empty declaration`,
- suggestion: 'Remove the empty declaration property or add CSS',
+ suggestion: 'Remove the empty declaration property or add CSS'
});
}
}
if (!warnings && result.warnings) {
warnings = result.warnings;
}
- return {ok: true, declaration: combined, warnings};
+ return { ok: true, declaration: combined, warnings };
}
// Handle declaration-only definitions
@@ -91,15 +91,15 @@ export const resolve_class_definition = (
level: 'warning',
identifier: class_name,
message: `Class "${class_name}" has an empty declaration`,
- suggestion: 'Remove the empty declaration property or add CSS',
- },
+ suggestion: 'Remove the empty declaration property or add CSS'
+ }
];
}
- return {ok: true, declaration: trimmed ?? '', warnings};
+ return { ok: true, declaration: trimmed ?? '', warnings };
}
// Ruleset definitions don't have a single declaration
- return {ok: true, declaration: '', warnings: null};
+ return { ok: true, declaration: '', warnings: null };
};
/**
@@ -129,7 +129,7 @@ export const resolve_composes = (
resolution_stack: Set,
visited: Set,
original_class_name: string,
- css_properties: Set | null = null,
+ css_properties: Set | null = null
): ResolveComposesResult => {
const declarations: Array = [];
let warnings: Array | null = null;
@@ -147,8 +147,8 @@ export const resolve_composes = (
level: 'error',
identifier: original_class_name,
message: `Circular reference detected: ${cycle_path}`,
- suggestion: 'Remove the circular dependency from composes arrays',
- },
+ suggestion: 'Remove the circular dependency from composes arrays'
+ }
};
}
@@ -161,7 +161,7 @@ export const resolve_composes = (
level: 'warning',
identifier: original_class_name,
message: `Class "${name}" is redundant`,
- suggestion: 'Already included by another class in this definition',
+ suggestion: 'Already included by another class in this definition'
});
}
continue;
@@ -188,8 +188,8 @@ export const resolve_composes = (
level: 'error',
identifier: original_class_name,
message: `Modified class "${name}" cannot be used in composes array`,
- suggestion: 'Apply modified classes directly in markup, not in composes arrays',
- },
+ suggestion: 'Apply modified classes directly in markup, not in composes arrays'
+ }
};
} else {
// Looks like modifier:unknown - report the unknown base
@@ -199,8 +199,8 @@ export const resolve_composes = (
level: 'error',
identifier: original_class_name,
message: `Unknown class "${base_name}" in composes array`,
- suggestion: `Check that "${base_name}" is defined in class_definitions`,
- },
+ suggestion: `Check that "${base_name}" is defined in class_definitions`
+ }
};
}
}
@@ -242,13 +242,13 @@ export const resolve_composes = (
message: `Unknown modifier "${failed_prop}" with class "${potential_class}"`,
suggestion: `Did you mean "${
corrected
- }"? Note: modified classes cannot be used in composes`,
- },
+ }"? Note: modified classes cannot be used in composes`
+ }
};
}
}
}
- return {ok: false, error: literal_result.error};
+ return { ok: false, error: literal_result.error };
}
// Not a literal - fall through to original "unknown class" error
return {
@@ -257,8 +257,8 @@ export const resolve_composes = (
level: 'error',
identifier: original_class_name,
message: `Unknown class "${name}" in composes array`,
- suggestion: `Check that "${name}" is defined in class_definitions`,
- },
+ suggestion: `Check that "${name}" is defined in class_definitions`
+ }
};
}
@@ -270,8 +270,8 @@ export const resolve_composes = (
level: 'error',
identifier: original_class_name,
message: `Cannot reference interpreter pattern "${name}" in composes array`,
- suggestion: 'Only static class definitions can be referenced',
- },
+ suggestion: 'Only static class definitions can be referenced'
+ }
};
}
@@ -283,8 +283,8 @@ export const resolve_composes = (
level: 'error',
identifier: original_class_name,
message: `Cannot reference ruleset class "${name}" in composes array`,
- suggestion: 'Ruleset classes have multiple selectors and cannot be inlined',
- },
+ suggestion: 'Ruleset classes have multiple selectors and cannot be inlined'
+ }
};
}
@@ -300,7 +300,7 @@ export const resolve_composes = (
resolution_stack,
visited,
original_class_name,
- css_properties,
+ css_properties
);
resolution_stack.delete(name);
if (!nested.ok) return nested; // Propagate error
@@ -325,11 +325,11 @@ export const resolve_composes = (
level: 'warning',
identifier: name,
message: `Class "${name}" has an empty declaration`,
- suggestion: 'Remove the empty declaration property or add CSS',
+ suggestion: 'Remove the empty declaration property or add CSS'
});
}
}
}
- return {ok: true, declaration: declarations.join(' '), warnings};
+ return { ok: true, declaration: declarations.join(' '), warnings };
};
diff --git a/src/lib/css_classes.ts b/src/lib/css_classes.ts
index a125f3988..f3f2f69ca 100644
--- a/src/lib/css_classes.ts
+++ b/src/lib/css_classes.ts
@@ -7,8 +7,8 @@
* @module
*/
-import type {SourceLocation, ExtractionDiagnostic} from './diagnostics.ts';
-import type {ExtractionData} from './css_class_extractor.ts';
+import type { SourceLocation, ExtractionDiagnostic } from './diagnostics.ts';
+import type { ExtractionData } from './css_class_extractor.ts';
/**
* Collection of CSS classes extracted from source files.
@@ -67,7 +67,7 @@ export class CssClasses {
*/
constructor(
additional_classes: Set | null = null,
- exclude_classes: Set | null = null,
+ exclude_classes: Set | null = null
) {
this.#additional_classes = additional_classes;
this.#exclude_classes = exclude_classes;
@@ -173,7 +173,7 @@ export class CssClasses {
explicit_classes: this.#explicit,
all_elements: this.#all_elements,
explicit_elements: this.#explicit_elements,
- explicit_variables: this.#explicit_variables,
+ explicit_variables: this.#explicit_variables
};
}
diff --git a/src/lib/css_literal.ts b/src/lib/css_literal.ts
index 1076a4f0f..9bc532ad5 100644
--- a/src/lib/css_literal.ts
+++ b/src/lib/css_literal.ts
@@ -11,10 +11,10 @@
* @module
*/
-import {levenshtein_distance} from '@fuzdev/fuz_util/string.ts';
+import { levenshtein_distance } from '@fuzdev/fuz_util/string.ts';
-import {type InterpreterDiagnostic} from './diagnostics.ts';
-import {get_modifier, get_all_modifier_names, type ModifierDefinition} from './modifiers.ts';
+import { type InterpreterDiagnostic } from './diagnostics.ts';
+import { get_modifier, get_all_modifier_names, type ModifierDefinition } from './modifiers.ts';
//
// Types
@@ -52,8 +52,8 @@ export interface ParsedCssLiteral {
* Callers should use a guard pattern: `if (result.diagnostics) { ... }`
*/
export type CssLiteralParseResult =
- | {ok: true; parsed: ParsedCssLiteral; diagnostics: Array | null}
- | {ok: false; error: InterpreterDiagnostic};
+ | { ok: true; parsed: ParsedCssLiteral; diagnostics: Array | null }
+ | { ok: false; error: InterpreterDiagnostic };
/**
* Extracted modifiers from a class name.
@@ -74,8 +74,8 @@ export interface ExtractedModifiers {
* Result of extracting modifiers from segments.
*/
export type ModifierExtractionResult =
- | {ok: true; modifiers: ExtractedModifiers; remaining: Array}
- | {ok: false; error: InterpreterDiagnostic};
+ | { ok: true; modifiers: ExtractedModifiers; remaining: Array }
+ | { ok: false; error: InterpreterDiagnostic };
/**
* Result of interpreting a CSS-literal class.
@@ -84,8 +84,8 @@ export type ModifierExtractionResult =
* Callers should use a guard pattern: `if (result.warnings) { ... }`
*/
export type InterpretCssLiteralResult =
- | {ok: true; output: CssLiteralOutput; warnings: Array | null}
- | {ok: false; error: InterpreterDiagnostic};
+ | { ok: true; output: CssLiteralOutput; warnings: Array | null }
+ | { ok: false; error: InterpreterDiagnostic };
//
// CSS Property Validation
@@ -112,7 +112,7 @@ export const load_css_properties = async (): Promise> => {
*/
export const is_valid_css_property = (
property: string,
- properties: Set | null,
+ properties: Set | null
): boolean => {
// Custom properties are always valid
if (property.startsWith('--')) return true;
@@ -146,7 +146,7 @@ const find_closest_match = (typo: string, candidates: Iterable): string
*/
export const suggest_css_property = (
typo: string,
- properties: Set | null,
+ properties: Set | null
): string | null => (properties ? find_closest_match(typo, properties) : null);
/**
@@ -278,7 +278,7 @@ export const extract_segments = (class_name: string): Array => {
*/
export const extract_and_validate_modifiers = (
segments: Array,
- class_name: string,
+ class_name: string
): ModifierExtractionResult => {
let media: ModifierDefinition | null = null;
let ancestor: ModifierDefinition | null = null;
@@ -305,8 +305,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Multiple media modifiers not allowed`,
identifier: class_name,
- suggestion: null,
- },
+ suggestion: null
+ }
};
}
if (ancestor) {
@@ -316,8 +316,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Media modifier must come before ancestor modifier`,
identifier: class_name,
- suggestion: `Move "${segment}" before "${ancestor.name}"`,
- },
+ suggestion: `Move "${segment}" before "${ancestor.name}"`
+ }
};
}
if (states.length > 0) {
@@ -327,8 +327,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Media modifier must come before state modifiers`,
identifier: class_name,
- suggestion: `Move "${segment}" before "${states[0]!.name}"`,
- },
+ suggestion: `Move "${segment}" before "${states[0]!.name}"`
+ }
};
}
if (pseudo_element) {
@@ -338,8 +338,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Media modifier must come before pseudo-element`,
identifier: class_name,
- suggestion: `Move "${segment}" before "${pseudo_element.name}"`,
- },
+ suggestion: `Move "${segment}" before "${pseudo_element.name}"`
+ }
};
}
media = modifier;
@@ -354,8 +354,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Modifiers "${ancestor.name}" and "${segment}" are mutually exclusive`,
identifier: class_name,
- suggestion: null,
- },
+ suggestion: null
+ }
};
}
if (states.length > 0) {
@@ -365,8 +365,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Ancestor modifier must come before state modifiers`,
identifier: class_name,
- suggestion: `Move "${segment}" before "${states[0]!.name}"`,
- },
+ suggestion: `Move "${segment}" before "${states[0]!.name}"`
+ }
};
}
if (pseudo_element) {
@@ -376,8 +376,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Ancestor modifier must come before pseudo-element`,
identifier: class_name,
- suggestion: `Move "${segment}" before "${pseudo_element.name}"`,
- },
+ suggestion: `Move "${segment}" before "${pseudo_element.name}"`
+ }
};
}
ancestor = modifier;
@@ -392,8 +392,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `State modifiers must come before pseudo-element`,
identifier: class_name,
- suggestion: `Move "${segment}" before "${pseudo_element.name}"`,
- },
+ suggestion: `Move "${segment}" before "${pseudo_element.name}"`
+ }
};
}
// Check alphabetical order (full string comparison)
@@ -408,8 +408,8 @@ export const extract_and_validate_modifiers = (
segment
}" should be "${segment}:${prev.name}"`,
identifier: class_name,
- suggestion: `Reorder to: ...${segment}:${prev.name}:...`,
- },
+ suggestion: `Reorder to: ...${segment}:${prev.name}:...`
+ }
};
}
}
@@ -425,8 +425,8 @@ export const extract_and_validate_modifiers = (
level: 'error',
message: `Multiple pseudo-element modifiers not allowed`,
identifier: class_name,
- suggestion: null,
- },
+ suggestion: null
+ }
};
}
pseudo_element = modifier;
@@ -437,8 +437,8 @@ export const extract_and_validate_modifiers = (
return {
ok: true,
- modifiers: {media, ancestor, states, pseudo_element},
- remaining: segments.slice(i),
+ modifiers: { media, ancestor, states, pseudo_element },
+ remaining: segments.slice(i)
};
};
@@ -452,7 +452,7 @@ export const extract_and_validate_modifiers = (
*/
export const parse_css_literal = (
class_name: string,
- css_properties: Set | null,
+ css_properties: Set | null
): CssLiteralParseResult => {
const segments = extract_segments(class_name);
@@ -463,8 +463,8 @@ export const parse_css_literal = (
level: 'error',
message: `Invalid CSS-literal syntax: expected "property:value" format`,
identifier: class_name,
- suggestion: null,
- },
+ suggestion: null
+ }
};
}
@@ -480,7 +480,7 @@ export const parse_css_literal = (
const modifier_result = extract_and_validate_modifiers(modifier_segments, class_name);
if (!modifier_result.ok) {
- return {ok: false, error: modifier_result.error};
+ return { ok: false, error: modifier_result.error };
}
// All segments should have been consumed as modifiers
@@ -494,12 +494,12 @@ export const parse_css_literal = (
level: 'error',
message: `Unknown modifier "${unknown}"`,
identifier: class_name,
- suggestion: suggestion ? `Did you mean "${suggestion}"?` : null,
- },
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : null
+ }
};
}
- const {media, ancestor, states, pseudo_element} = modifier_result.modifiers;
+ const { media, ancestor, states, pseudo_element } = modifier_result.modifiers;
// Validate property
if (!is_valid_css_property(property, css_properties)) {
@@ -510,8 +510,8 @@ export const parse_css_literal = (
level: 'error',
message: `Unknown CSS property "${property}"`,
identifier: class_name,
- suggestion: suggestion ? `Did you mean "${suggestion}"?` : null,
- },
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : null
+ }
};
}
@@ -525,7 +525,7 @@ export const parse_css_literal = (
level: 'warning',
message: calc_warning,
identifier: class_name,
- suggestion: `Use ~ for spaces in calc expressions`,
+ suggestion: `Use ~ for spaces in calc expressions`
});
}
@@ -538,9 +538,9 @@ export const parse_css_literal = (
states,
pseudo_element,
property,
- value: formatted_value,
+ value: formatted_value
},
- diagnostics,
+ diagnostics
};
};
@@ -603,15 +603,15 @@ export interface CssLiteralOutput {
export const interpret_css_literal = (
class_name: string,
escaped_class_name: string,
- css_properties: Set | null,
+ css_properties: Set | null
): InterpretCssLiteralResult => {
const result = parse_css_literal(class_name, css_properties);
if (!result.ok) {
- return {ok: false, error: result.error};
+ return { ok: false, error: result.error };
}
- const {parsed, diagnostics} = result;
+ const { parsed, diagnostics } = result;
return {
ok: true,
@@ -619,9 +619,9 @@ export const interpret_css_literal = (
declaration: generate_declaration(parsed),
selector: generate_selector(escaped_class_name, parsed),
media_wrapper: parsed.media?.css ?? null,
- ancestor_wrapper: parsed.ancestor?.css ?? null,
+ ancestor_wrapper: parsed.ancestor?.css ?? null
},
- warnings: diagnostics,
+ warnings: diagnostics
};
};
@@ -633,8 +633,8 @@ export const interpret_css_literal = (
* Result of attempting to resolve a CSS literal for composition.
*/
export type LiteralResolutionResult =
- | {ok: true; declaration: string; warnings: Array | null}
- | {ok: false; error: InterpreterDiagnostic | null};
+ | { ok: true; declaration: string; warnings: Array | null }
+ | { ok: false; error: InterpreterDiagnostic | null };
/**
* Checks if a parsed CSS literal has any modifiers.
@@ -671,21 +671,21 @@ export const has_extracted_modifiers = (modifiers: ExtractedModifiers): boolean
export const try_resolve_literal = (
class_name: string,
css_properties: Set | null,
- context_class_name: string,
+ context_class_name: string
): LiteralResolutionResult => {
// Quick check - must look like a CSS literal
if (!is_possible_css_literal(class_name)) {
- return {ok: false, error: null};
+ return { ok: false, error: null };
}
// Parse the literal
const result = parse_css_literal(class_name, css_properties);
if (!result.ok) {
- return {ok: false, error: contextualize_error(result.error, context_class_name)};
+ return { ok: false, error: contextualize_error(result.error, context_class_name) };
}
- const {parsed, diagnostics} = result;
+ const { parsed, diagnostics } = result;
// Check for modifiers - modified literals cannot be composed
if (has_modifiers(parsed)) {
@@ -695,8 +695,8 @@ export const try_resolve_literal = (
level: 'error',
identifier: context_class_name,
message: `Modified class "${class_name}" cannot be used in composes array`,
- suggestion: 'Apply modified classes directly in markup, not in composes arrays',
- },
+ suggestion: 'Apply modified classes directly in markup, not in composes arrays'
+ }
};
}
@@ -704,7 +704,7 @@ export const try_resolve_literal = (
return {
ok: true,
declaration: generate_declaration(parsed),
- warnings: diagnostics,
+ warnings: diagnostics
};
};
@@ -713,10 +713,10 @@ export const try_resolve_literal = (
*/
const contextualize_error = (
error: InterpreterDiagnostic,
- context_class_name: string,
+ context_class_name: string
): InterpreterDiagnostic => ({
...error,
- identifier: context_class_name,
+ identifier: context_class_name
});
/**
diff --git a/src/lib/css_plugin_options.ts b/src/lib/css_plugin_options.ts
index 26de0bda8..b23de14b7 100644
--- a/src/lib/css_plugin_options.ts
+++ b/src/lib/css_plugin_options.ts
@@ -28,11 +28,11 @@
* @module
*/
-import type {FileFilter} from './file_filter.ts';
-import type {AcornPlugin} from './css_class_extractor.ts';
-import type {CssClassDefinition, CssClassDefinitionInterpreter} from './css_class_generation.ts';
-import type {StyleVariable} from './variable.ts';
-import type {CacheDeps} from './deps.ts';
+import type { FileFilter } from './file_filter.ts';
+import type { AcornPlugin } from './css_class_extractor.ts';
+import type { CssClassDefinition, CssClassDefinitionInterpreter } from './css_class_generation.ts';
+import type { StyleVariable } from './variable.ts';
+import type { CacheDeps } from './deps.ts';
/**
* Options for CSS class extraction from source files.
diff --git a/src/lib/css_ruleset_parser.ts b/src/lib/css_ruleset_parser.ts
index bf38698dd..5621764eb 100644
--- a/src/lib/css_ruleset_parser.ts
+++ b/src/lib/css_ruleset_parser.ts
@@ -9,8 +9,8 @@
* @module
*/
-import {escape_regexp} from '@fuzdev/fuz_util/regexp.ts';
-import {parseCss, type AST} from 'svelte/compiler';
+import { escape_regexp } from '@fuzdev/fuz_util/regexp.ts';
+import { parseCss, type AST } from 'svelte/compiler';
//
// Parsing
@@ -55,7 +55,7 @@ export const parse_ruleset = (css: string): ParsedRuleset => {
// Walk the CSS AST to find rules
walk_css_children(ast, css, rules);
- return {rules};
+ return { rules };
};
/**
@@ -65,7 +65,7 @@ export const parse_ruleset = (css: string): ParsedRuleset => {
const walk_css_children = (
node: Omit | AST.CSS.Atrule,
original_css: string,
- rules: Array,
+ rules: Array
): void => {
const children = 'children' in node ? node.children : [];
@@ -86,7 +86,7 @@ const walk_css_children = (
const walk_css_block = (
block: AST.CSS.Block,
original_css: string,
- rules: Array,
+ rules: Array
): void => {
for (const child of block.children) {
if (child.type === 'Rule') {
@@ -125,7 +125,7 @@ const extract_rule = (rule: AST.CSS.Rule, original_css: string, rules: Array,
- escaped_class_name: string,
+ escaped_class_name: string
): boolean => {
if (rules.length !== 1) return false;
@@ -162,7 +162,7 @@ export const is_single_selector_ruleset = (
*/
export const ruleset_contains_class = (
rules: Array,
- escaped_class_name: string,
+ escaped_class_name: string
): boolean => {
// Match .class_name but not as part of another class (e.g., .class_name_foo)
const pattern = new RegExp(`\\.${escape_regexp(escaped_class_name)}(?![\\w-])`);
@@ -446,7 +446,7 @@ export const modify_single_selector = (
original_class: string,
new_class_escaped: string,
state_css: string,
- pseudo_element_css: string,
+ pseudo_element_css: string
): string => {
// Find the target class (must match the class name exactly, not as part of another class)
const class_pattern = new RegExp(`\\.${escape_regexp(original_class)}(?![\\w-])`);
@@ -486,7 +486,7 @@ export const modify_selector_group = (
original_class: string,
new_class_escaped: string,
states_to_add: Array,
- pseudo_element_css: string,
+ pseudo_element_css: string
): ModifiedSelectorGroupResult => {
const selectors = split_selector_list(selector_group);
let skipped_modifiers: Array | null = null;
@@ -507,7 +507,7 @@ export const modify_selector_group = (
(skipped_modifiers ??= []).push({
selector: trimmed,
reason: 'state_conflict',
- conflicting_modifier: state,
+ conflicting_modifier: state
});
} else {
non_conflicting_states.push(state);
@@ -519,7 +519,7 @@ export const modify_selector_group = (
(skipped_modifiers ??= []).push({
selector: trimmed,
reason: 'pseudo_element_conflict',
- conflicting_modifier: pseudo_element_css,
+ conflicting_modifier: pseudo_element_css
});
}
@@ -532,13 +532,13 @@ export const modify_selector_group = (
original_class,
new_class_escaped,
effective_state_css,
- effective_pseudo_element_css,
+ effective_pseudo_element_css
);
});
return {
selector: modified_selectors.join(',\n'),
- skipped_modifiers,
+ skipped_modifiers
};
};
@@ -567,7 +567,7 @@ export const generate_modified_ruleset = (
state_css: string,
pseudo_element_css: string,
media_wrapper: string | null,
- ancestor_wrapper: string | null,
+ ancestor_wrapper: string | null
): ModifiedRulesetResult => {
const parsed = parse_ruleset(original_ruleset);
let skipped_modifiers: Array | null = null;
@@ -597,7 +597,7 @@ export const generate_modified_ruleset = (
original_class,
new_class_escaped,
states_to_add,
- pseudo_element_css,
+ pseudo_element_css
);
// Collect skip info from per-selector conflict detection
@@ -619,5 +619,5 @@ export const generate_modified_ruleset = (
css += '}\n';
}
- return {css, skipped_modifiers};
+ return { css, skipped_modifiers };
};
diff --git a/src/lib/deps.ts b/src/lib/deps.ts
index d49e9c7f2..55ab148ac 100644
--- a/src/lib/deps.ts
+++ b/src/lib/deps.ts
@@ -38,10 +38,10 @@
* @module
*/
-import type {Result} from '@fuzdev/fuz_util/result.ts';
-import type {FsError} from '@fuzdev/fuz_util/fs.ts';
+import type { Result } from '@fuzdev/fuz_util/result.ts';
+import type { FsError } from '@fuzdev/fuz_util/fs.ts';
-export type {FsError};
+export type { FsError };
/**
* Cache-related file system deps.
@@ -56,17 +56,20 @@ export interface CacheDeps {
* Reads a text file.
* Returns a `not_found` error if the file doesn't exist.
*/
- read_text: (options: {path: string}) => Promise>;
+ read_text: (options: { path: string }) => Promise>;
/**
* Writes a text file atomically (temp file + rename for crash safety).
* Creates parent directories if they don't exist.
*/
- write_text_atomic: (options: {path: string; content: string}) => Promise>;
+ write_text_atomic: (options: {
+ path: string;
+ content: string;
+ }) => Promise>;
/**
* Removes a file. Returns a `not_found` error if the file doesn't exist —
* callers that want `rm -f` semantics should ignore that kind explicitly.
*/
- unlink: (options: {path: string}) => Promise>;
+ unlink: (options: { path: string }) => Promise>;
}
diff --git a/src/lib/deps_defaults.ts b/src/lib/deps_defaults.ts
index 39cde1d29..35d56f32f 100644
--- a/src/lib/deps_defaults.ts
+++ b/src/lib/deps_defaults.ts
@@ -7,13 +7,13 @@
* @module
*/
-import {readFile, writeFile, mkdir, unlink, rename} from 'node:fs/promises';
-import {dirname} from 'node:path';
+import { readFile, writeFile, mkdir, unlink, rename } from 'node:fs/promises';
+import { dirname } from 'node:path';
-import type {Result} from '@fuzdev/fuz_util/result.ts';
-import {fs_classify_error, type FsError} from '@fuzdev/fuz_util/fs.ts';
+import type { Result } from '@fuzdev/fuz_util/result.ts';
+import { fs_classify_error, type FsError } from '@fuzdev/fuz_util/fs.ts';
-import type {CacheDeps} from './deps.ts';
+import type { CacheDeps } from './deps.ts';
/**
* Wraps an async void-returning function, converting thrown errors to typed `FsError`.
@@ -21,9 +21,9 @@ import type {CacheDeps} from './deps.ts';
const wrap_void = async (fn: () => Promise): Promise> => {
try {
await fn();
- return {ok: true};
+ return { ok: true };
} catch (error) {
- return {ok: false, ...fs_classify_error(error)};
+ return { ok: false, ...fs_classify_error(error) };
}
};
@@ -31,24 +31,24 @@ const wrap_void = async (fn: () => Promise): Promise {
+ read_text: async ({ path }) => {
try {
- return {ok: true, value: await readFile(path, 'utf8')};
+ return { ok: true, value: await readFile(path, 'utf8') };
} catch (error) {
- return {ok: false, ...fs_classify_error(error)};
+ return { ok: false, ...fs_classify_error(error) };
}
},
- write_text_atomic: async ({path, content}) => {
+ write_text_atomic: async ({ path, content }) => {
return wrap_void(async () => {
// Atomic write: temp file + rename
// Include pid + timestamp to avoid conflicts with concurrent writes
- await mkdir(dirname(path), {recursive: true});
+ await mkdir(dirname(path), { recursive: true });
const temp_path = path + '.tmp.' + process.pid + '.' + Date.now();
await writeFile(temp_path, content);
await rename(temp_path, path);
});
},
- unlink: async ({path}) => wrap_void(() => unlink(path)),
+ unlink: async ({ path }) => wrap_void(() => unlink(path))
};
diff --git a/src/lib/diagnostics.ts b/src/lib/diagnostics.ts
index 93c784a2d..05b130083 100644
--- a/src/lib/diagnostics.ts
+++ b/src/lib/diagnostics.ts
@@ -87,14 +87,14 @@ export interface InterpreterDiagnostic {
*/
export const create_generation_diagnostic = (
diagnostic: InterpreterDiagnostic,
- locations: Array | null,
+ locations: Array | null
): GenerationDiagnostic => ({
phase: 'generation',
level: diagnostic.level,
message: diagnostic.message,
identifier: diagnostic.identifier,
suggestion: diagnostic.suggestion ?? null,
- locations,
+ locations
});
/**
diff --git a/src/lib/example_class_utilities.ts b/src/lib/example_class_utilities.ts
index 2f57099bd..2f88cc512 100644
--- a/src/lib/example_class_utilities.ts
+++ b/src/lib/example_class_utilities.ts
@@ -79,7 +79,7 @@ export const logicalClass = true && 'mt_md';
export const arrayClasses = ['mt_lg', 'mt_xl'];
/** Object - keys extracted as class names */
-export const objectClasses = {mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3'};
+export const objectClasses = { mt_xl2: 'mt_xl2', mt_xl3: 'mt_xl3' };
//
// Comment hint examples - @fuz-classes directive
diff --git a/src/lib/extract_file_cached.ts b/src/lib/extract_file_cached.ts
index 08e5626a3..8edbd5d3a 100644
--- a/src/lib/extract_file_cached.ts
+++ b/src/lib/extract_file_cached.ts
@@ -13,10 +13,10 @@
import {
extract_css_classes_with_locations,
type ExtractionData,
- type AcornPlugin,
+ type AcornPlugin
} from './css_class_extractor.ts';
-import {load_cached_extraction, from_cached_extraction} from './css_cache.ts';
-import type {CacheDeps} from './deps.ts';
+import { load_cached_extraction, from_cached_extraction } from './css_cache.ts';
+import type { CacheDeps } from './deps.ts';
export interface ExtractFileCachedOptions {
deps: CacheDeps;
@@ -45,9 +45,9 @@ export interface ExtractFileCachedResult {
* the caller should write.
*/
export const extract_file_cached = async (
- options: ExtractFileCachedOptions,
+ options: ExtractFileCachedOptions
): Promise => {
- const {deps, content, content_hash, cache_path, filename, acorn_plugins} = options;
+ const { deps, content, content_hash, cache_path, filename, acorn_plugins } = options;
if (cache_path) {
const cached = await load_cached_extraction(deps, cache_path);
@@ -55,11 +55,11 @@ export const extract_file_cached = async (
return {
extraction: from_cached_extraction(cached),
from_cache: true,
- cache_path_to_write: null,
+ cache_path_to_write: null
};
}
}
- const extraction = extract_css_classes_with_locations(content, {filename, acorn_plugins});
- return {extraction, from_cache: false, cache_path_to_write: cache_path};
+ const extraction = extract_css_classes_with_locations(content, { filename, acorn_plugins });
+ return { extraction, from_cache: false, cache_path_to_write: cache_path };
};
diff --git a/src/lib/gen_fuz_css.ts b/src/lib/gen_fuz_css.ts
index c7d26d65b..c620d2d71 100644
--- a/src/lib/gen_fuz_css.ts
+++ b/src/lib/gen_fuz_css.ts
@@ -7,30 +7,30 @@
* @module
*/
-import {join} from 'node:path';
-import type {Gen} from '@fuzdev/gro/gen.ts';
-import {map_concurrent, each_concurrent} from '@fuzdev/fuz_util/async.ts';
-
-import {filter_file_default} from './file_filter.ts';
-import {type ExtractionData, has_extraction_data} from './css_class_extractor.ts';
-import {format_diagnostic, CssGenerationError} from './diagnostics.ts';
-import {CssClasses} from './css_classes.ts';
-import {generate_css} from './generate_css.ts';
-import {create_bundled_resources, type BundledCssResources} from './bundled_resources.ts';
-import {extract_file_cached} from './extract_file_cached.ts';
-import {merge_class_definitions} from './css_class_definitions.ts';
-import {css_class_interpreters} from './css_class_interpreters.ts';
-import {load_css_properties} from './css_literal.ts';
+import { join } from 'node:path';
+import type { Gen } from '@fuzdev/gro/gen.ts';
+import { map_concurrent, each_concurrent } from '@fuzdev/fuz_util/async.ts';
+
+import { filter_file_default } from './file_filter.ts';
+import { type ExtractionData, has_extraction_data } from './css_class_extractor.ts';
+import { format_diagnostic, CssGenerationError } from './diagnostics.ts';
+import { CssClasses } from './css_classes.ts';
+import { generate_css } from './generate_css.ts';
+import { create_bundled_resources, type BundledCssResources } from './bundled_resources.ts';
+import { extract_file_cached } from './extract_file_cached.ts';
+import { merge_class_definitions } from './css_class_definitions.ts';
+import { css_class_interpreters } from './css_class_interpreters.ts';
+import { load_css_properties } from './css_literal.ts';
import {
DEFAULT_CACHE_DIR,
get_file_cache_path,
save_cached_extraction,
- delete_cached_extraction,
+ delete_cached_extraction
} from './css_cache.ts';
-import {default_cache_deps} from './deps_defaults.ts';
-import {get_all_variable_names} from './variable_graph.ts';
-import {extract_css_variables} from './css_variable_utils.ts';
-import type {CssGeneratorBaseOptions} from './css_plugin_options.ts';
+import { default_cache_deps } from './deps_defaults.ts';
+import { get_all_variable_names } from './variable_graph.ts';
+import { extract_css_variables } from './css_variable_utils.ts';
+import type { CssGeneratorBaseOptions } from './css_plugin_options.ts';
/**
* Skip cache on CI (no point writing cache that won't be reused).
@@ -117,7 +117,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
additional_variables,
exclude_elements,
exclude_variables,
- deps = default_cache_deps,
+ deps = default_cache_deps
} = options;
// Derive include flags from null check
@@ -131,7 +131,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
// Merge class definitions upfront (validates that definitions exist when needed)
const all_class_definitions = merge_class_definitions(
user_class_definitions,
- include_default_classes,
+ include_default_classes
);
// Lazy-load expensive resources, cached per generator instance so watch-mode
@@ -152,7 +152,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
base_css,
variables,
class_definitions: all_class_definitions,
- deps,
+ deps
});
}
return bundled_resources;
@@ -164,13 +164,13 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
return {
// Filter dependencies to skip non-extractable files.
// Returns 'all' when an extractable file changes, null otherwise.
- dependencies: ({changed_file_id}) => {
+ dependencies: ({ changed_file_id }) => {
if (!changed_file_id) return 'all';
if (filter_file(changed_file_id)) return 'all';
return null; // Ignore .json, .md, etc.
},
- generate: async ({filer, log, origin_path}) => {
+ generate: async ({ filer, log, origin_path }) => {
log.info('generating fuz_css classes...');
// Load CSS properties for validation (cached per instance)
@@ -196,7 +196,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
files_with_content: 0,
files_with_classes: 0,
cache_hits: 0,
- cache_misses: 0,
+ cache_misses: 0
};
// Collect nodes to process
@@ -224,7 +224,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
nodes.push({
id: disknode.id,
contents: disknode.contents,
- content_hash: disknode.content_hash,
+ content_hash: disknode.content_hash
});
}
}
@@ -239,13 +239,13 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
? null
: get_file_cache_path(node.id, resolved_cache_dir, project_root);
- const {extraction, from_cache, cache_path_to_write} = await extract_file_cached({
+ const { extraction, from_cache, cache_path_to_write } = await extract_file_cached({
deps,
content: node.contents,
content_hash: node.content_hash,
cache_path,
filename: node.id,
- acorn_plugins,
+ acorn_plugins
});
if (from_cache) {
@@ -263,9 +263,9 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
explicit_elements: extraction.explicit_elements,
explicit_variables: extraction.explicit_variables,
cache_path: cache_path_to_write,
- content_hash: node.content_hash,
+ content_hash: node.content_hash
};
- },
+ }
);
// Add to CssClasses (skip files with all-null extraction data)
@@ -280,7 +280,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
// Collect cache writes (entries that need writing)
const cache_writes = extractions.filter(
- (e): e is FileExtraction & {cache_path: string} => e.cache_path !== null,
+ (e): e is FileExtraction & { cache_path: string } => e.cache_path !== null
);
// Parallel cache writes (await completion)
@@ -290,7 +290,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
deps,
extraction.cache_path,
extraction.content_hash,
- extraction,
+ extraction
);
}).catch((err) => log.warn('Cache write error:', err));
}
@@ -317,7 +317,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
explicit_classes,
all_elements,
explicit_elements,
- explicit_variables,
+ explicit_variables
} = css_classes.get_all();
if (include_stats) {
@@ -349,7 +349,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
}
}
- const {css: final_css, diagnostics: all_diagnostics} = generate_css({
+ const { css: final_css, diagnostics: all_diagnostics } = generate_css({
all_classes,
all_classes_with_locations,
explicit_classes,
@@ -369,7 +369,7 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
exclude_elements,
exclude_variables,
log,
- include_stats,
+ include_stats
});
// Separate errors and warnings
@@ -421,6 +421,6 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => {
content_parts.push(`/* ${banner} */`);
return content_parts.join('\n\n');
- },
+ }
};
};
diff --git a/src/lib/generate_css.ts b/src/lib/generate_css.ts
index 76ee02a39..c45aa891f 100644
--- a/src/lib/generate_css.ts
+++ b/src/lib/generate_css.ts
@@ -10,17 +10,17 @@
* @module
*/
-import type {Logger} from '@fuzdev/fuz_util/log.ts';
+import type { Logger } from '@fuzdev/fuz_util/log.ts';
-import type {Diagnostic, SourceLocation} from './diagnostics.ts';
+import type { Diagnostic, SourceLocation } from './diagnostics.ts';
import {
generate_classes_css,
type CssClassDefinition,
- type CssClassDefinitionInterpreter,
+ type CssClassDefinitionInterpreter
} from './css_class_generation.ts';
-import {resolve_css, generate_bundled_css} from './css_bundled_resolution.ts';
-import {get_all_variable_names} from './variable_graph.ts';
-import type {BundledCssResources} from './bundled_resources.ts';
+import { resolve_css, generate_bundled_css } from './css_bundled_resolution.ts';
+import { get_all_variable_names } from './variable_graph.ts';
+import type { BundledCssResources } from './bundled_resources.ts';
/**
* Inputs to `generate_css`. The first group mirrors the shape returned by
@@ -104,7 +104,7 @@ export const generate_css = (options: GenerateCssOptions): GenerateCssResult =>
exclude_elements,
exclude_variables,
log,
- include_stats = false,
+ include_stats = false
} = options;
const utility_result = generate_classes_css({
@@ -114,7 +114,7 @@ export const generate_css = (options: GenerateCssOptions): GenerateCssResult =>
css_properties,
log,
class_locations: all_classes_with_locations,
- explicit_classes,
+ explicit_classes
});
const diagnostics: Array = [...extraction_diagnostics, ...utility_result.diagnostics];
@@ -144,19 +144,19 @@ export const generate_css = (options: GenerateCssOptions): GenerateCssResult =>
exclude_elements,
exclude_variables,
explicit_elements,
- explicit_variables,
+ explicit_variables
});
if (include_stats && resolution.stats && log) {
log.info(
`[css_resolution] Elements: ${
resolution.stats.element_count
- } (${resolution.stats.elements.join(', ')})`,
+ } (${resolution.stats.elements.join(', ')})`
);
log.info(
`[css_resolution] Rules: ${resolution.stats.included_rules} of ${
resolution.stats.total_rules
- }`,
+ }`
);
log.info(`[css_resolution] Variables: ${resolution.stats.variable_count} resolved`);
}
@@ -170,7 +170,7 @@ export const generate_css = (options: GenerateCssOptions): GenerateCssResult =>
if (include_base && !include_theme) {
const theme_var_names = get_all_variable_names(resources.variable_graph);
const references_theme_var = [...resolution.resolved_variables].some((v) =>
- theme_var_names.has(v),
+ theme_var_names.has(v)
);
if (references_theme_var) {
diagnostics.push({
@@ -180,7 +180,7 @@ export const generate_css = (options: GenerateCssOptions): GenerateCssResult =>
'Base styles are enabled but theme variables are disabled (variables: null); emitted styles reference theme variables that will be undefined',
suggestion: 'Import theme.css separately, or set base_css: null for utility-only mode.',
identifier: 'theme_variables_disabled',
- locations: null,
+ locations: null
});
}
}
@@ -188,12 +188,12 @@ export const generate_css = (options: GenerateCssOptions): GenerateCssResult =>
css = generate_bundled_css(resolution, utility_result.css, {
include_theme,
include_base,
- include_utilities: true,
+ include_utilities: true
});
} else {
// utility-only mode
css = utility_result.css;
}
- return {css, diagnostics};
+ return { css, diagnostics };
};
diff --git a/src/lib/knobs.ts b/src/lib/knobs.ts
index 58595fc2d..9a2842525 100644
--- a/src/lib/knobs.ts
+++ b/src/lib/knobs.ts
@@ -20,7 +20,7 @@
* @module
*/
-import type {StyleVariableName} from './variable.ts';
+import type { StyleVariableName } from './variable.ts';
/**
* The value kind of a knob, determining the editor widget and how the value
@@ -42,13 +42,7 @@ export type KnobKind =
* The axis of the theme-space taxonomy a knob moves.
*/
export type KnobAxis =
- | 'color'
- | 'shape'
- | 'density'
- | 'depth'
- | 'typography'
- | 'motion'
- | 'decoration';
+ 'color' | 'shape' | 'density' | 'depth' | 'typography' | 'motion' | 'decoration';
/**
* Leverage tier: how much of the system a knob reshapes, which the editor
@@ -96,7 +90,7 @@ const hue = (
name: StyleVariableName,
leverage: KnobLeverage,
tier: KnobTier = 'semantic',
- bindable = false,
+ bindable = false
): ThemeKnob => ({
name,
kind: 'hue',
@@ -105,7 +99,7 @@ const hue = (
tier,
range: [0, 360],
step: 1,
- ...(bindable ? {bindable} : null),
+ ...(bindable ? { bindable } : null)
});
const lightness_ramp = (family: string): Array => [
@@ -116,7 +110,7 @@ const lightness_ramp = (family: string): Array => [
leverage: 'md',
tier: 'semantic',
range: [0, 1],
- step: 0.001,
+ step: 0.001
},
{
name: `${family}_lightness_100`,
@@ -125,7 +119,7 @@ const lightness_ramp = (family: string): Array => [
leverage: 'md',
tier: 'semantic',
range: [0, 1],
- step: 0.001,
+ step: 0.001
},
{
name: `${family}_lightness_curve`,
@@ -134,8 +128,8 @@ const lightness_ramp = (family: string): Array => [
leverage: 'md',
tier: 'semantic',
range: [0.2, 4],
- step: 0.01,
- },
+ step: 0.01
+ }
];
/**
@@ -152,7 +146,7 @@ export const theme_knobs: Array = [
leverage: 'lg',
tier: 'semantic',
range: [0, 0.1],
- step: 0.001,
+ step: 0.001
},
hue('hue_accent', 'lg', 'semantic', true),
{
@@ -162,7 +156,7 @@ export const theme_knobs: Array = [
leverage: 'lg',
tier: 'semantic',
range: [0, 2],
- step: 0.05,
+ step: 0.05
},
{
name: 'hue_shift',
@@ -171,7 +165,7 @@ export const theme_knobs: Array = [
leverage: 'lg',
tier: 'semantic',
range: [-45, 45],
- step: 1,
+ step: 1
},
hue('hue_positive', 'md', 'semantic', true),
hue('hue_negative', 'md', 'semantic', true),
@@ -187,7 +181,7 @@ export const theme_knobs: Array = [
leverage: 'md',
tier: 'semantic',
range: [0, 0.06],
- step: 0.001,
+ step: 0.001
},
{
// requests up to the per-stop gamut caps; chroma_scale pushes past them
@@ -197,7 +191,7 @@ export const theme_knobs: Array = [
leverage: 'md',
tier: 'semantic',
range: [0, 0.125],
- step: 0.001,
+ step: 0.001
},
{
name: 'palette_chroma_curve',
@@ -206,17 +200,24 @@ export const theme_knobs: Array = [
leverage: 'md',
tier: 'semantic',
range: [0.2, 4],
- step: 0.01,
+ step: 0.01
},
// color - micro-surface hooks consumed by style.css via var() fallbacks
- {name: 'caret_color', kind: 'color', axis: 'color', leverage: 'sm', tier: 'semantic', hook: true},
+ {
+ name: 'caret_color',
+ kind: 'color',
+ axis: 'color',
+ leverage: 'sm',
+ tier: 'semantic',
+ hook: true
+ },
{
name: 'scrollbar_thumb_color',
kind: 'color',
axis: 'color',
leverage: 'sm',
tier: 'semantic',
- hook: true,
+ hook: true
},
{
name: 'scrollbar_track_color',
@@ -224,7 +225,7 @@ export const theme_knobs: Array = [
axis: 'color',
leverage: 'sm',
tier: 'semantic',
- hook: true,
+ hook: true
},
// color - the palette tier (moving these makes a theme an exemplar)
hue('hue_a', 'sm', 'palette'),
@@ -245,7 +246,7 @@ export const theme_knobs: Array = [
leverage: 'lg',
tier: 'semantic',
range: [0, 3],
- step: 0.05,
+ step: 0.05
},
{
name: 'border_style',
@@ -253,25 +254,25 @@ export const theme_knobs: Array = [
axis: 'shape',
leverage: 'md',
tier: 'semantic',
- values: ['solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'none'],
- },
- {name: 'border_width', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_1', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_2', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_3', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_4', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_5', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_6', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_7', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_8', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_width_9', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_xs3', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_xs2', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_xs', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_sm', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_md', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_lg', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
- {name: 'border_radius_xl', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic'},
+ values: ['solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'none']
+ },
+ { name: 'border_width', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_1', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_2', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_3', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_4', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_5', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_6', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_7', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_8', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_width_9', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_xs3', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_xs2', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_xs', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_sm', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_md', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_lg', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
+ { name: 'border_radius_xl', kind: 'length', axis: 'shape', leverage: 'sm', tier: 'semantic' },
// density
{
name: 'scale_factor',
@@ -280,31 +281,31 @@ export const theme_knobs: Array = [
leverage: 'lg',
tier: 'semantic',
range: [0.25, 2],
- step: 0.05,
- },
- {name: 'space_xs5', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xs4', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xs3', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xs2', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xs', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_sm', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_md', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_lg', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl2', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl3', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl4', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl5', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl6', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl7', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl8', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl9', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl10', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl11', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl12', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl13', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl14', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
- {name: 'space_xl15', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic'},
+ step: 0.05
+ },
+ { name: 'space_xs5', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xs4', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xs3', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xs2', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xs', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_sm', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_md', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_lg', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl2', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl3', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl4', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl5', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl6', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl7', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl8', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl9', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl10', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl11', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl12', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl13', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl14', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
+ { name: 'space_xl15', kind: 'length', axis: 'density', leverage: 'sm', tier: 'semantic' },
// depth
{
name: 'shadow_alpha_scale',
@@ -313,12 +314,18 @@ export const theme_knobs: Array = [
leverage: 'lg',
tier: 'semantic',
range: [0, 2],
- step: 0.05,
+ step: 0.05
+ },
+ { name: 'shadow_color_umbra', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic' },
+ {
+ name: 'shadow_color_highlight',
+ kind: 'color',
+ axis: 'depth',
+ leverage: 'md',
+ tier: 'semantic'
},
- {name: 'shadow_color_umbra', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic'},
- {name: 'shadow_color_highlight', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic'},
- {name: 'shadow_color_glow', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic'},
- {name: 'shadow_color_shroud', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic'},
+ { name: 'shadow_color_glow', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic' },
+ { name: 'shadow_color_shroud', kind: 'color', axis: 'depth', leverage: 'md', tier: 'semantic' },
// the dialog/fullscreen ::backdrop dim, a style.css var() fallback hook
{
name: 'backdrop_color',
@@ -326,39 +333,39 @@ export const theme_knobs: Array = [
axis: 'depth',
leverage: 'sm',
tier: 'semantic',
- hook: true,
+ hook: true
},
- {name: 'button_shadow', kind: 'shadow', axis: 'depth', leverage: 'md', tier: 'semantic'},
- {name: 'button_shadow_hover', kind: 'shadow', axis: 'depth', leverage: 'md', tier: 'semantic'},
- {name: 'button_shadow_active', kind: 'shadow', axis: 'depth', leverage: 'md', tier: 'semantic'},
+ { name: 'button_shadow', kind: 'shadow', axis: 'depth', leverage: 'md', tier: 'semantic' },
+ { name: 'button_shadow_hover', kind: 'shadow', axis: 'depth', leverage: 'md', tier: 'semantic' },
+ { name: 'button_shadow_active', kind: 'shadow', axis: 'depth', leverage: 'md', tier: 'semantic' },
// typography
{
name: 'font_family_sans',
kind: 'font_stack',
axis: 'typography',
leverage: 'md',
- tier: 'semantic',
+ tier: 'semantic'
},
{
name: 'font_family_serif',
kind: 'font_stack',
axis: 'typography',
leverage: 'md',
- tier: 'semantic',
+ tier: 'semantic'
},
{
name: 'font_family_mono',
kind: 'font_stack',
axis: 'typography',
leverage: 'md',
- tier: 'semantic',
+ tier: 'semantic'
},
{
name: 'heading_font_family',
kind: 'font_stack',
axis: 'typography',
leverage: 'md',
- tier: 'semantic',
+ tier: 'semantic'
},
{
name: 'font_weight',
@@ -367,7 +374,7 @@ export const theme_knobs: Array = [
leverage: 'md',
tier: 'semantic',
range: [100, 900],
- step: 100,
+ step: 100
},
{
// setting this flattens the per-tier heading weight ladder deliberately
@@ -378,7 +385,7 @@ export const theme_knobs: Array = [
tier: 'semantic',
range: [100, 900],
step: 100,
- hook: true,
+ hook: true
},
{
name: 'line_height_xs',
@@ -387,7 +394,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0.8, 3],
- step: 0.05,
+ step: 0.05
},
{
name: 'line_height_sm',
@@ -396,7 +403,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0.8, 3],
- step: 0.05,
+ step: 0.05
},
{
name: 'line_height_md',
@@ -405,7 +412,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0.8, 3],
- step: 0.05,
+ step: 0.05
},
{
name: 'line_height_lg',
@@ -414,7 +421,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0.8, 3],
- step: 0.05,
+ step: 0.05
},
{
name: 'line_height_xl',
@@ -423,7 +430,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0.8, 3],
- step: 0.05,
+ step: 0.05
},
// motion
{
@@ -433,7 +440,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 5],
- step: 0.01,
+ step: 0.01
},
{
name: 'duration_2',
@@ -442,7 +449,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 5],
- step: 0.01,
+ step: 0.01
},
{
name: 'duration_3',
@@ -451,7 +458,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 5],
- step: 0.01,
+ step: 0.01
},
{
name: 'duration_4',
@@ -460,7 +467,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 5],
- step: 0.01,
+ step: 0.01
},
{
name: 'duration_5',
@@ -469,7 +476,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 5],
- step: 0.01,
+ step: 0.01
},
{
name: 'duration_6',
@@ -478,7 +485,7 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 5],
- step: 0.01,
+ step: 0.01
},
{
name: 'disabled_opacity',
@@ -487,30 +494,30 @@ export const theme_knobs: Array = [
leverage: 'sm',
tier: 'semantic',
range: [0, 100],
- step: 1,
+ step: 1
},
// decoration
- {name: 'background_image', kind: 'text', axis: 'decoration', leverage: 'md', tier: 'semantic'},
+ { name: 'background_image', kind: 'text', axis: 'decoration', leverage: 'md', tier: 'semantic' }
];
/**
* The theme-space axes in editor display order, with display titles.
*/
-export const knob_axes: Array<{axis: KnobAxis; title: string}> = [
- {axis: 'color', title: 'Color'},
- {axis: 'shape', title: 'Shape'},
- {axis: 'density', title: 'Density'},
- {axis: 'depth', title: 'Depth'},
- {axis: 'typography', title: 'Typography'},
- {axis: 'motion', title: 'Motion'},
- {axis: 'decoration', title: 'Decoration'},
+export const knob_axes: Array<{ axis: KnobAxis; title: string }> = [
+ { axis: 'color', title: 'Color' },
+ { axis: 'shape', title: 'Shape' },
+ { axis: 'density', title: 'Density' },
+ { axis: 'depth', title: 'Depth' },
+ { axis: 'typography', title: 'Typography' },
+ { axis: 'motion', title: 'Motion' },
+ { axis: 'decoration', title: 'Decoration' }
];
/**
* The catalog indexed by variable name.
*/
export const theme_knob_by_name: Map = new Map(
- theme_knobs.map((k) => [k.name, k]),
+ theme_knobs.map((k) => [k.name, k])
);
/**
@@ -519,5 +526,5 @@ export const theme_knob_by_name: Map = new Map(
* variable names.
*/
export const theme_knob_hook_names: Set = new Set(
- theme_knobs.filter((k) => k.hook).map((k) => k.name),
+ theme_knobs.filter((k) => k.hook).map((k) => k.name)
);
diff --git a/src/lib/modifiers.ts b/src/lib/modifiers.ts
index 8aa358914..6e4fb1ca5 100644
--- a/src/lib/modifiers.ts
+++ b/src/lib/modifiers.ts
@@ -38,97 +38,97 @@ export interface ModifierDefinition {
*/
export const MODIFIERS: Array = [
// Media modifiers - viewport breakpoints (mobile-first)
- {name: 'sm', type: 'media', css: '@media (width >= 40rem)', order: 1},
- {name: 'md', type: 'media', css: '@media (width >= 48rem)', order: 2},
- {name: 'lg', type: 'media', css: '@media (width >= 64rem)', order: 3},
- {name: 'xl', type: 'media', css: '@media (width >= 80rem)', order: 4},
- {name: '2xl', type: 'media', css: '@media (width >= 96rem)', order: 5},
+ { name: 'sm', type: 'media', css: '@media (width >= 40rem)', order: 1 },
+ { name: 'md', type: 'media', css: '@media (width >= 48rem)', order: 2 },
+ { name: 'lg', type: 'media', css: '@media (width >= 64rem)', order: 3 },
+ { name: 'xl', type: 'media', css: '@media (width >= 80rem)', order: 4 },
+ { name: '2xl', type: 'media', css: '@media (width >= 96rem)', order: 5 },
// Max-width variants (for targeting below a breakpoint)
- {name: 'max-sm', type: 'media', css: '@media (width < 40rem)', order: 11},
- {name: 'max-md', type: 'media', css: '@media (width < 48rem)', order: 12},
- {name: 'max-lg', type: 'media', css: '@media (width < 64rem)', order: 13},
- {name: 'max-xl', type: 'media', css: '@media (width < 80rem)', order: 14},
- {name: 'max-2xl', type: 'media', css: '@media (width < 96rem)', order: 15},
+ { name: 'max-sm', type: 'media', css: '@media (width < 40rem)', order: 11 },
+ { name: 'max-md', type: 'media', css: '@media (width < 48rem)', order: 12 },
+ { name: 'max-lg', type: 'media', css: '@media (width < 64rem)', order: 13 },
+ { name: 'max-xl', type: 'media', css: '@media (width < 80rem)', order: 14 },
+ { name: 'max-2xl', type: 'media', css: '@media (width < 96rem)', order: 15 },
// Media modifiers - feature queries
- {name: 'print', type: 'media', css: '@media print'},
- {name: 'motion-safe', type: 'media', css: '@media (prefers-reduced-motion: no-preference)'},
- {name: 'motion-reduce', type: 'media', css: '@media (prefers-reduced-motion: reduce)'},
- {name: 'contrast-more', type: 'media', css: '@media (prefers-contrast: more)'},
- {name: 'contrast-less', type: 'media', css: '@media (prefers-contrast: less)'},
- {name: 'portrait', type: 'media', css: '@media (orientation: portrait)'},
- {name: 'landscape', type: 'media', css: '@media (orientation: landscape)'},
- {name: 'forced-colors', type: 'media', css: '@media (forced-colors: active)'},
+ { name: 'print', type: 'media', css: '@media print' },
+ { name: 'motion-safe', type: 'media', css: '@media (prefers-reduced-motion: no-preference)' },
+ { name: 'motion-reduce', type: 'media', css: '@media (prefers-reduced-motion: reduce)' },
+ { name: 'contrast-more', type: 'media', css: '@media (prefers-contrast: more)' },
+ { name: 'contrast-less', type: 'media', css: '@media (prefers-contrast: less)' },
+ { name: 'portrait', type: 'media', css: '@media (orientation: portrait)' },
+ { name: 'landscape', type: 'media', css: '@media (orientation: landscape)' },
+ { name: 'forced-colors', type: 'media', css: '@media (forced-colors: active)' },
// Ancestor modifiers - color scheme
- {name: 'dark', type: 'ancestor', css: ':root.dark'},
- {name: 'light', type: 'ancestor', css: ':root.light'},
+ { name: 'dark', type: 'ancestor', css: ':root.dark' },
+ { name: 'light', type: 'ancestor', css: ':root.light' },
// State modifiers - interaction (ordered for proper cascade: LVFHA)
- {name: 'any-link', type: 'state', css: ':any-link'},
- {name: 'link', type: 'state', css: ':link'},
- {name: 'visited', type: 'state', css: ':visited', order: 1},
- {name: 'focus-within', type: 'state', css: ':focus-within', order: 2},
- {name: 'focus', type: 'state', css: ':focus', order: 3},
- {name: 'focus-visible', type: 'state', css: ':focus-visible', order: 4},
- {name: 'hover', type: 'state', css: ':hover', order: 5},
- {name: 'active', type: 'state', css: ':active', order: 6},
- {name: 'target', type: 'state', css: ':target', order: 7},
+ { name: 'any-link', type: 'state', css: ':any-link' },
+ { name: 'link', type: 'state', css: ':link' },
+ { name: 'visited', type: 'state', css: ':visited', order: 1 },
+ { name: 'focus-within', type: 'state', css: ':focus-within', order: 2 },
+ { name: 'focus', type: 'state', css: ':focus', order: 3 },
+ { name: 'focus-visible', type: 'state', css: ':focus-visible', order: 4 },
+ { name: 'hover', type: 'state', css: ':hover', order: 5 },
+ { name: 'active', type: 'state', css: ':active', order: 6 },
+ { name: 'target', type: 'state', css: ':target', order: 7 },
// State modifiers - form states
- {name: 'autofill', type: 'state', css: ':autofill'},
- {name: 'blank', type: 'state', css: ':blank'},
- {name: 'disabled', type: 'state', css: ':disabled'},
- {name: 'enabled', type: 'state', css: ':enabled'},
- {name: 'checked', type: 'state', css: ':checked'},
- {name: 'indeterminate', type: 'state', css: ':indeterminate'},
- {name: 'default', type: 'state', css: ':default'},
- {name: 'required', type: 'state', css: ':required'},
- {name: 'optional', type: 'state', css: ':optional'},
- {name: 'valid', type: 'state', css: ':valid'},
- {name: 'invalid', type: 'state', css: ':invalid'},
- {name: 'user-valid', type: 'state', css: ':user-valid'},
- {name: 'user-invalid', type: 'state', css: ':user-invalid'},
- {name: 'in-range', type: 'state', css: ':in-range'},
- {name: 'out-of-range', type: 'state', css: ':out-of-range'},
- {name: 'placeholder-shown', type: 'state', css: ':placeholder-shown'},
- {name: 'read-only', type: 'state', css: ':read-only'},
- {name: 'read-write', type: 'state', css: ':read-write'},
+ { name: 'autofill', type: 'state', css: ':autofill' },
+ { name: 'blank', type: 'state', css: ':blank' },
+ { name: 'disabled', type: 'state', css: ':disabled' },
+ { name: 'enabled', type: 'state', css: ':enabled' },
+ { name: 'checked', type: 'state', css: ':checked' },
+ { name: 'indeterminate', type: 'state', css: ':indeterminate' },
+ { name: 'default', type: 'state', css: ':default' },
+ { name: 'required', type: 'state', css: ':required' },
+ { name: 'optional', type: 'state', css: ':optional' },
+ { name: 'valid', type: 'state', css: ':valid' },
+ { name: 'invalid', type: 'state', css: ':invalid' },
+ { name: 'user-valid', type: 'state', css: ':user-valid' },
+ { name: 'user-invalid', type: 'state', css: ':user-invalid' },
+ { name: 'in-range', type: 'state', css: ':in-range' },
+ { name: 'out-of-range', type: 'state', css: ':out-of-range' },
+ { name: 'placeholder-shown', type: 'state', css: ':placeholder-shown' },
+ { name: 'read-only', type: 'state', css: ':read-only' },
+ { name: 'read-write', type: 'state', css: ':read-write' },
// State modifiers - structural
- {name: 'first', type: 'state', css: ':first-child'},
- {name: 'last', type: 'state', css: ':last-child'},
- {name: 'only', type: 'state', css: ':only-child'},
- {name: 'first-of-type', type: 'state', css: ':first-of-type'},
- {name: 'last-of-type', type: 'state', css: ':last-of-type'},
- {name: 'only-of-type', type: 'state', css: ':only-of-type'},
- {name: 'odd', type: 'state', css: ':nth-child(odd)'},
- {name: 'even', type: 'state', css: ':nth-child(even)'},
- {name: 'empty', type: 'state', css: ':empty'},
+ { name: 'first', type: 'state', css: ':first-child' },
+ { name: 'last', type: 'state', css: ':last-child' },
+ { name: 'only', type: 'state', css: ':only-child' },
+ { name: 'first-of-type', type: 'state', css: ':first-of-type' },
+ { name: 'last-of-type', type: 'state', css: ':last-of-type' },
+ { name: 'only-of-type', type: 'state', css: ':only-of-type' },
+ { name: 'odd', type: 'state', css: ':nth-child(odd)' },
+ { name: 'even', type: 'state', css: ':nth-child(even)' },
+ { name: 'empty', type: 'state', css: ':empty' },
// Note: nth-child(N), nth-last-child(N), nth-of-type(N), nth-last-of-type(N) are handled dynamically
// State modifiers - UI states
- {name: 'fullscreen', type: 'state', css: ':fullscreen'},
- {name: 'modal', type: 'state', css: ':modal'},
- {name: 'open', type: 'state', css: ':open'},
- {name: 'popover-open', type: 'state', css: ':popover-open'},
+ { name: 'fullscreen', type: 'state', css: ':fullscreen' },
+ { name: 'modal', type: 'state', css: ':modal' },
+ { name: 'open', type: 'state', css: ':open' },
+ { name: 'popover-open', type: 'state', css: ':popover-open' },
// State modifiers - media states
- {name: 'paused', type: 'state', css: ':paused'},
- {name: 'playing', type: 'state', css: ':playing'},
+ { name: 'paused', type: 'state', css: ':paused' },
+ { name: 'playing', type: 'state', css: ':playing' },
// Pseudo-element modifiers
- {name: 'before', type: 'pseudo-element', css: '::before'},
- {name: 'after', type: 'pseudo-element', css: '::after'},
- {name: 'cue', type: 'pseudo-element', css: '::cue'},
- {name: 'first-letter', type: 'pseudo-element', css: '::first-letter'},
- {name: 'first-line', type: 'pseudo-element', css: '::first-line'},
- {name: 'placeholder', type: 'pseudo-element', css: '::placeholder'},
- {name: 'selection', type: 'pseudo-element', css: '::selection'},
- {name: 'marker', type: 'pseudo-element', css: '::marker'},
- {name: 'file', type: 'pseudo-element', css: '::file-selector-button'},
- {name: 'backdrop', type: 'pseudo-element', css: '::backdrop'},
+ { name: 'before', type: 'pseudo-element', css: '::before' },
+ { name: 'after', type: 'pseudo-element', css: '::after' },
+ { name: 'cue', type: 'pseudo-element', css: '::cue' },
+ { name: 'first-letter', type: 'pseudo-element', css: '::first-letter' },
+ { name: 'first-line', type: 'pseudo-element', css: '::first-line' },
+ { name: 'placeholder', type: 'pseudo-element', css: '::placeholder' },
+ { name: 'selection', type: 'pseudo-element', css: '::selection' },
+ { name: 'marker', type: 'pseudo-element', css: '::marker' },
+ { name: 'file', type: 'pseudo-element', css: '::file-selector-button' },
+ { name: 'backdrop', type: 'pseudo-element', css: '::backdrop' }
];
// Generated lookup maps for efficient access
@@ -269,14 +269,14 @@ export const parse_arbitrary_breakpoint = (segment: string): string | null => {
* @returns object with name (including parameter) and CSS, or null if not parameterized
*/
export const parse_parameterized_state = (
- segment: string,
-): {name: string; css: string; type: 'state'} | null => {
+ segment: string
+): { name: string; css: string; type: 'state' } | null => {
const nth_child_match = NTH_CHILD_PATTERN.exec(segment);
if (nth_child_match) {
return {
name: segment,
css: `:nth-child(${nth_child_match[1]})`,
- type: 'state',
+ type: 'state'
};
}
@@ -285,7 +285,7 @@ export const parse_parameterized_state = (
return {
name: segment,
css: `:nth-last-child(${nth_last_child_match[1]})`,
- type: 'state',
+ type: 'state'
};
}
@@ -294,7 +294,7 @@ export const parse_parameterized_state = (
return {
name: segment,
css: `:nth-of-type(${nth_of_type_match[1]})`,
- type: 'state',
+ type: 'state'
};
}
@@ -303,7 +303,7 @@ export const parse_parameterized_state = (
return {
name: segment,
css: `:nth-last-of-type(${nth_last_of_type_match[1]})`,
- type: 'state',
+ type: 'state'
};
}
@@ -317,8 +317,8 @@ export const parse_parameterized_state = (
* @returns the modifier definition or null if not a known modifier
*/
export const get_modifier = (
- segment: string,
-): (ModifierDefinition & {is_arbitrary?: boolean}) | null => {
+ segment: string
+): (ModifierDefinition & { is_arbitrary?: boolean }) | null => {
// Check static modifiers first
const media = MEDIA_MODIFIERS.get(segment);
if (media) return media;
@@ -339,7 +339,7 @@ export const get_modifier = (
name: segment,
type: 'media',
css: arbitrary_css,
- is_arbitrary: true,
+ is_arbitrary: true
};
}
@@ -348,7 +348,7 @@ export const get_modifier = (
if (parameterized) {
return {
...parameterized,
- is_arbitrary: true,
+ is_arbitrary: true
};
}
diff --git a/src/lib/oklch.ts b/src/lib/oklch.ts
index 19eae2800..d26a435ad 100644
--- a/src/lib/oklch.ts
+++ b/src/lib/oklch.ts
@@ -60,7 +60,7 @@ export const srgb_to_oklab = ([r, g, b]: RgbUnit): Oklab => {
return [
0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
- 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
];
};
@@ -100,7 +100,7 @@ export const oklab_to_oklch = ([lightness, a, b]: Oklab): Oklch => {
export const oklch_to_oklab = ([lightness, chroma, hue]: Oklch): Oklab => [
lightness,
chroma * Math.cos(hue * DEG_TO_RAD),
- chroma * Math.sin(hue * DEG_TO_RAD),
+ chroma * Math.sin(hue * DEG_TO_RAD)
];
/**
diff --git a/src/lib/ramps.ts b/src/lib/ramps.ts
index bb746fceb..cf3627f87 100644
--- a/src/lib/ramps.ts
+++ b/src/lib/ramps.ts
@@ -26,9 +26,9 @@ import {
numeric_scale_variants,
type PaletteVariant,
type ColorSchemeVariant,
- type NumericScaleVariant,
+ type NumericScaleVariant
} from './variable_data.ts';
-import {oklch_max_srgb_chroma, type Oklch} from './oklch.ts';
+import { oklch_max_srgb_chroma, type Oklch } from './oklch.ts';
/** The 13 intensity stops in ramp order (00 → 100). */
export const RAMP_STOPS: ReadonlyArray = numeric_scale_variants;
@@ -79,25 +79,25 @@ export const PALETTE_HUES: Record = {
g: 358, // pink
h: 41, // orange
i: 204, // cyan
- j: 163, // teal
+ j: 163 // teal
};
/** Fitted lightness knobs for the palette (`palette_X_NN`) ramps. */
export const PALETTE_LIGHTNESS_KNOBS: Record = {
- light: {lightness_00: 0.985, lightness_100: 0.116, curve: 1.15},
- dark: {lightness_00: 0.147, lightness_100: 0.971, curve: 0.76},
+ light: { lightness_00: 0.985, lightness_100: 0.116, curve: 1.15 },
+ dark: { lightness_00: 0.147, lightness_100: 0.971, curve: 0.76 }
};
/** Fitted lightness knobs for the shade (surface) ramps. */
export const SHADE_LIGHTNESS_KNOBS: Record = {
- light: {lightness_00: 0.97, lightness_100: 0.171, curve: 1.09},
- dark: {lightness_00: 0.171, lightness_100: 0.97, curve: 0.92},
+ light: { lightness_00: 0.97, lightness_100: 0.171, curve: 1.09 },
+ dark: { lightness_00: 0.171, lightness_100: 0.97, curve: 0.92 }
};
/** Fitted lightness knobs for the text ramps. */
export const TEXT_LIGHTNESS_KNOBS: Record = {
- light: {lightness_00: 0.97, lightness_100: 0.146, curve: 1.19},
- dark: {lightness_00: 0.146, lightness_100: 0.97, curve: 0.87},
+ light: { lightness_00: 0.97, lightness_100: 0.146, curve: 1.19 },
+ dark: { lightness_00: 0.146, lightness_100: 0.97, curve: 0.87 }
};
/**
@@ -114,8 +114,8 @@ export const TEXT_LIGHTNESS_KNOBS: Record = {
- light: {chroma_min: 0.0132, chroma_max: 0.106, curve: 1.3},
- dark: {chroma_min: 0.0214, chroma_max: 0.1088, curve: 1.27},
+ light: { chroma_min: 0.0132, chroma_max: 0.106, curve: 1.3 },
+ dark: { chroma_min: 0.0214, chroma_max: 0.1088, curve: 1.27 }
};
/**
@@ -133,7 +133,7 @@ export const NEUTRAL_HUE = PALETTE_HUES.f;
*/
export const NEUTRAL_CHROMA: Record = {
light: 0.024,
- dark: 0.025,
+ dark: 0.025
};
/**
@@ -160,7 +160,7 @@ export const PALETTE_CHROMA_CAPS: Record<
'80': 0.0533,
'90': 0.0366,
'95': 0.0282,
- '100': 0.0197,
+ '100': 0.0197
},
dark: {
'00': 0.025,
@@ -175,8 +175,8 @@ export const PALETTE_CHROMA_CAPS: Record<
'80': 0.0812,
'90': 0.0465,
'95': 0.03,
- '100': 0.0141,
- },
+ '100': 0.0141
+ }
};
/**
@@ -208,7 +208,7 @@ export const ramp_chroma = (
stop: NumericScaleVariant,
chroma_scale = 1,
knobs: ChromaRampKnobs = PALETTE_CHROMA_KNOBS[scheme],
- cap: number = PALETTE_CHROMA_CAPS[scheme][stop],
+ cap: number = PALETTE_CHROMA_CAPS[scheme][stop]
): number => {
const requested =
knobs.chroma_min + (knobs.chroma_max - knobs.chroma_min) * ramp_chroma_shape(stop, knobs.curve);
@@ -224,7 +224,7 @@ export const ramp_chroma = (
export const ramp_hue_shift_offset = (
stop: NumericScaleVariant,
scheme: ColorSchemeVariant,
- hue_shift: number,
+ hue_shift: number
): number => {
const centered = ramp_stop_t(stop) - 0.5;
return (scheme === 'light' ? centered : -centered) * hue_shift;
@@ -236,11 +236,11 @@ export const ramp_hue_shift_offset = (
export const palette_stop_oklch = (
letter: PaletteVariant,
stop: NumericScaleVariant,
- scheme: ColorSchemeVariant,
+ scheme: ColorSchemeVariant
): Oklch => [
ramp_lightness(PALETTE_LIGHTNESS_KNOBS[scheme], stop),
ramp_chroma(scheme, stop),
- PALETTE_HUES[letter],
+ PALETTE_HUES[letter]
];
/**
@@ -249,7 +249,7 @@ export const palette_stop_oklch = (
export const shade_stop_oklch = (stop: NumericScaleVariant, scheme: ColorSchemeVariant): Oklch => [
ramp_lightness(SHADE_LIGHTNESS_KNOBS[scheme], stop),
NEUTRAL_CHROMA[scheme] * ramp_chroma_shape(stop, PALETTE_CHROMA_KNOBS[scheme].curve),
- NEUTRAL_HUE,
+ NEUTRAL_HUE
];
/**
@@ -258,7 +258,7 @@ export const shade_stop_oklch = (stop: NumericScaleVariant, scheme: ColorSchemeV
export const text_stop_oklch = (stop: NumericScaleVariant, scheme: ColorSchemeVariant): Oklch => [
ramp_lightness(TEXT_LIGHTNESS_KNOBS[scheme], stop),
NEUTRAL_CHROMA[scheme] * ramp_chroma_shape(stop, PALETTE_CHROMA_KNOBS[scheme].curve),
- NEUTRAL_HUE,
+ NEUTRAL_HUE
];
/**
@@ -279,7 +279,7 @@ export const compute_palette_chroma_caps = (
hues: ReadonlyArray,
lightness_knobs: LightnessRampKnobs,
scheme: ColorSchemeVariant,
- hue_shift = 0,
+ hue_shift = 0
): Record => {
const caps = {} as Record;
for (const stop of numeric_scale_variants) {
@@ -315,7 +315,7 @@ const format_ramp_number = (n: number): string => String(Math.round(n * 1e6) / 1
*/
export const render_lightness_stop_css = (
family: RampFamily,
- stop: NumericScaleVariant,
+ stop: NumericScaleVariant
): string => {
const p = `--${family}_lightness`;
const t = format_ramp_number(ramp_stop_t(stop));
@@ -346,7 +346,7 @@ export const render_chroma_shape_css = (stop: NumericScaleVariant): string => {
export const render_chroma_stop_css = (
stop: NumericScaleVariant,
scheme: ColorSchemeVariant,
- cap: number = PALETTE_CHROMA_CAPS[scheme][stop],
+ cap: number = PALETTE_CHROMA_CAPS[scheme][stop]
): string => {
const cap_str = format_ramp_number(cap);
return `min(calc(var(--palette_chroma_min) + (var(--palette_chroma_max) - var(--palette_chroma_min)) * var(--chroma_shape_${
@@ -362,7 +362,7 @@ export const render_chroma_stop_css = (
*/
export const render_hue_shift_offset_css = (
stop: NumericScaleVariant,
- scheme: ColorSchemeVariant,
+ scheme: ColorSchemeVariant
): string => {
const centered = ramp_stop_t(stop) - 0.5;
const factor = Math.round((scheme === 'light' ? centered : -centered) * 1e6) / 1e6;
@@ -376,7 +376,7 @@ export const render_hue_shift_offset_css = (
*/
export const render_palette_stop_css = (
letter: PaletteVariant,
- stop: NumericScaleVariant,
+ stop: NumericScaleVariant
): string => render_ramp_color_css(`var(--hue_${letter})`, stop);
/**
@@ -390,7 +390,7 @@ export const render_palette_stop_css = (
export const render_ramp_color_css = (
hue_reference: string,
stop: NumericScaleVariant,
- alpha?: string,
+ alpha?: string
): string =>
`oklch(var(--palette_lightness_${stop}) calc(var(--palette_chroma_${
stop
@@ -404,7 +404,7 @@ export const render_ramp_color_css = (
*/
export const render_neutral_stop_css = (
family: Exclude,
- stop: NumericScaleVariant,
+ stop: NumericScaleVariant
): string =>
`oklch(var(--${family}_lightness_${stop}) calc(var(--neutral_chroma) * var(--chroma_shape_${
stop
diff --git a/src/lib/style_rule_parser.ts b/src/lib/style_rule_parser.ts
index 9d7bc2915..5eab7a754 100644
--- a/src/lib/style_rule_parser.ts
+++ b/src/lib/style_rule_parser.ts
@@ -9,12 +9,12 @@
* @module
*/
-import {parseCss, type AST} from 'svelte/compiler';
-import {hash_blake3} from '@fuzdev/fuz_util/hash_blake3.ts';
+import { parseCss, type AST } from 'svelte/compiler';
+import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
-import {extract_css_variables} from './css_variable_utils.ts';
-import type {CacheDeps} from './deps.ts';
-import type {BaseCssOption} from './css_plugin_options.ts';
+import { extract_css_variables } from './css_variable_utils.ts';
+import type { CacheDeps } from './deps.ts';
+import type { BaseCssOption } from './css_plugin_options.ts';
/**
* Base fields shared by all style rules.
@@ -36,13 +36,7 @@ export interface StyleRuleBase {
* Reasons a rule is considered "core" and always included.
*/
export type CoreReason =
- | 'universal'
- | 'root'
- | 'body'
- | 'media_query'
- | 'html'
- | 'host'
- | 'font_face';
+ 'universal' | 'root' | 'body' | 'media_query' | 'html' | 'host' | 'font_face';
/**
* A core style rule that is always included in output.
@@ -122,7 +116,7 @@ export const parse_style_css = (css: string, content_hash: string): StyleRuleInd
};
const walk_children = (
- children: Iterable,
+ children: Iterable
): void => {
for (const child of children) {
if (child.type === 'Rule') {
@@ -154,7 +148,7 @@ export const parse_style_css = (css: string, content_hash: string): StyleRuleInd
rules,
by_element,
by_class,
- content_hash,
+ content_hash
};
};
@@ -175,7 +169,7 @@ const extract_style_rule = (rule: AST.CSS.Rule, css: string, order: number): Sty
const variables_used = extract_css_variables(block_css);
// Determine if core rule
- const {is_core, core_reason} = check_core_rule(selector_css, elements);
+ const { is_core, core_reason } = check_core_rule(selector_css, elements);
// Type assertion needed because destructuring widens is_core to boolean
return {
@@ -185,7 +179,7 @@ const extract_style_rule = (rule: AST.CSS.Rule, css: string, order: number): Sty
variables_used,
order,
is_core,
- core_reason,
+ core_reason
} as StyleRule;
};
@@ -197,7 +191,7 @@ const extract_nested_rules = (
css: string,
elements: Set,
classes: Set,
- variables_used: Set,
+ variables_used: Set
): void => {
for (const child of block.children) {
if (child.type === 'Rule') {
@@ -240,7 +234,7 @@ const extract_atrule = (atrule: AST.CSS.Atrule, css: string, order: number): Sty
variables_used,
order,
is_core: true,
- core_reason: 'media_query',
+ core_reason: 'media_query'
} as const;
}
@@ -251,7 +245,7 @@ const extract_atrule = (atrule: AST.CSS.Atrule, css: string, order: number): Sty
variables_used,
order,
is_core: false,
- core_reason: null,
+ core_reason: null
} as const;
}
@@ -271,7 +265,7 @@ const extract_atrule = (atrule: AST.CSS.Atrule, css: string, order: number): Sty
variables_used,
order,
is_core: false,
- core_reason: null,
+ core_reason: null
} as const;
}
@@ -290,7 +284,7 @@ const extract_atrule = (atrule: AST.CSS.Atrule, css: string, order: number): Sty
variables_used,
order,
is_core: true,
- core_reason: 'font_face',
+ core_reason: 'font_face'
} as const;
}
@@ -310,7 +304,7 @@ const extract_atrule = (atrule: AST.CSS.Atrule, css: string, order: number): Sty
const parse_selector_list = (
selector_css: string,
elements: Set,
- classes: Set,
+ classes: Set
): void => {
// Split on commas, respecting parentheses
const selectors = split_selector_list(selector_css);
@@ -369,8 +363,8 @@ const split_selector_list = (selector_group: string): Array => {
*/
const extract_functional_content = (
selector: string,
- start: number,
-): {content: string; end: number} | null => {
+ start: number
+): { content: string; end: number } | null => {
let depth = 1;
let i = start;
@@ -385,7 +379,7 @@ const extract_functional_content = (
return {
content: selector.slice(start, i - 1),
- end: i,
+ end: i
};
};
@@ -396,12 +390,12 @@ const extract_functional_content = (
const parse_single_selector = (
selector: string,
elements: Set,
- classes: Set,
+ classes: Set
): void => {
// Find all functional pseudo-classes and extract their content iteratively
const functional_start_pattern = /:(?:where|is|not|has)\(/g;
let match;
- const ranges_to_remove: Array<{start: number; end: number}> = [];
+ const ranges_to_remove: Array<{ start: number; end: number }> = [];
while ((match = functional_start_pattern.exec(selector)) !== null) {
const content_start = match.index + match[0].length;
@@ -409,7 +403,7 @@ const parse_single_selector = (
if (result) {
// Recursively parse the inner content
parse_selector_list(result.content, elements, classes);
- ranges_to_remove.push({start: match.index, end: result.end});
+ ranges_to_remove.push({ start: match.index, end: result.end });
// Update the regex lastIndex to continue after this match
functional_start_pattern.lastIndex = result.end;
}
@@ -445,7 +439,8 @@ const parse_single_selector = (
* Result from core rule check - discriminated union for type safety.
* Both variants include `core_reason` for consistent object shape.
*/
-type CoreRuleCheck = {is_core: true; core_reason: CoreReason} | {is_core: false; core_reason: null};
+type CoreRuleCheck =
+ { is_core: true; core_reason: CoreReason } | { is_core: false; core_reason: null };
/**
* Checks if a rule is a "core" rule that should always be included.
@@ -458,30 +453,30 @@ type CoreRuleCheck = {is_core: true; core_reason: CoreReason} | {is_core: false;
const check_core_rule = (selector_css: string, elements: Set): CoreRuleCheck => {
// Universal selector
if (selector_css.includes('*')) {
- return {is_core: true, core_reason: 'universal'};
+ return { is_core: true, core_reason: 'universal' };
}
// :root pseudo-class
if (selector_css.includes(':root')) {
- return {is_core: true, core_reason: 'root'};
+ return { is_core: true, core_reason: 'root' };
}
// :host pseudo-class (for web components)
if (selector_css.includes(':host')) {
- return {is_core: true, core_reason: 'host'};
+ return { is_core: true, core_reason: 'host' };
}
// body element
if (elements.has('body')) {
- return {is_core: true, core_reason: 'body'};
+ return { is_core: true, core_reason: 'body' };
}
// html element
if (elements.has('html')) {
- return {is_core: true, core_reason: 'html'};
+ return { is_core: true, core_reason: 'html' };
}
- return {is_core: false, core_reason: null};
+ return { is_core: false, core_reason: null };
};
/**
@@ -493,10 +488,10 @@ const check_core_rule = (selector_css: string, elements: Set): CoreRuleC
*/
export const load_style_rule_index = async (
deps: CacheDeps,
- style_css_path?: string,
+ style_css_path?: string
): Promise => {
const path = style_css_path ?? new URL('./style.css', import.meta.url).pathname;
- const r = await deps.read_text({path});
+ const r = await deps.read_text({ path });
if (!r.ok) {
throw new Error(`Failed to read style.css from ${path}: ${r.message}`);
}
@@ -525,10 +520,10 @@ export const create_style_rule_index = (css: string): StyleRuleIndex => {
*/
export const load_default_style_css = async (
deps: CacheDeps,
- style_css_path?: string,
+ style_css_path?: string
): Promise => {
const path = style_css_path ?? new URL('./style.css', import.meta.url).pathname;
- const r = await deps.read_text({path});
+ const r = await deps.read_text({ path });
if (!r.ok) {
throw new Error(`Failed to read style.css from ${path}: ${r.message}`);
}
@@ -545,7 +540,7 @@ export const load_default_style_css = async (
*/
export const resolve_base_css_option = async (
base_css: BaseCssOption,
- deps: CacheDeps,
+ deps: CacheDeps
): Promise => {
// null = disabled
if (base_css === null) {
@@ -579,7 +574,7 @@ export const resolve_base_css_option = async (
export const get_matching_rules = (
index: StyleRuleIndex,
detected_elements: Set,
- detected_classes: Set,
+ detected_classes: Set