diff --git a/.changeset/dev-initial-load-prescan.md b/.changeset/dev-initial-load-prescan.md new file mode 100644 index 000000000..9a2815a1b --- /dev/null +++ b/.changeset/dev-initial-load-prescan.md @@ -0,0 +1,13 @@ +--- +'@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. Files are +isolated during the scan: one file that fails to read or extract logs and +is skipped rather than aborting the rest of the scan. diff --git a/.changeset/interaction-and-surfaces.md b/.changeset/interaction-and-surfaces.md new file mode 100644 index 000000000..c017cf192 --- /dev/null +++ b/.changeset/interaction-and-surfaces.md @@ -0,0 +1,61 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: rework interaction states and micro-surfaces - focus ring gap, hover previews focus, themable surface variables, contrast-safe selected buttons + +Focus and hover now read as one escalating highlight: + +- Focusable elements (links, buttons, inputs, contenteditable) draw their + outline with `outline-offset: var(--outline_offset)` (default `1px`). The + ring and the border share a color by design (the element color, defaulting + to the accent), so without a gap they merged into one thick band; the + offset makes them read as two shapes while keeping the ring's full + contrast against the page. +- Hovering an input, textarea, or select colors the border with + `var(--outline_color)` — the element color when one is set (an + `outline_*` class, or `.palette_*` on buttons), the accent intent + otherwise — instead of fading it to the weaker `--border_color_20` + alpha. Focus keeps setting the border to the same color and adds the + outline. Disabled inputs no longer react to hover. + +New micro-surface defaults in `style.css`, each themable through an +ordinary declared variable (all in the knob catalog): + +- `scrollbar-color` on `:root` — `--scrollbar_thumb_color` (default + `var(--shade_40)`) on `--scrollbar_track_color` (default transparent) +- `caret-color` on text inputs — `--caret_color`, default `var(--accent_50)` +- `dialog::backdrop` — `--backdrop_color`, default `var(--darken_60)` +- `--outline_offset` — the border-to-focus-ring gap above +- `@media (prefers-contrast: more)` maps the OS preference onto the curve + knobs, mirroring the `'high contrast'` theme. It ships in the + `fuz.preferences` cascade layer (as does the `prefers-reduced-motion` + duration suppression), above the `fuz.base` defaults and below + `fuz.theme`, so it applies regardless of stylesheet order and theme + overrides beat it + +(`--heading_font_weight` is the lone `var()`-fallback hook — its per-tier +fallbacks mean no single declared default exists, and setting it flattens +the heading weight ladder deliberately.) + +Selected-button text stays readable under contrast-bent themes and colored +fills: + +- Selected buttons use `--text_00` (the text ramp endpoint) instead of + `--text_05`/`--text_10` for inverse text. Themes bending + `--text_lightness_curve` — the high-contrast modifier, the OS + `prefers-contrast: more` mapping — drag the near-background stops toward + the fill lightness, washing selected text out (down to ~1.2:1); the + endpoints are the knobs themselves, so the curve can never move them. +- Colored buttons (`.palette_X`) fill with `palette_X_50` instead of + `palette_X_40`, matching the neutral `shade_50` selected fill — stop-40 + fills leave light-scheme inverse text at ~2.5:1, below the 3:1 + large-text floor (disabled-active feedback fills with `negative_50` for + the same reason). The selected border now matches the fill, rendering + flat. Unselected tint fills mix from the same `--fill`, so they read + slightly richer. +- A matching contrast gate in `check_theme` — `GATE_SELECTED_TEXT`: + `text_00` on `shade_50` and on every stop-50 fill must clear 3:1 — keeps + these pairings from silently regressing. The selected-deselectable hover + uses `--text_min`, the text-semantic twin of `--shade_min` (identical + values). diff --git a/.changeset/oklch-color-system.md b/.changeset/oklch-color-system.md new file mode 100644 index 000000000..ca4349cec --- /dev/null +++ b/.changeset/oklch-color-system.md @@ -0,0 +1,89 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: rework the color system to derived OKLCH with semantic intents and cascade layers + +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_*` variables renamed to `--palette_*`**: `--color_a_50` → + `--palette_a_50` for all 10 letters × 13 stops, with the TS mirrors + `ColorVariant`/`color_variants` → `PaletteVariant`/`palette_variants`. + Classes are property-first and the letter alone implies the palette: the + text-color stop classes keep their names (`.color_a_50`, now applying + `--palette_a_50`), while the compound families shorten - + `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). The bare component conventions + `.color_a`-`.color_j` → `.palette_a`-`.palette_j` (they recolor buttons + and chips as a unit, not one property). +- **`.fg_NN`/`.bg_NN` token classes removed**: the adaptive alpha overlays + stay as variables (`--fg_*`/`--bg_*`) but no longer generate bare token + classes — `bg_` is the opaque background class prefix (`.bg_a_50`, + `.bg_positive_50`), and a translucent `.bg_50` beside those was the one + collision in the naming family. Use literals instead: + `background-color:var(--fg_10)`. The `.darken_NN`/`.lighten_NN` classes + are unchanged. +- **`--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. +- **`color-mix()` interpolation moved from `in hsl` to `in oklab`** in + button fills/borders, composites, and shadow classes. +- **Cascade layers**: all shipped CSS is layered `fuz.base` < + `fuz.preferences` < `fuz.theme` < `fuz.utilities`, so consumers' unlayered + styles beat everything. The OS user-preference mappings + (`prefers-contrast`, `prefers-reduced-motion`) live in `fuz.preferences`, + above the defaults and below themes, so they apply in every consumption + mode and explicit theme overrides still win. Custom `base_css` is + re-layered into `fuz.base` in bundled output (only the `fuz.preferences` + identity is preserved). + +New: + +- **Curve knobs** (the promoted theme API): `--chroma_scale`, + `--palette_lightness_00/_100/_curve` (same trio for `shade_`/`text_`), + and `--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`). +- **Semantic intent knobs**: `--hue_accent`, `--hue_positive`, + `--hue_negative`, `--hue_caution`, `--hue_info`, each deriving a full + 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. +- **Per-slot chroma character**: `--palette_a_chroma_scale` … + `--palette_j_chroma_scale` and intent twins (`--accent_chroma_scale`, + same for positive/negative/caution/info), default `1`, each multiplying + its slot's chroma under the global `--chroma_scale` so the slot's + character holds at any global setting — grayscale stays grayscale and + vivid scales proportionally. Values at or below 1 stay inside the sRGB + gamut caps by construction; above 1 knowingly clips, like the global + knob. The brown slot `f` ships at `0.55`: brown is low-chroma dark + orange, so under uniform chroma it read as a second orange beside + `--hue_h`. An intent hue bound to a palette letter shares only the angle + — `validate_theme` warns when the bound letter's multiplier differs from + the intent's twin, and `check_theme` runs its gates through the + multipliers. +- **Derived border colors**: the `border_color_*` alpha ramp colors through + the neutral intent — new `--border_color_lightness` and + `--border_color_chroma` knobs, the chroma derived from `--neutral_chroma` + — so grayscale and retinted themes reshape borders in the same move as + surfaces and text. +- **Design-time modules**: `ramps.ts` (fitted knob constants, numeric + evaluators, CSS emitters), `oklch.ts` (OKLCH↔sRGB + gamut math), and + `wcag.ts` (luminance/contrast), with tests gating every default stop for + gamut, monotonicity, and contrast. diff --git a/.changeset/theme-authoring.md b/.changeset/theme-authoring.md new file mode 100644 index 000000000..15ef1b66c --- /dev/null +++ b/.changeset/theme-authoring.md @@ -0,0 +1,99 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: rework themes - registry and contrast modifiers, scheme stance, knob catalog, validation/gates/compile, pure renderer, build-time `theme` option + +Themes are now knob-first: a theme moves the derived color system's curve +and scale knobs, composes with modifiers, validates and gates like the +shipped defaults, and applies at runtime or build time. + +**Registry and modifiers.** `themes.ts` exports the curated +`default_themes` registry (just base) and `contrast_modifiers`: low/high +contrast are curve-knob fragments composed over any theme with the new +`compose_themes(base, ...overlays)` (flatten + last-wins; a single-scheme +base re-slots dual-slot overlay variables to its stance), not themes in the +list. Themes live one module per theme under `themes/`, with unregistered +exemplars: necromancer, sunset ember, brutalish, and terminalien. Low +contrast is tuned to the softest compression that passes every +`check_theme` contrast gate. + +**Scheme stance.** `Theme` gains `scheme?: 'dual' | 'light' | 'dark'` +(default `'dual'`). A single-scheme theme renders its one appearance in +both color schemes by mirroring every scheme-adaptive default it doesn't +override and pinning `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 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 terminalien exemplars are dark-only via the stance. + +**Pure renderer.** `theme.ts` no longer depends on `variables.ts`, so +mounting a theme stops pulling the full derived variable set into the +bundle: minified, `theme.ts` drops from ~38KB to ~1.3KB (~9KB to ~0.7KB +gzipped). Breaking: + +- `RenderThemeStyleOptions.empty_default_theme` is removed — to render the + full defaults, pass them: + `render_theme_style({name: 'base', variables: default_variables})`. The + default-theme special case now keys on empty `variables` rather than the + `'base'` name. +- `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 stance mirror is computed before rendering, not by the renderer: + `resolve_theme_stance` (new `theme_stance.ts`, replacing `theme.ts`'s + `scheme_stance_variables`) stores it on the theme's `scheme_mirror` + field. The shipped stanced exemplars resolve at module scope, so only + hand-rolled stanced themes need the call. + +**Theme knobs and the catalog.** New scale knobs derive into existing token +defaults so one knob move reshapes a whole family while individual tokens +stay pinnable: `--shadow_alpha_scale` (the `shadow_alpha_*` ramp, button +shadows included), `--radius_scale` (the `border_radius_*` tiers), +`--scale_factor` (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 the +`--background_image` decoration hook on `:root`. New `knobs.ts` catalogs +the theme-facing knobs with typed metadata (`kind`, `axis`, `leverage`, +`tier`, `bindable`, ranges), powering the inline theme editor on the themes +docs page; `variable_data.ts` gains `palette_glosses`, the letter → +color/default-intent display data. + +**Validation, gates, compile.** New `theme_check.ts` resolves 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, with the thresholds exported as the `GATE_*` constants +- `compile_theme` — recomputes per-stop worst-hue chroma caps from a + theme's own hues and lightness ramp, emits `palette_chroma_NN` overrides + where the baked caps no longer fit, and re-checks the result + +The shipped themes pass these in CI - including every theme × +contrast-modifier composition - and the docs page's inline editor runs the +same lint and gates live on every edit. + +**Build-time `theme` option.** The Vite plugin and Gro generator take a +`theme` baked into the generated CSS: + +```ts +import {necromancer_theme} from '@fuzdev/fuz_css/themes/necromancer.ts'; + +vite_plugin_fuz_css({theme: necromancer_theme}); +``` + +The theme overlays the resolved `variables` last-wins by name, so its +values flow through the dependency graph like any other - the variables a +theme references are pulled in transitively and the output stays +tree-shaken. A single-scheme theme's `scheme_mirror` applies first, +matching the renderer's order. This is the static counterpart to fuz_ui's +`ThemeRoot`: no runtime theme rendering, no JavaScript shipped; the two +compose, the runtime theme winning by cascade layer. Pinning +`color-scheme` stays separate - the `dark`/`light` class on the root +element drives it. Also exposes `apply_theme_variables` from +`variable_graph.ts`; `build_variable_graph_from_options` takes an optional +theme. diff --git a/CLAUDE.md b/CLAUDE.md index e834cd228..5e77a2d84 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/ThemeEditor.svelte b/src/routes/ThemeEditor.svelte new file mode 100644 index 000000000..bed7a43a8 --- /dev/null +++ b/src/routes/ThemeEditor.svelte @@ -0,0 +1,385 @@ + + +{#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} + {@const sm_meta = sm_details[axis]} +
+ + {sm_meta?.title ?? 'granular tokens'} + ({sm_meta?.hint ?? '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} + +
+

Gates

+

+ validate_theme and check_theme run live against the draft - the same + lint and gamut/monotonicity/contrast gates the shipped themes pass in CI. +

+ {#if editor.issues.length === 0 && + failing_gates.length === 0 && + editor.check_report.unchecked.length === 0 + } +

+ all gates pass ({editor.check_report.entries.length} checks) +

+ {:else} + {#if editor.issues.length} +
    + {#each editor.issues as issue, i (i)} +
  • + {issue.level}: {issue.message} +
  • + {/each} +
+ {/if} + {#if failing_gates.length} +
    + {#each failing_gates as entry, i (i)} +
  • + {entry.gate} · {entry.scheme} · {entry.subject}: {format_gate_value(entry)} +
  • + {/each} +
+ {/if} + {#if editor.check_report.unchecked.length} +
+ + {editor.check_report.unchecked.length} unchecked + (values the numeric gates can't resolve) + + +
    + {#each editor.check_report.unchecked as u (u.variable + u.value)} +
  • {u.variable}: {u.value} ({u.reason})
  • + {/each} +
+
+ {/if} + {/if} +
+ +
+

Output

+

+ The copyable Theme object, and the CSS it renders - the variables the theme sets, + plus the scheme-stance mirror when the theme is single-scheme. +

+
+
+ +
+ +
+ {#if editor.output.variables.length || editor.output.scheme_mirror?.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 58bc3b7b7..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/docs/borders/+page.svelte b/src/routes/docs/borders/+page.svelte index 867eba686..ec8730ee3 100644 --- a/src/routes/docs/borders/+page.svelte +++ b/src/routes/docs/borders/+page.svelte @@ -10,7 +10,7 @@ import { border_width_variants, - color_variants, + palette_variants, outline_width_variants, border_radius_variants, alpha_variants, @@ -18,6 +18,7 @@ type IntensityVariant } from '$lib/variable_data.ts'; import UnfinishedImplementationWarning from '$routes/docs/UnfinishedImplementationWarning.svelte'; + import ResolvedColorCode from '$routes/docs/borders/ResolvedColorCode.svelte'; const LIBRARY_ITEM_NAME = 'borders'; @@ -46,10 +47,6 @@ let selected_intensity: IntensityVariant = $state.raw('50'); - // @fuz-classes outline_width_focus outline_width_active - // @fuz-classes border_color_00 border_color_05 border_color_10 border_color_20 border_color_30 border_color_40 border_color_50 border_color_60 border_color_70 border_color_80 border_color_90 border_color_95 border_color_100 - // @fuz-classes color_a_50 color_b_50 color_c_50 color_d_50 color_e_50 color_f_50 color_g_50 color_h_50 color_i_50 color_j_50 - // TODO @many smoother gradient? esp on the low end? for both shadows and borders @@ -65,7 +62,7 @@

Border variables integrate with the theme system and adapt - to color scheme. Alpha borders are tuned for visual balance -- dark mode uses higher alpha + to color scheme. Alpha borders are tuned for visual balance: dark mode uses higher alpha because light-on-dark has lower perceived contrast.

@@ -73,7 +70,7 @@

The border_color_NN variables provide tinted alpha borders that integrate with - the theme. They use tint_hue for cohesion. + the theme. They use hue_neutral for cohesion.

{#each alpha_variants as v (v)} @@ -123,8 +120,8 @@ border-color: var(--shade_30);

- Use color variables like color_a_{selected_intensity} for colored borders. The - intensity controls the color's prominence. + Use palette variables like palette_a_{selected_intensity} for colored borders. + The intensity controls the color's prominence.

@@ -144,9 +141,9 @@ border-color: var(--shade_30);
- {#each color_variants as color_variant (color_variant)} - {@const name = 'color_' + color_variant + '_' + selected_intensity} - {@const text_class = 'color_' + color_variant + '_50'} + {#each palette_variants as letter (letter)} + {@const name = 'palette_' + letter + '_' + selected_intensity} + {@const text_class = 'palette_' + letter + '_50'}
{/each}
-
- = - {computed_styles?.getPropertyValue('--' + name)} - +
+ =
{/each} @@ -193,7 +188,7 @@ border-color: var(--shade_30);

Each border utility class has a corresponding outline variant using the same border variables - (like outline_color_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/borders/ResolvedColorCode.svelte b/src/routes/docs/borders/ResolvedColorCode.svelte new file mode 100644 index 000000000..b3ffa312a --- /dev/null +++ b/src/routes/docs/borders/ResolvedColorCode.svelte @@ -0,0 +1,44 @@ + + +{formatted} + + diff --git a/src/routes/docs/buttons/+page.svelte b/src/routes/docs/buttons/+page.svelte index b3c4533fc..63b177993 100644 --- a/src/routes/docs/buttons/+page.svelte +++ b/src/routes/docs/buttons/+page.svelte @@ -8,7 +8,7 @@ import TomeSection from '@fuzdev/fuz_ui/TomeSection.svelte'; import TomeLink from '@fuzdev/fuz_ui/TomeLink.svelte'; - import { color_variants } from '$lib/variable_data.ts'; + import { palette_variants } from '$lib/variable_data.ts'; const LIBRARY_ITEM_NAME = 'buttons'; @@ -30,8 +30,8 @@

The <button> element is styled by default without adding classes. Classes - like .selected and .plain and .color_a modify the base - style. + like .selected and .plain and .palette_a modify the + base style.

Buttons have a .selected state that can be used for various UI purposes, like @@ -71,29 +71,31 @@ - {#each color_variants as c (c)} - {@const color_name = `color_${c}`} + {#each palette_variants as c (c)} + {@const palette_name = `palette_${c}`}

- `} /> - - + - - - - - - +
{/each} @@ -264,7 +266,7 @@
- +
diff --git a/src/routes/docs/chips/+page.svelte b/src/routes/docs/chips/+page.svelte index 31efcd31b..7f774b0a1 100644 --- a/src/routes/docs/chips/+page.svelte +++ b/src/routes/docs/chips/+page.svelte @@ -7,7 +7,7 @@ import TomeSection from '@fuzdev/fuz_ui/TomeSection.svelte'; import TomeLink from '@fuzdev/fuz_ui/TomeLink.svelte'; - import { color_variants } from '$lib/variable_data.ts'; + import { palette_variants } from '$lib/variable_data.ts'; const LIBRARY_ITEM_NAME = 'chips'; @@ -26,8 +26,8 @@ with <span> and <a>.

- Chips have color variants (.color_a through .color_j) that tint both - the text and background. Links (a.chip) have slightly bolder text. + Chips have color variants (.palette_a through .palette_j) that tint + both the text and background. Links (a.chip) have slightly bolder text.

a chip`} /> @@ -48,13 +48,13 @@ - {#each color_variants as c (c)} - {@const color_name = `color_${c}`} + {#each palette_variants as c (c)} + {@const palette_name = `palette_${c}`}
- `} /> - .chip.{color_name} + `} /> + .chip.{palette_name} - a.chip.{color_name} + a.chip.{palette_name}
{/each}
@@ -83,9 +83,9 @@ ...
`} />
one - two + two - three + three
diff --git a/src/routes/docs/classes/+page.svelte b/src/routes/docs/classes/+page.svelte index 8b3907a89..5223fc780 100644 --- a/src/routes/docs/classes/+page.svelte +++ b/src/routes/docs/classes/+page.svelte @@ -7,9 +7,11 @@ import TomeLink from '@fuzdev/fuz_ui/TomeLink.svelte'; import DeclarationLink from '@fuzdev/fuz_ui/DeclarationLink.svelte'; import ModuleLink from '@fuzdev/fuz_ui/ModuleLink.svelte'; + import MdnLink from '@fuzdev/fuz_ui/MdnLink.svelte'; import { space_variants, - color_variants, + palette_variants, + intent_variants, intensity_variants, shade_variants, text_variants, @@ -56,7 +58,7 @@

Compared to TailwindCSS and UnoCSS, fuz_css utility classes follow the grain of semantic HTML rather than being foundational to the design, and the DSL is currently more limited, with - interpreters providing a programmatic escape hatch -- see the + interpreters providing a programmatic escape hatch - see the comparison below.

@@ -106,7 +108,7 @@ The Vite plugin extracts classes and generates CSS on-demand. It works with Svelte and plain HTML/TS/JS out of the box. JSX frameworks (React, Preact, Solid) require the - acorn-jsx plugin -- see + acorn-jsx plugin - see React and JSX below.

@@ -150,7 +152,7 @@ import 'virtual:fuz.css';`}

The plugin extracts classes from files as Vite processes them, including from node_modules - dependencies. It supports HMR -- changes to classes in your code trigger automatic CSS + dependencies. It supports HMR: changes to classes in your code trigger automatic CSS updates.

Plugin options

@@ -203,6 +205,12 @@ import 'virtual:fuz.css';`} variables - customize or disable theme variables; set to null for utility-only mode, or provide a callback to modify defaults +
  • + theme - a to bake into the generated CSS, overlaid + onto + variables + last-wins by name +
  • additional_elements - elements to always include styles for (for runtime-created elements), or 'all' to include all base styles @@ -326,7 +334,7 @@ import './fuz.css';`}
    -`} +`} />

    Usage tracking works for variables inside clsx(), arrays, ternaries, and logical expressions within class attributes. Note that standalone clsx() calls - outside class attributes don't trigger tracking -- use the naming convention for those - cases. + outside class attributes don't trigger tracking - use the naming convention for those cases.

  • - .hue_{@render variant_range(color_variants)} + + .hue_{@render variant_range(palette_variants)} +
  • @@ -860,7 +906,7 @@ vite_plugin_fuz_css({
  • - .border_color_{@render variant_range(color_variants)}_{@render variant_range( + .border_{@render variant_range(palette_variants)}_{@render variant_range( intensity_variants )} @@ -916,7 +962,7 @@ vite_plugin_fuz_css({
  • - .outline_color_{@render variant_range(color_variants)}_{@render variant_range( + .outline_{@render variant_range(palette_variants)}_{@render variant_range( intensity_variants )} @@ -964,7 +1010,7 @@ vite_plugin_fuz_css({
  • - .shadow_color_{@render variant_range(color_variants)}_{@render variant_range( + .shadow_{@render variant_range(palette_variants)}_{@render variant_range( intensity_variants )} @@ -1224,9 +1270,9 @@ export const gen = gen_fuz_css({

    - Modifiers prefix any class type -- token, composite, or literal -- to apply styles - conditionally based on viewport, state, or color scheme. This is what makes utility classes - more powerful than inline styles. + Modifiers prefix any class type (token, composite, or literal) to apply styles conditionally + based on viewport, state, or color scheme. This is what makes utility classes more powerful + than inline styles.

    Responsive modifiers

    @@ -1376,7 +1422,7 @@ export const gen = gen_fuz_css({

    Combined modifiers follow a canonical order enforced with errors that guide you. Multiple states must be alphabetical (focus:hover: not hover:focus:) - because both generate equivalent CSS -- canonical ordering prevents duplicates. + because both generate equivalent CSS; canonical ordering prevents duplicates.

      @@ -1425,7 +1471,7 @@ export const gen = gen_fuz_css({ fuz_css's main stylesheet provides styles for base HTML elements using style variables, acting as a modern CSS reset that adapts to dark mode. It includes CSS classes that provide common generic - functionality -- these are called builtin classes. + functionality - these are called builtin classes.

      .unstyled

      Default list (styled):

      @@ -1483,7 +1529,7 @@ export const gen = gen_fuz_css({
    1. .title - see
    2. .row - see ,
    3. - .color_a through .color_j - see , + .palette_a through .palette_j - see ,
    4. @@ -1580,9 +1626,9 @@ export const gen = gen_fuz_css({
    5. expressions: logical (&&, - ||, ??), ternaries, template literals (complete tokens only -- + ||, ??), ternaries, template literals (complete tokens only - `color_a_50 ${base}` extracts color_a_50, but - `color_${hue}_50` cannot be extracted; use + `palette_${hue}_50` cannot be extracted; use @fuz-classes or additional_classes) @@ -1704,11 +1750,10 @@ const grid_cols_interpreter: CssClassDefinitionInterpreter = { };`} />

      - This generates grid-cols-1 through grid-cols-24 on-demand -- - something that would require 24 separate composite definitions. Note the classes for this - example could also be created as composites with a helper function -- fuz_css uses this - strategy internally to create its token classes in - . + This generates grid-cols-1 through grid-cols-24 on-demand, something + that would require 24 separate composite definitions. Note the classes for this example could + also be created as composites with a helper function - fuz_css uses this strategy internally + to create its token classes in .

      Register with the Vite plugin or Gro generator:

      feedback is welcome!

      diff --git a/src/routes/docs/colors/+page.svelte b/src/routes/docs/colors/+page.svelte index 5f9096aad..c1d5331eb 100644 --- a/src/routes/docs/colors/+page.svelte +++ b/src/routes/docs/colors/+page.svelte @@ -6,10 +6,11 @@ import ColorSchemeInput from '@fuzdev/fuz_ui/ColorSchemeInput.svelte'; import TomeSectionHeader from '@fuzdev/fuz_ui/TomeSectionHeader.svelte'; import TomeSection from '@fuzdev/fuz_ui/TomeSection.svelte'; + import Code from '@fuzdev/fuz_code/Code.svelte'; import HueSwatch from './HueSwatch.svelte'; import ColorSwatch from './ColorSwatch.svelte'; - import { color_variants } from '$lib/variable_data.ts'; + import { palette_variants, palette_glosses, type PaletteVariant } from '$lib/variable_data.ts'; const LIBRARY_ITEM_NAME = 'colors'; @@ -20,57 +21,111 @@ // TODO button to add an inline hue input for runtime modification of the theme - // TODO maybe add this to the variable data as comments - // Note: This array must stay in sync with color_variants (a-j = 10 elements) - const descriptions = [ - 'primary', - 'success', - 'error/danger', - 'accent/secondary', - 'highlight/tertiary', - 'muted/quaternary', - 'decorative/quinary', - 'caution/senary', - 'info/septenary', - 'flourish/octonary' - ]; + // 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; + };

      - fuz_css provides color that adapt to the - , working naturally in both light and dark modes. Each - theme - can customize the 10 hues (a-j) and their intensity variants (00-100). + fuz_css's colors are derived: around a dozen knobs produce every color + in pure CSS, in the + colorspace, adapting to the + + automatically. OKLCH lightness is perceptually uniform (equal lightness reads equally light in + every hue), so rotating a hue knob is safe: contrast and visual weight hold.

      - Hues use letters so themes can reassign colors without breaking semantics -- "a" is blue by - default but could be any color. Each hue has 13 intensity variants tuned independently for - visual balance across color schemes. + 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 intent knobs layered on top: + --hue_accent (links, focus, selection, selected states - what other systems call + "primary") defaults to + --hue_a, --hue_negative to --hue_c, and so on. Retarget + an intent to move just that meaning; rotate a letter to move the palette.

      - + +

      + The knobs are the theme API, from highest leverage down: +

      +
        +
      • + --hue_a--hue_j - OKLCH hue angles for the 10 palette slots +
      • +
      • + --hue_neutral + --neutral_chroma - the temperature and strength 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 + (above 1, deliberately clipping the weakest hues) +
      • +
      • + 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, + .bg_caution_10) +
      • +
      • + lightness ramps - --palette_lightness_00/_100/_curve + (and the same trio for shade_ and text_): the endpoint stops plus + a curve exponent bending the ramp between them, per color scheme +
      • +
      • + chroma curve - --palette_chroma_min/_max/_curve: a + mid-peaked curve, clamped per stop by gamut caps computed from the worst hue +
      • +
      • + per-slot chroma multipliers - --palette_a_chroma_scale … + --palette_j_chroma_scale and intent twins (--accent_chroma_scale, + …), each multiplying one slot's chroma under the global --chroma_scale. The + brown slot ships muted (--palette_f_chroma_scale: 0.55) because brown is + low-chroma orange, unreachable by hue alone. An intent bound to a muted slot needs its twin + set too - bindings share only the hue angle, and validate_theme warns when the + character would be dropped +
      • +

      - Hue variables contain a single number. Each color variable - combines a hue variable with saturation and lightness values for light and dark modes. + Every intermediate value these produce is also its own variable ( + --palette_lightness_30 + , --palette_chroma_50, …), so a theme can pin any individual stop as an + escape hatch.

      + +
      + +

      - Hue variables therefore provide a single source of truth that's easy to theme, but to achieve - pleasing results, setting the hue alone is not always sufficient. Custom colors generally need - tuning for saturation and lightness. + Hue variables contain a single OKLCH angle. Because lightness + and chroma are shared across all hues at each stop, the scales are interchangeable: setting a + hue alone is enough, no per-hue tuning required. The one deliberate exception is the per-slot + chroma multiplier - the brown slot ships muted because no hue angle renders brown at full + palette chroma.

      - Hue variables are also useful to construct custom colors not covered by the color variables. - For example, fuz_css's base stylesheet uses hue_a for the semi-transparent - ::selection. (try selecting some text -- - same hue!) + Hue variables are also useful to construct custom colors not covered by the palette. For + example, fuz_css's selection color derives from --hue_accent (try selecting some + text - + same hue!)

      Hue variables are the same in both light and dark modes (non-adaptive).

        - {#each color_variants as color_name, i (color_name)} - + {#each palette_variants as letter (letter)} + {/each}
      @@ -78,7 +133,7 @@
  • - +

    There are 13 intensity variants per hue (00, 05, 10, 20, ..., 80, 90, 95, 100), from subtle to bold. The 50 variant of each color is used as the base for things like @@ -87,58 +142,19 @@

    Unlike the shade and text scales (which are separate), - color variables can be used for both text and backgrounds via utility classes: + palette variables can be used for both text and backgrounds via utility classes: .color_a_50 sets text color, .bg_a_50 sets background color.

    -

    Each color exists in two forms:

    -
      -
    • - Adaptive (color_a_50) -- switches between light and dark - values based on color scheme. Use for most UI work. -
    • -
    • - Absolute (color_a_50_light, color_a_50_dark) -- - stable values that never change. Use when you need a pinned color. -
    • +

      + Palette stops are adaptive: they switch between light and dark ramps based on color scheme. + There are no absolute variants; for a color that doesn't adapt, write the literal color or + define one custom property. +

      +
        + {#each palette_variants as letter (letter)} + + {/each}
      - - -

      - The colors you'll use most often. They automatically adjust to maintain visual consistency - across color schemes. Note that these values differ between light and dark modes! See the - discussion above for why. -

      -
        - {#each color_variants as color_name (color_name)} - - {/each} -
      -
      -
      - -
      - - -

      - Sometimes you need a color that doesn't adapt, like logos, charts, color-coded - data, or elements that must match across screenshots. Every adaptive color has two absolute - variants: -

      -
        -
      • color_a_50_light - the value used in light mode
      • -
      • color_a_50_dark - the value used in dark mode
      • -
      -

      - These are stable regardless of color scheme. Light and dark variants are tuned independently - for visual balance -- achieving equivalent appearance across color schemes requires - different saturation and lightness values. -

      -
        - {#each color_variants as color_name (color_name)} - - {/each} -
      -
      diff --git a/src/routes/docs/colors/ColorSwatch.svelte b/src/routes/docs/colors/ColorSwatch.svelte index 6be6b89d4..427b8c67c 100644 --- a/src/routes/docs/colors/ColorSwatch.svelte +++ b/src/routes/docs/colors/ColorSwatch.svelte @@ -1,38 +1,19 @@ -{#if absolute} -
        - {#each intensity_variants as intensity (intensity)} - - {/each} -
      -
        - {#each intensity_variants as intensity (intensity)} - - {/each} -
      -{:else} -
        - {#each intensity_variants as intensity (intensity)} - - {/each} -
      -{/if} +
        + {#each intensity_variants as intensity (intensity)} + + {/each} +