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..08b78c7b5 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..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 new file mode 100644 index 000000000..08ce4d75c --- /dev/null +++ b/src/routes/ThemeEditor.svelte @@ -0,0 +1,301 @@ + + +{#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/docs/borders/+page.svelte b/src/routes/docs/borders/+page.svelte index b3773490f..fea96a2eb 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, @@ -46,10 +46,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 @@ -73,7 +69,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)} @@ -124,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.

@@ -145,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 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/buttons/+page.svelte b/src/routes/docs/buttons/+page.svelte index 9359087f2..f38b89b01 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 @@ -70,28 +70,30 @@ - {#each color_variants as c (c)} - {@const color_name = `color_${c}`} + {#each palette_variants as c (c)} + {@const palette_name = `palette_${c}`}

- `} /> - - `} /> + + - - .{palette_name}.plain - .{palette_name}.plain:disabled - .{palette_name}.selected - .{palette_name}.selected:disabled - .{palette_name}.selected.deselectable +
{/each} @@ -258,7 +260,7 @@
- +
diff --git a/src/routes/docs/chips/+page.svelte b/src/routes/docs/chips/+page.svelte index 4d6a48879..fb4a11ead 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 f442fbadf..179557c46 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, @@ -320,7 +322,7 @@ import './fuz.css';`}
-`} +`} />

Usage tracking works for variables inside clsx(), arrays, ternaries, and @@ -367,7 +369,7 @@ const turtle_class_name = 'turtle';`} content={`// @fuz-classes opacity:50% opacity:75% opacity:100% const opacity_classes = [50, 75, 100].map((n) => \`opacity:\${n}%\`); -/* @fuz-classes color_a_50 color_b_50 color_c_50 */ +/* @fuz-classes palette_a_50 palette_b_50 palette_c_50 */ const color = get_dynamic_color();`} />

@@ -398,7 +400,7 @@ const color = get_dynamic_color();`} Classes annotated with @fuz-classes and configured with additional_classes produce errors if they can't be resolved. This helps catch typos like - @fuz-classes color_a_55 instead of color_a_50. + @fuz-classes palette_a_55 instead of palette_a_50.

Alternatively, use the used utility classes (always generated on demand; there's no full version)

- Layers are emitted in that order, so variable definitions precede their use and utilities land - last to win over the low-specificity :where() reset. + Sections are emitted in that order inside cascade layers: + defaults in fuz.base, theme overrides in fuz.theme, and utilities in + fuz.utilities, so utilities beat the reset by layer order (not specificity) and + your own unlayered styles beat everything.

Base styles

@@ -598,8 +602,8 @@ vite_plugin_fuz_css({ use literal classes instead.

- -

some token classes

+ +

some token classes

Token classes use snake_case because style variables are designed for optional use in JS (imported from , but costing nothing @@ -763,7 +767,7 @@ vite_plugin_fuz_css({

  • .color_{@render variant_range(color_variants)}_{@render variant_range( + >.palette_{@render variant_range(palette_variants)}_{@render variant_range( intensity_variants, )} @@ -772,15 +776,37 @@ vite_plugin_fuz_css({
  • .bg_{@render variant_range(color_variants)}_{@render variant_range( + >.bg_{@render variant_range(palette_variants)}_{@render variant_range( intensity_variants, )}
  • +
  • + {#each intent_variants as intent (intent)}.{intent}_{@render variant_range(intensity_variants)}{/each} + +
  • +
  • + {#each intent_variants as intent (intent)}.bg_{intent}_{@render variant_range(intensity_variants)}{/each} + +
  • + @@ -800,7 +826,8 @@ vite_plugin_fuz_css({
  • - .hue_{@render variant_range(color_variants)} + .hue_{@render variant_range(palette_variants)}
  • .border_color_{@render variant_range(color_variants)}_{@render variant_range( + >.border_{@render variant_range(palette_variants)}_{@render variant_range( intensity_variants, )} @@ -908,7 +935,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, )} @@ -957,7 +984,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, )} @@ -1065,7 +1092,7 @@ export const custom_composites: Record = {

    • - token classes (p_lg, color_a_50) - resolved to their + token classes (p_lg, palette_a_50) - resolved to their declarations
    • @@ -1475,7 +1502,7 @@ export const gen = gen_fuz_css({
    • .title - see
    • .row - see ,
    • - .color_a through .color_j - see , + .palette_a through .palette_j - see ,
    • @@ -1573,8 +1600,8 @@ export const gen = gen_fuz_css({
    • expressions: logical (&&, ||, ??), ternaries, template literals (complete tokens only -- - `color_a_50 ${base}` extracts color_a_50, but - `color_${hue}_50` cannot be extracted; use + `palette_a_50 ${base}` extracts palette_a_50, but + `palette_${hue}_50` cannot be extracted; use @fuz-classes or additional_classes) diff --git a/src/routes/docs/colors/+page.svelte b/src/routes/docs/colors/+page.svelte index b342ce5ce..93be8bc30 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,104 @@ // 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, which is what makes themes a small set of knob values instead of hundreds of hand-tuned stops.

      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. + 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 + 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, knowingly clipping the weakest hues) +
      • +
      • + --hue_shift -- degrees of hue rotation across each ramp for painterly + warm-light/cool-shadow character (default 0) +
      • +
      • + 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 +
      • +

      - 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 a surgical + escape hatch -- but the knobs come first.

      + +
      + +

      - 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.

      - 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 +126,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 +135,20 @@

    Unlike the shade and text scales (which are separate), - color variables can be used for both text and backgrounds via utility classes: - .color_a_50 sets text color, .bg_a_50 sets background color. + palette variables can be used for both text and backgrounds via utility classes: + .palette_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. + For a color that doesn't adapt, write the literal color or define one custom property -- the + old generated absolute variants (color_a_50_light-style) were removed with the + OKLCH migration. +

      +
        + {#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 478cd1481..4e4242f1f 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} +