diff --git a/.changeset/dev-initial-load-prescan.md b/.changeset/dev-initial-load-prescan.md new file mode 100644 index 000000000..45bd69c24 --- /dev/null +++ b/.changeset/dev-initial-load-prescan.md @@ -0,0 +1,11 @@ +--- +'@fuzdev/fuz_css': patch +--- + +fix: complete utility CSS on the first dev page load + +The Vite plugin now 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. diff --git a/.changeset/micro-surface-hooks.md b/.changeset/micro-surface-hooks.md new file mode 100644 index 000000000..0ebc9f6c3 --- /dev/null +++ b/.changeset/micro-surface-hooks.md @@ -0,0 +1,21 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: theme scrollbars, caret, dialog backdrop, and the OS contrast preference + +New micro-surface defaults in `style.css`, each themable through a `var()` +fallback hook (all registered theme variables): + +- `scrollbar-color` on `:root` — thumb defaults to `var(--shade_40)` on a + transparent track; hooks `--scrollbar_thumb_color` / + `--scrollbar_track_color` +- `caret-color` on text inputs — defaults to `var(--accent_50)`; hook + `--caret_color` +- `dialog::backdrop` — defaults to `var(--darken_60)`; hook + `--backdrop_color` +- `@media (prefers-contrast: more)` maps the OS preference onto the curve + knobs, mirroring the `'high contrast'` theme; theme overrides beat it + +Also tunes the `'low contrast'` theme's shade compression to the softest +values that pass every `check_theme` contrast gate. diff --git a/.changeset/oklch-color-system.md b/.changeset/oklch-color-system.md new file mode 100644 index 000000000..625902baf --- /dev/null +++ b/.changeset/oklch-color-system.md @@ -0,0 +1,60 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: rework the color system to derived OKLCH, add semantic intents, cascade layers, and themes + +Colors are now derived — curve knobs → ramp stops → color stops — in pure +CSS (`calc()`/`pow()`/`oklch()`), fitted to minimize the perceptual delta +from the old HSL palette. Breaking changes: + +- **`color_` renamed to `palette_`**: `--color_a_50` → `--palette_a_50`, + `.color_a_50` → `.palette_a_50`, `.color_a`–`.color_j` → + `.palette_a`–`.palette_j`. In compound families the letter alone implies + the palette: `border_color_X_NN` → `border_X_NN`, `outline_color_X_NN` → + `outline_X_NN`, `shadow_color_X_NN` → `shadow_X_NN` (`bg_X_NN` and the + letterless families — `border_color_NN`, `outline_color_NN`, + `shadow_color_umbra` — keep their names). In TS: + `ColorVariant`/`color_variants` → `PaletteVariant`/`palette_variants`. +- **`--hue_a`…`--hue_j` are now OKLCH hue angles** (blue is `250`, not HSL + `210`). Consumer CSS doing `hsl(var(--hue_x) …)` breaks — use + `oklch( var(--hue_x))` or the palette/intent stops. +- **`--tint_hue`/`--tint_saturation` removed** → `--hue_neutral` (defaults + to `var(--hue_f)`) + `--neutral_chroma`. +- **Absolute `_light`/`_dark` variants removed**: the ~286 generated + variables (`--color_a_50_light`-style, `--shade_XX_light/dark`) and all + their classes. 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 per-stop derived variables themes + can pin individually (`--palette_lightness_NN`, `--palette_chroma_NN`, + `--chroma_shape_NN`, `--hue_shift_NN`). +- **New semantic intent knobs**: `--hue_accent`, `--hue_positive`, + `--hue_negative`, `--hue_caution`, `--hue_info`, each deriving a 13-stop + scale through the shared ramps (`--accent_00`…`--accent_100`, etc.) with + matching token classes (`.positive_50`, `.bg_caution_10`), plus + `--selection_color` and `intent_variants`/`IntentVariant` in + `variable_data.ts`. Links, focus, selection, `accent-color`, and + disabled-active feedback route through them; focus follows the element + color (via `--outline_color`) with the accent as fallback. +- **Cascade layers**: all shipped CSS is layered `fuz.base` < `fuz.theme` < + `fuz.utilities`; consumers' unlayered styles beat everything. + `render_theme_style` loses `specificity` (the `:root:root` hack) and gains + `layer?: string | null` (default `'fuz.theme'`); `generate_theme_css` + loses its specificity parameter; the `theme_specificity` generator option + is removed. The default-theme special case now keys on empty `variables` + rather than the `'base'` name. Custom `base_css` is re-layered into + `fuz.base` in bundled output. +- **`color-mix()` interpolation moved from `in hsl` to `in oklab`** in + button fills/borders, composites, and shadow classes. +- **Themes**: `themes.ts` exports the curated `default_themes` registry + (base, low contrast, high contrast — the contrast themes are rewritten as + curve-knob overrides), one module per theme under `themes/`, plus + unregistered exemplars (necromancer, sunset ember, brutalish, terminal + green + the `create_terminal_theme(hue)` factory). +- **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. diff --git a/.changeset/theme-knobs-and-checks.md b/.changeset/theme-knobs-and-checks.md new file mode 100644 index 000000000..39ff32431 --- /dev/null +++ b/.changeset/theme-knobs-and-checks.md @@ -0,0 +1,33 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: add theme scale knobs, the knob catalog, and theme validation/gates/compile + +New theme knobs, each derived into existing token defaults so one knob move +reshapes a whole family while individual tokens stay pinnable: +`--shadow_alpha_scale` (multiplies the `shadow_alpha_*` ramp, including +button shadows), `--radius_scale` (multiplies the `border_radius_*` tiers), +`--scale_factor` (multiplies the `space_*` scale), `--font_weight` (body), +`--heading_font_weight` (a hook with per-tier fallbacks — setting it +flattens the heading ladder), `--heading_font_family`, and +`--background_image` (decoration hook on `:root`). + +New `knobs.ts`: `theme_knobs`, a typed catalog of the theme-facing knobs +(`kind`, `axis`, `leverage`, `tier`, `bindable`, ranges), which powers the +inline theme editor on the themes docs page. `variable_data.ts` gains +`palette_glosses`, the letter → color/default-intent display data. + +New `theme_check.ts`, resolving a theme's authored values back to numbers +(literals, `var(--hue_x)` binding chains, compiled-cap overrides): + +- `validate_theme` — structural lint: shape and unknown-name errors, plus + advisory type/range warnings for the knob-tier variables +- `check_theme` — report-only gamut, ramp-monotonicity, and contrast gates + (the same thresholds the repo's tests assert, exported as the `GATE_*` + constants) +- `compile_theme` — recomputes per-stop worst-hue chroma caps from a theme's + own hues, lightness ramp, and hue shift, emits `palette_chroma_NN` + overrides where the baked caps no longer fit, and re-checks the result + +`ramps.ts` gains `ramp_hue_shift_offset` and `compute_palette_chroma_caps`. diff --git a/.changeset/theme-scheme-stance.md b/.changeset/theme-scheme-stance.md new file mode 100644 index 000000000..3a546a2f7 --- /dev/null +++ b/.changeset/theme-scheme-stance.md @@ -0,0 +1,18 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: add the theme scheme stance + +`Theme` gains `scheme?: 'dual' | 'light' | 'dark'` (default `'dual'`). A +single-scheme theme renders its one appearance in both color schemes: +`render_theme_style` mirrors every scheme-adaptive default the theme doesn't +override (exported as `scheme_stance_variables`, including the +`palette_chroma_NN` gamut-cap stops) and pins `color-scheme` on the scope so +form controls and native scrollbars agree. Author a stanced theme's own +variables single-slot in the light/base position. + +`validate_theme` warns on dark slots a single-scheme stance makes +meaningless; `check_theme`/`compile_theme` resolve through the same mirror +so the gates evaluate the stanced reality in both schemes. The necromancer +and terminal exemplars use the stance instead of hand-mirrored ramp knobs. diff --git a/CLAUDE.md b/CLAUDE.md index e0442022e..0d22abfa5 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/RampStrip.svelte b/src/routes/RampStrip.svelte new file mode 100644 index 000000000..3640f30de --- /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/SourceFileLink.svelte b/src/routes/SourceFileLink.svelte index 3ff0227d4..dd0d8c9ed 100644 --- a/src/routes/SourceFileLink.svelte +++ b/src/routes/SourceFileLink.svelte @@ -1,5 +1,5 @@ + +{#snippet knob_control(knob: ThemeKnob, compact: boolean)} + editor.set_value(knob.name, value, editing_scheme)} + onreset={() => editor.reset(knob.name)} + /> +{/snippet} + +
+
+
+ + + +
+ {editor.is_palette_tier ? 'palette-tier' : 'semantic-tier'} + {#if editor.dirty} + + {/if} +
+
+ {#if name_collides} + + {/if} +
+ + {#if editor.stance} + single-scheme theme - edits write to the base slots and the {editor.stance} appearance renders in both color schemes + {:else} + edits write to the {editing_scheme} scheme's slots + {/if} +
+
+ + +
+
+ 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 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}

+ {#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'} +
+ palette hues (the letter slots - moving these makes the theme palette-tier) +
+ {#each palette_variants as letter (letter)} + + {/each} +
+
+ {#each palette_knobs as knob (knob.name)} + {@render knob_control(knob, false)} + {/each} +
+
+ {/if} +
+ {/if} + {/each} + +
+

Output

+

+ The copyable Theme object, and the CSS it renders — the theme's own footprint. +

+
+
+ +
+ +
+ {#if editor.output.variables.length} +
+
+ +
+ +
+ {:else} +

+ The theme is empty -- every variable keeps its base default, so it renders no CSS. Move a + knob to see its output. +

+ {/if} +
+
+ + diff --git a/src/routes/ThemeForm.svelte b/src/routes/ThemeForm.svelte deleted file mode 100644 index 0f56da08c..000000000 --- a/src/routes/ThemeForm.svelte +++ /dev/null @@ -1,162 +0,0 @@ - - -
-

- {#if editing}edit{:else}create{/if} theme -

- -
-
-

variables: {light_count} light, {dark_count} dark

- -
-
- - -
-
-
-
- {#each new_variables as variable (variable.name)} - - {/each} -
-
- {#if code} -
- -
- - {/if} -
-
-
-{#if selected_variable} - (selected_variable = null)}> - - {#snippet children({close})} -
- - - -
- {/snippet} -
-
-{/if} - - diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index c75176ae5..b12113b73 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,14 +1,14 @@