diff --git a/.changeset/base-styles.md b/.changeset/base-styles.md new file mode 100644 index 000000000..3e4fac9d2 --- /dev/null +++ b/.changeset/base-styles.md @@ -0,0 +1,34 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: rework base styles - interaction states, micro-surface variables, body font, button border style, section rhythm + +Breaking: + +- `body` reads `--font_family` (default `var(--font_family_sans)`) instead + of `--font_family_sans`; set `--font_family` for a serif body. +- Buttons read `--button_border_style` (default `var(--border_style)`) and + `--button_border_style_active` while pressed. A contextual + `--border_style` on an ancestor no longer reaches buttons - set the + button knobs there too. +- Selected buttons use `--text_00` for inverse text (was + `--text_05`/`--text_10`); `.palette_X` buttons fill with `palette_X_50` + (was stop 40), the selected border matching the fill. +- Hovering an input, textarea, or select colors the border with + `--outline_color` instead of `--border_color_20`; disabled inputs no + longer react to hover. +- `section` bottom margin is a `--flow_margin` multiple (same default), + scaled by size composites; `.unstyled` opts out. + +New: + +- Focus outlines use `outline-offset: var(--outline_offset)` (default + `1px`). +- Themable micro-surfaces: `--scrollbar_thumb_color` (`var(--shade_40)`), + `--scrollbar_track_color` (transparent), `--caret_color` + (`var(--accent_50)`), `--backdrop_color` (`var(--darken_60)`). +- `@media (prefers-contrast: more)` maps onto the curve knobs in the + `fuz.preferences` layer; theme overrides beat it. +- `check_theme` gains `GATE_SELECTED_TEXT`; the knob catalog's border + styles gain `inset`/`outset`. diff --git a/.changeset/generators.md b/.changeset/generators.md new file mode 100644 index 000000000..4ba66fa0d --- /dev/null +++ b/.changeset/generators.md @@ -0,0 +1,14 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: dev-server prescan, and `base_css` without `variables` is an error + +- The Vite plugin pre-scans sources at dev-server startup so the first page + load has complete utility CSS. New `prescan` option: `true` (default, + `src` under the Vite root), `false`, or an array of directories. +- `base_css` enabled with `variables: null` is now the error diagnostic + `theme_variables_disabled`. Set `base_css: null` too for utility-only + mode, or keep `variables` and set `additional_variables: 'all'` to bundle + the full theme. +- `CssResolutionResult` loses `referenced_variables`. diff --git a/.changeset/oklch-color-system.md b/.changeset/oklch-color-system.md new file mode 100644 index 000000000..b1d1e60b5 --- /dev/null +++ b/.changeset/oklch-color-system.md @@ -0,0 +1,52 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: derived OKLCH color system with semantic intents and cascade layers + +Breaking: + +- `--color_X_NN` → `--palette_X_NN` (10 letters × 13 stops); + `ColorVariant`/`color_variants` → `PaletteVariant`/`palette_variants`. +- Class renames: `border_color_X_NN` → `border_X_NN`, + `outline_color_X_NN` → `outline_X_NN`, `shadow_color_X_NN` → + `shadow_X_NN`, `.color_a`-`.color_j` → `.palette_a`-`.palette_j`. + `.color_X_NN`, `bg_X_NN`, `border_color_NN`, and `shadow_color_*` keep + their names. +- Classes removed: `.fg_NN`/`.bg_NN` (use + `background-color:var(--fg_10)`; `bg_` is now the opaque prefix), + `.hue_a`-`.hue_j` and `--hue`, and every `_light`/`_dark` variable and + class. +- `--hue_a`…`--hue_j` are OKLCH angles (blue `250`, was `210`); replace + `hsl(var(--hue_x) …)` with `oklch( var(--hue_x))` or a stop. +- `--tint_hue`/`--tint_saturation` → `--hue_neutral` + `--neutral_chroma`. +- `color-mix()` interpolates `in oklab` (was `in hsl`). +- Shipped CSS is layered `fuz.base` < `fuz.preferences` < `fuz.theme` < + `fuz.utilities`; unlayered consumer styles beat all of it. Custom + `base_css` is re-layered into `fuz.base`. +- `variables.ts` exports only `default_variables`; read a variable with + `default_variables.find((v) => v.name === 'space_md')`. `icon_sizes` → + `ICON_SIZES` (`ICON_SIZES.xs === 18`, was `'18px'`); `Z_INDEX_MAX` + removed (inline `2147483647`). + +New: + +- Curve knobs: `--chroma_scale`, `--palette_lightness_00/_100/_curve` (and + `shade_`/`text_`), `--palette_chroma_min/_max`, `--chroma_curve`, with + pinnable derived stops `--palette_lightness_NN`, `--palette_chroma_NN`, + `--chroma_shape_NN`. +- Intent knobs `--hue_accent`/`_positive`/`_negative`/`_caution`/`_info`, + each with a 13-stop scale (`--accent_00`…`--accent_100`), token classes + (`.positive_50`, `.bg_caution_10`), `--selection_color`, and + `intent_variants`/`IntentVariant`. Links, focus, selection, + `accent-color`, and disabled-active feedback use them; focus follows + `--outline_color` with the accent as fallback. +- Per-slot chroma multipliers `--palette_X_chroma_scale` and + `--_chroma_scale` (default `1`; brown `f` ships at `0.55`). +- `--border_color_lightness`/`--border_color_chroma` derive the + `border_color_*` alpha ramp through the neutral intent. +- Value tables in `variable_data.ts`: `FONT_SIZES`, `SPACE_SIZES`, + `BORDER_RADII`, `DISTANCES`, `LINE_HEIGHTS`, `DURATIONS` + (+ `duration_variants`), `SHADOW_GEOMETRY`, `SHADOW_ALPHAS`, + `OVERLAY_ALPHAS`. +- Design-time modules `ramps.ts`, `oklch.ts`, `wcag.ts`. diff --git a/.changeset/theme-authoring.md b/.changeset/theme-authoring.md new file mode 100644 index 000000000..1c8bc79b3 --- /dev/null +++ b/.changeset/theme-authoring.md @@ -0,0 +1,43 @@ +--- +'@fuzdev/fuz_css': minor +--- + +feat: themes as knob-sets - schema, registry and modifiers, scheme stance, knob catalog, checks, pure renderer, build-time `theme` option + +Breaking: + +- `Theme` moves to `variable.ts` as a strict zod schema: + `import type {Theme} from '@fuzdev/fuz_css/variable.ts'` (was + `theme.ts`). Unknown properties are errors; `parse_theme(value)` returns + a theme-or-`null`. +- `RenderThemeStyleOptions.empty_default_theme` removed - pass the defaults: + `render_theme_style({name: 'base', variables: default_variables})`. +- `render_theme_style` loses `specificity` and gains + `layer?: string | null` (default `'fuz.theme'`); `generate_theme_css` + loses its specificity parameter; the `theme_specificity` generator option + is removed. +- `default_themes` is just base. Low/high contrast are `contrast_modifiers`, + composed over a theme with `compose_themes(base, ...overlays)`. + +New: + +- `Theme.scheme?: 'dual' | 'light' | 'dark'` (`ThemeScheme`). Author a + single-scheme theme single-slot and pass it through + `resolve_theme_stance` (`theme_stance.ts`), which fills `scheme_mirror`; + the renderer pins `color-scheme`. +- Exemplar themes under `themes/`: smolder, parchment, concrete, nineties, + phosphor (dark-only), neon (dark-only). +- Scale knobs `--shadow_alpha_scale`, `--radius_scale`, `--scale_factor`, + `--font_weight`, `--heading_font_weight` (hook; setting it flattens the + ladder), `--heading_font_family`, `--background_image`. +- `knobs.ts`: the typed knob catalog (`theme_knobs`, `theme_knob_by_name`, + `theme_knob_axes`); `palette_glosses` in `variable_data.ts`. +- `theme_check.ts`: `validate_theme(unknown)`, `check_theme` (gamut, + monotonicity, contrast gates; `GATE_*` thresholds), `compile_theme` + (per-theme chroma caps), `create_theme_resolver`, + `known_theme_variable_names`. +- Generators take `theme`, baked into the output and tree-shaken: + `vite_plugin_fuz_css({theme: phosphor_theme})`. Composes with fuz_ui's + `ThemeRoot`, the runtime theme winning. `apply_theme_variables` is + exported from `variable_graph.ts`. +- `theme.ts` no longer imports `variables.ts` (~1.3KB minified, was ~38KB). diff --git a/CLAUDE.md b/CLAUDE.md index e834cd228..be912b9eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ fuz_css (`@fuzdev/fuz_css`) styles HTML elements by default and integrates custom properties, themes, and utility classes into a complete system. It -ships two plain CSS files — the base `style.css` and replaceable `theme.css` — +ships two plain CSS files - the base `style.css` and replaceable `theme.css` - that work with any framework and plain HTML, and its class generator supports HTML/JS/TS, Svelte, and JSX (React/Preact/Solid). Early alpha with breaking changes ahead. @@ -15,7 +15,7 @@ components (themes, color scheme controls), see [`fuz_ui`](../fuz_ui/CLAUDE.md). ## Committing `git add` and `git commit` are denied by `.claude/settings.local.json` in -this repo — make the edits and stop, the user commits. +this repo - make the edits and stop, the user commits. ## Gro commands @@ -61,20 +61,21 @@ fuz_css is a **semantic-first CSS framework and design system**: ### Styling philosophy -**Default element styling is the baseline — reach past it only with a +**Default element styling is the baseline - reach past it only with a reason.** fuz_css styles semantic HTML out of the box, so most content needs no classes: headings are tiered, form controls share sizing and focus states, and block elements (`p`, `ul`, `ol`, `table`, `aside`, `blockquote`, `pre`, `fieldset`, …) get vertical rhythm automatically from the **flow-margin** -system — each gets `margin-bottom: var(--flow_margin, var(--space_lg))` unless -`:last-child` or `.unstyled`, and margins reset to 0 on the direct children of -a `.row` (horizontal flex; use `gap_*` there instead). Adding a +system - each gets `margin-bottom: var(--flow_margin, var(--space_lg))` unless +`:last-child` or `.unstyled` (headings and `section` take multiples of it), +and margins reset to 0 on the direct children of a `.row` (horizontal flex; +use `gap_*` there instead). Adding a `mb_*`/`gap_*`/`p_*` class or a ` diff --git a/src/routes/KnobControl.svelte b/src/routes/KnobControl.svelte new file mode 100644 index 000000000..fcca57588 --- /dev/null +++ b/src/routes/KnobControl.svelte @@ -0,0 +1,271 @@ + + + +
+ {#if knob.bindable} + +
--{knob.name}
+
+ {#each palette_variants as letter (letter)} + + {/each} + +
+ {#if bound_letter === null && numeric_value !== null} + numeric_value ?? 0, (v) => emit_numeric(String(v))} /> + {:else if bound_letter === null} + + onchange(e.currentTarget.value)} + /> + {/if} + {:else if knob.kind === 'hue' && (numeric_value ?? derived_numeric) !== null} + + numeric_value ?? derived_numeric ?? 0, (v) => emit_numeric(String(v))} + > + --{knob.name} + + {:else} + + {/if} + {#if changed} + + + {/if} +
+ + 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..869f2c1f1 --- /dev/null +++ b/src/routes/ThemeEditor.svelte @@ -0,0 +1,398 @@ + + +{#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 theme_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 (issue.level + (issue.variable ?? '') + issue.message)} +
  • + {issue.level}: {issue.message} +
  • + {/each} +
+ {/if} + {#if failing_gates.length} +
    + {#each failing_gates as entry (entry.gate + entry.scheme + entry.subject)} +
  • + {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..003e1fcba 100644 --- a/src/routes/docs/borders/+page.svelte +++ b/src/routes/docs/borders/+page.svelte @@ -5,12 +5,13 @@ import TomeSectionHeader from '@fuzdev/fuz_ui/TomeSectionHeader.svelte'; import TomeSection from '@fuzdev/fuz_ui/TomeSection.svelte'; import TomeLink from '@fuzdev/fuz_ui/TomeLink.svelte'; + import MdnLink from '@fuzdev/fuz_ui/MdnLink.svelte'; import ColorSchemeInput from '@fuzdev/fuz_ui/ColorSchemeInput.svelte'; import StyleVariableButton from '@fuzdev/fuz_ui/StyleVariableButton.svelte'; import { border_width_variants, - color_variants, + palette_variants, outline_width_variants, border_radius_variants, alpha_variants, @@ -18,6 +19,7 @@ type IntensityVariant } from '$lib/variable_data.ts'; import UnfinishedImplementationWarning from '$routes/docs/UnfinishedImplementationWarning.svelte'; + import ResolvedColorCode from './ResolvedColorCode.svelte'; const LIBRARY_ITEM_NAME = 'borders'; @@ -46,17 +48,12 @@ 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,15 +62,43 @@

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.

+ + +

+ --border_style is global - asides, blockquotes, and form fields all read it. + Buttons take --button_border_style instead (defaulting to + var(--border_style)), and swap to + --button_border_style_active + while pressed. Buttons are the only element with a raised/pressed affordance, so the split + lands there rather than as a general state variable. +

+

+ That pair is what makes beveled chrome expressible, since + 's inset and outset derive + their light and dark edges from the border color - raised buttons that press in, over sunken + fields: +

+ +

+ The nineties exemplar theme is built on exactly this. +

+

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)} @@ -91,9 +116,7 @@ {/each}
- = - {computed_styles?.getPropertyValue('--' + name)} - + =
{/each} @@ -123,8 +146,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 +167,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 = 'color_' + letter + '_50'}
{/each}
-
- = - {computed_styles?.getPropertyValue('--' + name)} - +
+ =
{/each} @@ -193,7 +214,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..6e4e8faa8 --- /dev/null +++ b/src/routes/docs/borders/ResolvedColorCode.svelte @@ -0,0 +1,21 @@ + + +{color.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..ab81b6edd 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)} -
  • .darken_{@render variant_range(darken_lighten_variants)} @@ -860,7 +901,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 +957,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 +1005,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 +1265,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 +1417,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 +1466,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 +1524,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,7 +1621,7 @@ 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 @fuz-classes @@ -1704,11 +1745,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..2eb14259c 100644 --- a/src/routes/docs/colors/+page.svelte +++ b/src/routes/docs/colors/+page.svelte @@ -6,71 +6,117 @@ 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, format_palette_gloss } from '$lib/variable_data.ts'; const LIBRARY_ITEM_NAME = 'colors'; const tome = tome_get_by_slug(LIBRARY_ITEM_NAME); - const computed_styles = - typeof window === 'undefined' ? null : window.getComputedStyle(document.documentElement); - // 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' - ];

      - 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: a handful of high-leverage 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 +124,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 +133,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} +