@motion-proto/live-tokens 0.68.1 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +16 -5
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +43 -8
  3. package/.claude/skills/live-tokens-create-component/references/token-naming.md +30 -1
  4. package/.claude/skills/live-tokens-fix-findings/SKILL.md +131 -0
  5. package/.claude/skills/live-tokens-pick-component/SKILL.md +2 -1
  6. package/CHANGELOG.md +207 -0
  7. package/README.md +11 -4
  8. package/bin/check-component.mjs +367 -63
  9. package/bin/check-page.mjs +409 -0
  10. package/bin/cli.mjs +57 -8
  11. package/bin/lib/cssValues.mjs +50 -0
  12. package/bin/lib/findings.mjs +106 -0
  13. package/bin/lib/tokenVocabulary.mjs +213 -0
  14. package/dist-plugin/adjust/index.cjs +174 -23
  15. package/dist-plugin/adjust/index.js +68 -23
  16. package/dist-plugin/{chunk-2UX6EVVA.js → chunk-2YNERPXY.js} +1 -1
  17. package/dist-plugin/{chunk-NE6N66EE.js → chunk-GPIBU44G.js} +107 -1
  18. package/dist-plugin/{chunk-ZHPX7ZYQ.js → chunk-RFVYPNRO.js} +39 -1
  19. package/dist-plugin/generateColorsAndType/index.cjs +107 -1
  20. package/dist-plugin/generateColorsAndType/index.js +1 -1
  21. package/dist-plugin/index.cjs +146 -2
  22. package/dist-plugin/index.js +3 -3
  23. package/dist-plugin/migrateData/index.cjs +107 -1
  24. package/dist-plugin/migrateData/index.js +2 -2
  25. package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
  26. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  27. package/package.json +3 -2
  28. package/src/app/site.css +4 -4
  29. package/src/editor/component-editor/ButtonEditor.svelte +71 -4
  30. package/src/editor/component-editor/CardEditor.svelte +6 -6
  31. package/src/editor/component-editor/DialogEditor.svelte +3 -3
  32. package/src/editor/component-editor/IconButtonEditor.svelte +68 -3
  33. package/src/editor/component-editor/ImageEditor.svelte +8 -8
  34. package/src/editor/component-editor/ImageLightboxEditor.svelte +12 -1
  35. package/src/editor/component-editor/MenuSelectEditor.svelte +63 -3
  36. package/src/editor/component-editor/SegmentedControlEditor.svelte +63 -3
  37. package/src/editor/component-editor/SideNavigationEditor.svelte +67 -3
  38. package/src/editor/component-editor/SliderEditor.svelte +186 -0
  39. package/src/editor/component-editor/TabBarEditor.svelte +63 -3
  40. package/src/editor/component-editor/registry.ts +10 -0
  41. package/src/editor/core/components/aliasKinds.ts +51 -28
  42. package/src/editor/core/sketch/sketchLayer.ts +16 -0
  43. package/src/editor/core/store/editorPersistence.ts +44 -1
  44. package/src/editor/core/store/editorRenderer.ts +2 -2
  45. package/src/editor/core/store/editorStore.ts +18 -18
  46. package/src/editor/core/store/editorTypes.ts +7 -6
  47. package/src/editor/core/themes/migrations/2026-09-01-gate-suffix-enabled.ts +31 -0
  48. package/src/editor/core/themes/migrations/2026-09-01-scrim-rename.ts +52 -0
  49. package/src/editor/core/themes/migrations/2026-09-01-tabbar-active-tint.ts +27 -0
  50. package/src/editor/core/themes/migrations/2026-09-01-tint-rename.ts +42 -0
  51. package/src/editor/core/themes/migrations/index.ts +16 -0
  52. package/src/editor/core/themes/slices/domainVars.ts +2 -2
  53. package/src/editor/core/themes/slices/washes.ts +107 -0
  54. package/src/editor/docs/content/editing-tokens.md +5 -3
  55. package/src/editor/docs/content.generated.ts +1 -1
  56. package/src/editor/pages/EditorShell.svelte +1 -1
  57. package/src/editor/ui/SurfacesTab.svelte +3 -3
  58. package/src/editor/ui/UITokenSelector.svelte +1 -0
  59. package/src/editor/ui/VariablesTab.svelte +2 -2
  60. package/src/editor/ui/sections/{OverlaysSection.svelte → WashesSection.svelte} +44 -43
  61. package/src/live-tokens/data/colors-and-type/autumn.json +6 -6
  62. package/src/live-tokens/data/colors-and-type/default.json +6 -6
  63. package/src/live-tokens/data/colors-and-type/halloween.json +6 -6
  64. package/src/live-tokens/data/colors-and-type/midnight-study.json +6 -6
  65. package/src/live-tokens/data/colors-and-type/ocean.json +6 -6
  66. package/src/live-tokens/data/colors-and-type/royal-velvet.json +6 -6
  67. package/src/live-tokens/data/colors-and-type/sketchy.json +6 -6
  68. package/src/live-tokens/data/colors-and-type/spring-meadow.json +6 -6
  69. package/src/live-tokens/data/colors-and-type/sunset.json +6 -6
  70. package/src/live-tokens/data/themes/autumn.json +81 -13
  71. package/src/live-tokens/data/themes/halloween.json +81 -13
  72. package/src/live-tokens/data/themes/midnight-study.json +81 -13
  73. package/src/live-tokens/data/themes/ocean.json +81 -13
  74. package/src/live-tokens/data/themes/royal-velvet.json +81 -13
  75. package/src/live-tokens/data/themes/sketchy.json +81 -13
  76. package/src/live-tokens/data/themes/spring-meadow.json +81 -13
  77. package/src/live-tokens/data/themes/sunset.json +81 -13
  78. package/src/live-tokens/data/tokens.generated.css +6 -6
  79. package/src/system/components/Button.svelte +28 -10
  80. package/src/system/components/Card.svelte +6 -6
  81. package/src/system/components/Dialog.svelte +3 -3
  82. package/src/system/components/IconButton.svelte +23 -7
  83. package/src/system/components/Image.svelte +6 -6
  84. package/src/system/components/MenuSelect.svelte +17 -1
  85. package/src/system/components/SegmentedControl.svelte +20 -2
  86. package/src/system/components/SideNavigation.svelte +22 -2
  87. package/src/system/components/Slider.svelte +348 -0
  88. package/src/system/components/TabBar.svelte +20 -2
  89. package/src/system/styles/CONVENTIONS.md +2 -2
  90. package/src/system/styles/tokens.css +12 -4
  91. package/template/package.json +3 -2
  92. package/template/src/pages/Home.svelte +1 -1
  93. package/src/editor/core/themes/slices/overlays.ts +0 -101
@@ -0,0 +1,52 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Scrim rename (2026-09-01): `--overlay-*` became `--scrim-*`.
5
+ *
6
+ * An overlay is a scrim, a translucent layer that dims what is behind it. The
7
+ * name had spread to cover surface tints too, which are the opposite operation,
8
+ * and it collided with `backdrop`, an exported concept about what paints behind
9
+ * an element. Each name now means one thing.
10
+ *
11
+ * Values are unchanged, so this is a pure key rename in colors-and-type files.
12
+ * Component configs need both halves: Dialog's `--dialog-overlay-surface` is a
13
+ * key, and any alias *value* pointing at a scrim stop is a bare token name that
14
+ * has to be rebound.
15
+ */
16
+ const RENAMED: Record<string, string> = {
17
+ '--overlay-low': '--scrim-low',
18
+ '--overlay': '--scrim',
19
+ '--overlay-high': '--scrim-high',
20
+ };
21
+
22
+ const RENAMED_COMPONENT_KEYS: Record<string, string> = {
23
+ '--dialog-overlay-surface': '--dialog-scrim-surface',
24
+ };
25
+
26
+ export const colorsAndTypeMigration_2026_09_01_scrimRename: Migration = {
27
+ id: '2026-09-01-scrim-rename-theme',
28
+ fromVersion: 5,
29
+ toVersion: 6,
30
+ appliesTo: 'colors-and-type',
31
+ apply(rawVars) {
32
+ const out: Record<string, string> = {};
33
+ for (const [key, value] of Object.entries(rawVars)) {
34
+ out[RENAMED[key] ?? key] = value;
35
+ }
36
+ return out;
37
+ },
38
+ };
39
+
40
+ export const componentMigration_2026_09_01_scrimRename: Migration = {
41
+ id: '2026-09-01-scrim-rename-component',
42
+ fromVersion: 21,
43
+ toVersion: 22,
44
+ appliesTo: 'component-config',
45
+ apply(rawVars) {
46
+ const out: Record<string, string> = {};
47
+ for (const [key, value] of Object.entries(rawVars)) {
48
+ out[RENAMED_COMPONENT_KEYS[key] ?? key] = RENAMED[value] ?? value;
49
+ }
50
+ return out;
51
+ },
52
+ };
@@ -0,0 +1,27 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * TabBar active surface, scrim to tint (2026-09-01, tabbar only).
5
+ *
6
+ * The active tab is a wash *on* the bar, not a layer dimming what is behind it,
7
+ * so it belongs to the tint family. It only ever read a scrim because no tint
8
+ * family existed. Rebinding is a visible change: the 38% near-black scrim
9
+ * becomes a 10% white tint, so the active tab reads lighter rather than darker.
10
+ *
11
+ * Scoped to the shipped default value. A theme that pointed this alias
12
+ * somewhere else keeps its choice.
13
+ */
14
+ export const componentMigration_2026_09_01_tabbarActiveTint: Migration = {
15
+ id: '2026-09-01-tabbar-active-tint',
16
+ fromVersion: 23,
17
+ toVersion: 24,
18
+ appliesTo: 'component-config',
19
+ apply(rawVars, meta) {
20
+ if (meta.component !== 'tabbar') return { ...rawVars };
21
+ const out = { ...rawVars };
22
+ if (out['--tabbar-active-surface'] === '--scrim-low') {
23
+ out['--tabbar-active-surface'] = '--tint-low';
24
+ }
25
+ return out;
26
+ },
27
+ };
@@ -0,0 +1,42 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Tint rename (2026-09-01): `--hover-*` became `--tint-*`.
5
+ *
6
+ * A state is a segment of a property name (`--button-outline-hover-surface`),
7
+ * not a token of its own, so the three stops are named for what they are: a
8
+ * tint shades the surface it sits on. Values are unchanged.
9
+ *
10
+ * Nothing shipped read the old names, so this only touches saved files that
11
+ * captured them from the editor's own stops.
12
+ */
13
+ const RENAMED: Record<string, string> = {
14
+ '--hover-low': '--tint-low',
15
+ '--hover': '--tint',
16
+ '--hover-high': '--tint-high',
17
+ };
18
+
19
+ export const colorsAndTypeMigration_2026_09_01_tintRename: Migration = {
20
+ id: '2026-09-01-tint-rename-theme',
21
+ fromVersion: 6,
22
+ toVersion: 7,
23
+ appliesTo: 'colors-and-type',
24
+ apply(rawVars) {
25
+ const out: Record<string, string> = {};
26
+ for (const [key, value] of Object.entries(rawVars)) out[RENAMED[key] ?? key] = value;
27
+ return out;
28
+ },
29
+ };
30
+
31
+ export const componentMigration_2026_09_01_tintRename: Migration = {
32
+ id: '2026-09-01-tint-rename-component',
33
+ fromVersion: 22,
34
+ toVersion: 23,
35
+ appliesTo: 'component-config',
36
+ apply(rawVars) {
37
+ const out: Record<string, string> = {};
38
+ // Alias values are bare token names; rebind any that pointed at a hover stop.
39
+ for (const [key, value] of Object.entries(rawVars)) out[key] = RENAMED[value] ?? value;
40
+ return out;
41
+ },
42
+ };
@@ -62,6 +62,16 @@ import {
62
62
  colorsAndTypeMigration_2026_08_15_lineHeightScaleRename,
63
63
  componentMigration_2026_08_15_lineHeightScaleRename,
64
64
  } from './2026-08-15-line-height-scale-rename';
65
+ import {
66
+ colorsAndTypeMigration_2026_09_01_scrimRename,
67
+ componentMigration_2026_09_01_scrimRename,
68
+ } from './2026-09-01-scrim-rename';
69
+ import {
70
+ colorsAndTypeMigration_2026_09_01_tintRename,
71
+ componentMigration_2026_09_01_tintRename,
72
+ } from './2026-09-01-tint-rename';
73
+ import { componentMigration_2026_09_01_tabbarActiveTint } from './2026-09-01-tabbar-active-tint';
74
+ import { componentMigration_2026_09_01_gateSuffixEnabled } from './2026-09-01-gate-suffix-enabled';
65
75
 
66
76
  /**
67
77
  * Registered migrations. Order in this array does not matter — the runner
@@ -94,6 +104,12 @@ export const MIGRATIONS: Migration[] = [
94
104
  colorsAndTypeMigration_2026_08_13_dropLegacyShapeSpaceKeys,
95
105
  colorsAndTypeMigration_2026_08_15_lineHeightScaleRename,
96
106
  componentMigration_2026_08_15_lineHeightScaleRename,
107
+ colorsAndTypeMigration_2026_09_01_scrimRename,
108
+ componentMigration_2026_09_01_scrimRename,
109
+ colorsAndTypeMigration_2026_09_01_tintRename,
110
+ componentMigration_2026_09_01_tintRename,
111
+ componentMigration_2026_09_01_tabbarActiveTint,
112
+ componentMigration_2026_09_01_gateSuffixEnabled,
97
113
  ];
98
114
 
99
115
  function countFor(kind: 'colors-and-type' | 'component-config'): number {
@@ -5,11 +5,11 @@
5
5
  * so the store stays the single source of truth.
6
6
  */
7
7
  import { COLUMN_VAR_NAMES } from './columns';
8
- import { OVERLAY_VAR_NAMES } from './overlays';
8
+ import { WASH_VAR_NAMES } from './washes';
9
9
  import { SHADOW_VAR_NAMES } from './shadows';
10
10
 
11
11
  export const DOMAIN_VAR_NAMES: readonly string[] = [
12
12
  ...COLUMN_VAR_NAMES,
13
- ...OVERLAY_VAR_NAMES,
13
+ ...WASH_VAR_NAMES,
14
14
  ...SHADOW_VAR_NAMES,
15
15
  ];
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Washes slice — a wash is a translucent layer of color, stored as
3
+ * `{alias, opacity}` and emitted as
4
+ * `color-mix(in srgb, var(<alias>) <pct>%, transparent)`, so it follows its
5
+ * aliased source token (a brand-color shift propagates without re-editing
6
+ * every stop).
7
+ *
8
+ * Two families, opposite jobs. A **scrim** dims what sits behind it, which is
9
+ * what a modal needs. A **tint** shades the surface it sits on, which is what
10
+ * a hover needs. They are not interchangeable: scrims alias a near-black
11
+ * surface, tints alias the text color, so a tint lightens a dark theme and
12
+ * darkens a light one without picking a direction.
13
+ *
14
+ * Defaults diverge from tokens.css by design: the editor starts from a
15
+ * neutral alias and tokens.css continues to win until first edit.
16
+ */
17
+ import type { EditorState, WashToken } from '../../store/editorTypes';
18
+
19
+ export function makeDefaultScrimTokens(): WashToken[] {
20
+ return [
21
+ { variable: '--scrim-low', label: 'Low', alias: '--surface-neutral-lowest', opacity: 0.38 },
22
+ { variable: '--scrim', label: 'Base', alias: '--surface-neutral-lowest', opacity: 0.51 },
23
+ { variable: '--scrim-high', label: 'High', alias: '--surface-neutral-lowest', opacity: 0.64 },
24
+ ];
25
+ }
26
+
27
+ export function makeDefaultTintTokens(): WashToken[] {
28
+ return [
29
+ { variable: '--tint-low', label: 'Low', alias: '--text-primary', opacity: 0.05 },
30
+ { variable: '--tint', label: 'Base', alias: '--text-primary', opacity: 0.1 },
31
+ { variable: '--tint-high', label: 'High', alias: '--text-primary', opacity: 0.15 },
32
+ ];
33
+ }
34
+
35
+ export function makeDefaultWashesState(): EditorState['washes'] {
36
+ return {
37
+ scrims: makeDefaultScrimTokens(),
38
+ tints: makeDefaultTintTokens(),
39
+ };
40
+ }
41
+
42
+ export const WASH_VAR_NAMES = [
43
+ '--scrim-low', '--scrim', '--scrim-high',
44
+ '--tint-low', '--tint', '--tint-high',
45
+ ] as const;
46
+
47
+ export function washTokenToCss(t: WashToken): string {
48
+ const pct = Math.round(t.opacity * 100);
49
+ if (pct >= 100) return `var(${t.alias})`;
50
+ return `color-mix(in srgb, var(${t.alias}) ${pct}%, transparent)`;
51
+ }
52
+
53
+ const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+)%,\s*transparent\)$/i;
54
+ const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
55
+
56
+ export function parseWashCss(raw: string): { alias: string; opacity: number } | null {
57
+ const s = raw.trim();
58
+ const mix = s.match(COLOR_MIX_RE);
59
+ if (mix) {
60
+ const pct = parseInt(mix[2], 10);
61
+ if (!Number.isFinite(pct)) return null;
62
+ return { alias: mix[1], opacity: Math.max(0, Math.min(100, pct)) / 100 };
63
+ }
64
+ const plain = s.match(PLAIN_VAR_RE);
65
+ if (plain) return { alias: plain[1], opacity: 1 };
66
+ return null;
67
+ }
68
+
69
+ export function washesToVars(w: EditorState['washes']): Record<string, string> {
70
+ const out: Record<string, string> = {};
71
+ for (const t of w.scrims) out[t.variable] = washTokenToCss(t);
72
+ for (const t of w.tints) out[t.variable] = washTokenToCss(t);
73
+ return out;
74
+ }
75
+
76
+ export function applyWashVarsToState(washes: EditorState['washes'], vars: Record<string, string>): void {
77
+ const applyTo = (list: WashToken[]) => {
78
+ for (const t of list) {
79
+ const raw = vars[t.variable];
80
+ if (!raw) continue;
81
+ const parsed = parseWashCss(raw);
82
+ if (!parsed) continue;
83
+ t.alias = parsed.alias;
84
+ t.opacity = parsed.opacity;
85
+ }
86
+ };
87
+ applyTo(washes.scrims);
88
+ applyTo(washes.tints);
89
+ }
90
+
91
+ /**
92
+ * Loader: route wash entries from a freshly-loaded theme's vars bag into
93
+ * `next.washes` and remove them from the bag — but only when the value parses
94
+ * as the new format (color-mix / plain var). Legacy rgba values pass through to
95
+ * the cssVars bag so the DOM still paints them; the user's next edit in the
96
+ * picker promotes them to the typed slice.
97
+ */
98
+ export function loadWashesFromVars(
99
+ next: EditorState,
100
+ rawVars: Record<string, string>,
101
+ ): void {
102
+ applyWashVarsToState(next.washes, rawVars);
103
+ for (const name of WASH_VAR_NAMES) {
104
+ const raw = rawVars[name];
105
+ if (raw && parseWashCss(raw) !== null) delete rawVars[name];
106
+ }
107
+ }
@@ -57,10 +57,12 @@ Numeric scales with a slider per step.
57
57
 
58
58
  Change a step and every element using it repaints.
59
59
 
60
- ## Overlays and gradients
60
+ ## Washes and gradients
61
61
 
62
- - **Overlays** are translucent tints layered over surfaces, like the subtle
63
- tint a card gets on hover. Set a colour and opacity per state.
62
+ - **Scrims** are translucent layers that dim what sits behind them, like the
63
+ one a dialog draws over the page. Set a colour and opacity per stop.
64
+ - **Tints** are the opposite operation: they shade the surface they sit on
65
+ rather than dimming what is behind it, which is what a hover needs.
64
66
  - **Gradients** are reusable gradient tokens with a stop list and direction, for
65
67
  hero panels and accent backgrounds.
66
68
 
@@ -4,7 +4,7 @@
4
4
  export const docContent: Record<string, string> = {
5
5
  "01-overview": "# Overview\n\nLiveTokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Sketch mode](sketch-mode.md)**: redraw the page by hand.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
6
6
  "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
7
- "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has four views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
7
+ "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has four views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
8
8
  "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any theme you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
9
9
  "light-and-dark": "# Light and dark\n\nSome things on a page cannot be written as a token. A wordmark drawn in white\ndisappears on a pale theme. Ink that multiplies onto paper vanishes on a dark\none. A photograph behind a headline is dark no matter what the palette says.\n\nEach of those needs the same fact first: which way does the surface behind this\nthing lean? One attribute carries it.\n\n## The attribute\n\n`data-backdrop` is either `light` or `dark`, and it does two things at once: it\nselects, so a rule can key on it, and it sets `color-scheme`, so every\n`light-dark()` under it resolves the half that reads.\n\n```css\n.title {\n color: light-dark(var(--color-black), var(--color-white));\n}\n```\n\nThat line is right on both sides of the theme, and it is right inside a dark\nband on a pale page, because the nearest `color-scheme` wins.\n\n## Stating it\n\nPut it in the markup when the surface knows its own tone — a hero over a\nphotograph, a plate that stays pale in every theme:\n\n```svelte\n<div class=\"hero-panel\" data-backdrop=\"dark\">\n```\n\nA stated tone beats any measurement, and it inherits, so everything inside the\npanel resolves against it.\n\n## Measuring it\n\nWhere the tone is a property of the theme rather than of the markup, let it be\nmeasured:\n\n```svelte\n<script>\n import { backdrop } from '@motion-proto/live-tokens/backdrop';\n</script>\n\n<section use:backdrop>\n```\n\nThe action reads whatever actually paints behind the element — the nearest\nancestor with an opaque fill, averaged across its gradient stops, falling back\nto the theme's `--page-bg` — and stamps the answer. It re-reads when the theme\nchanges, which the editor does by rewriting custom properties with no reload,\nso the stamp follows a live edit.\n\nThe page itself is stamped for you: the build bakes the production theme's\npolarity into `tokens.generated.css`, so the first paint is already right, and\n`syncDocumentBackdrop()` keeps `<html>` current as themes switch.\n\n```ts\nimport { syncDocumentBackdrop } from '@motion-proto/live-tokens/backdrop';\n\nsyncDocumentBackdrop();\n```\n\n## Reading it from JavaScript\n\nAnything that paints outside CSS — a canvas, a WebGL uniform, an `<img>` that\ncomes in two versions — asks the same question through the same module:\n\n```ts\nimport { isLightBackdrop, watchBackdrop, cssColorToHex } from '@motion-proto/live-tokens/backdrop';\n\nconst stop = watchBackdrop(logoEl, {\n stamp: false,\n onChange: (polarity) => (src = polarity === 'light' ? darkMark : lightMark),\n});\n```\n\n`isLightBackdrop(el)` answers once. `watchBackdrop` keeps answering and returns\na stop function. `cssColorToHex` resolves any CSS colour — including the\n`oklch()` a token holds — to a hex a non-CSS consumer can take.\n\n## What it does not do\n\nPolarity is a property of a surface, not of a component, so nothing is stamped\nfor you below `<html>`: a section that needs an answer either states one or asks\nfor one. And a measurement reads the paint at the moment it runs — an element\nthat scrolls from a pale band onto a dark one keeps the answer it was given.\nState the tone on each band instead.\n",
10
10
  "sketch-mode": "# Sketch mode\n\nSketch mode redraws your whole page as if it had been drawn by hand. Every\ncomponent keeps its own colours, spacing and corners; what changes is the line\nthey are drawn with.\n\nIt is an effect layer, not a set of token values. It never touches a token\nitself, so turning it off returns every component to exactly what its tokens\nalready say.\n\nOpen the **Sketchstyle** view in the editor and switch **Sketch mode** on. The effect\napplies to the page behind the editor as well as to the preview, so what you see\nin context is what it does.\n\n## What it draws\n\nEach component's fill and outline are repainted from the tokens that component\nalready owns. The real background and border are hidden behind them, then both\nare pushed around one shared field of noise. Because every component samples the\nsame field, the whole page reads as one drawing rather than as a set of\nseparately wobbled boxes.\n\n## The sketchstyles\n\nSeven sketchstyles ship with the package, and each is a complete set of dials rather\nthan just a name:\n\n- **Pencil.** Two graphite passes on their own seeds, so the outline disagrees\n with itself the way a hand coming back round does.\n- **Marker.** A broad translucent nib gone round twice on the same line, so the\n overlap darkens and the ink pools where it slows.\n- **Whiteboard.** The fattest nib on glass, with a mask that streaks the fill\n like a half-wiped board.\n- **Hatched.** An etching. The fill is angled shading and the outline a single\n hard-edged scratch.\n- **Dashed.** A drafting outline: one slow drift along the ruler, broken into\n strokes. The clean pole.\n- **Napkin.** Ballpoint in a hurry. Everything loose at once.\n- **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.\n\nAll seven ship as files, one per sketchstyle, under\n`src/live-tokens/data/sketch-styles/` in the package. There is no sketchstyle that\nexists only as code, so every one of them can be read, copied and edited.\n\nPick one, then move whatever you like. **Save As** keeps your dials under a name\nof your own, alongside the shipped seven, as a file under\n`src/live-tokens/data/sketch-styles/` in your project. **Save** writes them back\nover the sketchstyle you have selected, and lights as soon as the dials leave\nit. On one of your own it writes that file. On a shipped one it writes your\nproject's own copy under the same name, which takes its place in the list;\ndelete that copy and the shipped file behind it comes back. Both are a\ndifferent gesture from saving a theme; see \"Where the settings live\" below.\n\nA theme carries its sketch settings by value, so it can hold a set that belongs\nto no file. The list shows that set as **Unsaved**. It owns no file, so there is\nnothing for Save to write over; **Save As** turns it into a sketchstyle like any\nother, which you can then save over and delete.\n\nYour sketchstyles and the shipped ones are one list. A sketchstyle named after\na shipped one replaces it, keeping its place in the list, so a project that\nwants its own Pencil saves one and every picker shows that one instead.\n\n## The dials\n\n- **Border.** How far the outline travels and how long its wave is, then its\n width, ink, pressure and pooling. A second pass either copies the first line a\n few pixels off or runs it through the pen again on its own seed.\n- **Fill.** Solid or hatched, how far the fill's edge travels, and how far each\n instance is offset, rotated and scaled from its neighbours. **Ink coverage**\n thins the fill with a field of blotches: set their size across and down and\n how many levels of detail, then work the field as a levels control. The two\n sizes move together under a chain; break it and the blotches stretch, which\n reads as ink dragged along the axis you widened, and a **Rotation** dial joins\n them to point that stretch anywhere you like. The field always runs black\n to white whatever the noise underneath. Steps flattens it into tones, and\n Output squeezes the whole of it into the range the ink covers, from how pale\n it gets at its thinnest to how dense at its fullest. Menus and tooltips are\n drawn solid whatever the coverage dials say: they float over the page, and a\n fill worn through in patches lets the page show through them.\n- **Shape.** **Corner spread** rounds each corner by its own share of the dial,\n so no two match. **Corner travel** leans the drawn box into a quadrilateral\n with no two sides parallel. This is the dial that stops a component reading as\n a rectangle.\n- **Icons and SVG.** Glyph travel and wavelength on their own scale. A glyph is\n all curves already, so it needs more travel than a card's long straight edge\n before the wobble reads at all.\n- **Noise.** The shared field itself: its wavelength, how many layers of detail\n sit on it, and the shape of its wave. A square wave sends nearly every edge to\n full travel, which is what makes the effect stronger rather than bigger.\n\n## Where the settings live\n\nThe sketch layer is part of the theme, the same way colors and type are.\n**Save** in the Theme panel folds your dials into the open theme; **Load**\napplies whatever a theme carries, and turns the effect off for a theme that\ncarries none.\n\nUntil you save, the dials sit in your browser only. The Theme panel calls\nthat state off the theme, the same word it uses for an unsaved component\nchange. The built-in **Motion Proto** theme is read-only, so Save\nis disabled there; use **Save As** to fold the dials into a theme of your\nown.\n\n**Save** and **Save As** in the **Sketchstyle** view are a different gesture.\nThey write a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a sketchstyle\nyou can pick from any theme. Neither touches the open theme, and neither marks\nthe theme as changed.\n\n## Shipping the layer\n\nThe dev server reads the open theme and paints whatever it carries. A built site\nhas no server to ask, so it hands the field over itself:\n\n```ts\nimport { seedSketchFromTheme } from '@motion-proto/live-tokens/sketch';\nimport theme from './live-tokens/data/themes/sketchy.json';\n\nseedSketchFromTheme(theme.sketchSettings);\nawait bootLiveTokens(App, '#app');\n```\n\nCall it before mounting, so the sketch layer is up on the first frame. Pass the field\nraw. A theme written against older dial names is carried forward on the way in,\nthe same reconciliation the dev server runs on every theme it reads.\n\nNothing is baked. `tokens.generated.css` still holds token values only, and the\nlayer stays JavaScript the page runs, because it builds an SVG filter bank\nrather than a set of custom properties.\n\nA visitor who has picked a sketchstyle of their own keeps it, None included. The theme\nseeds a browser that has decided nothing and never overwrites one that has, so\ncalling this on every boot is safe.\n\n### Your own sketchstyles\n\nThe dev server lists the files in `sketch-styles/`. A built site has no server\nto ask, so it hands them over at boot, the way it hands over components:\n\n```ts\nconst files = import.meta.glob<{ name?: string; settings: unknown }>(\n './live-tokens/data/sketch-styles/*.json',\n { eager: true, import: 'default' },\n);\n\nawait bootLiveTokens(App, '#app', {\n sketchStyles: Object.entries(files).map(([path, file]) => {\n const id = path.split('/').pop()!.replace('.json', '');\n return { id, label: file.name || id, settings: file.settings };\n }),\n});\n```\n\nProjects made with `create` ship this already. The file's slug is the sketchstyle's\nid, so a sketchstyle picked in the editor keeps working once the site is built.\n\n### Building a picker\n\n`sketchStyles` is every sketchstyle on offer, shipped and your own, as a store. Give\neach row `setSketch(style.id)`, and add your own **None** row: off is a state of\nthe effect rather than one of the sketchstyles.\n\n`unsavedSketchStyle` is the settings the theme carries as one more row. It is null when the\ntheme carries none, and null when what it carries matches a sketchstyle already in\n`sketchStyles`, since that row names it. Its id goes to `setSketch` like any\nother, so a visitor who wanders off the theme's own settings can come back to it.\n\n## Drawing your own elements\n\nThe layer draws a fixed set of parts: the shipped components, and four classes\nit reserves for you. Nothing else is touched, so a page element or a\nconsumer-authored component is left crisp until it carries one of them.\n\n| Class | For |\n|---------------------|-----------------------------------------------------------|\n| `sketch-surface` | A box. The default treatment. |\n| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |\n| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |\n| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |\n\nPick by size, not by kind: a card and a modal both take `sketch-container`, a\nbadge and a pill both take `sketch-chip`.\n\nThe class opts the element in; it names no colours, so the element states its\nown. `--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color`,\n`--sketch-radius` and `--sketch-shadow` name the fill, the outline, the hatching\nink, the corners and the shadow for one element and everything inside it. The\nlayer blanks the real background and border, so an element whose fill matters\nunder Sketch mode has to name it here as well as paint it.\n\nThe layer also paints on the element's `::before` and `::after`, forces its\n`overflow` visible, and gives it a stacking context of its own. Keep the class\noff anything that owns a pseudo-element, clips its content, or is positioned\nabsolutely, and put it on a wrapper instead.\n\n```css\n.my-callout {\n background: var(--surface-brand-lowest);\n border: var(--border-width-1) solid var(--border-brand);\n border-radius: var(--radius-xl);\n\n --sketch-fill: var(--surface-brand-lowest);\n --sketch-stroke: var(--border-brand);\n --sketch-radius: var(--radius-xl);\n}\n```\n\nA gradient is a valid fill: the shorthand's last layer takes a colour or an\nimage, so `--sketch-fill` accepts either. States work the same way, since\nnothing is competing with you for the value:\n\n```css\n.my-callout:hover { --sketch-stroke: var(--border-brand-strong); }\n```\n\n## Images inside a drawn part\n\nA drawn part's `overflow` is forced visible, because the fill and outline are\npainted on pseudo-elements that travel past the box and would otherwise be cut\noff at its edge. A background that bleeds is the effect working. An image that\nbleeds is not: it keeps its square corners while the card around it turns.\n\nMedia that runs to a part's edge therefore has to carry that part's corners\nitself. `--sketch-radius` is the radius the layer drew, and it inherits, so a\nchild can read it and fall back to its own value when Sketch mode is off:\n\n```css\n.cover {\n overflow: hidden;\n border-top-left-radius: var(--sketch-radius, var(--card-default-radius));\n border-top-right-radius: var(--sketch-radius, var(--card-default-radius));\n}\n```\n\nCorner spread is per-corner and per-instance, so at high spread the crop is the\nmean rather than an exact trace of the drawn edge.\n\nA rule made from a `border` is not a box and cannot be displaced. Make it an\nelement, give it `sketch-rule`, and name its ink:\n\n```html\n<div class=\"rule sketch-rule\"></div>\n```\n```css\n.rule {\n height: var(--border-width-2);\n background: var(--border-brand);\n --sketch-fill: var(--border-brand);\n}\n```\n\nIcons and inline SVG take the wobble directly, since a glyph has no box to\nredraw. Body type is left alone: an icon is a shape and survives a wobble, a\nparagraph is not.\n\n`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It\ninherits, so one declaration covers everything under it, and it takes the ink\nmask off as well as the wobble:\n\n```css\n/* Crisp. Chrome, a logo, anything that has to stay exact. */\n.app-bar { --sketch-icon-off: none; }\n\n/* Drawn back rather than off, at a third of the travel. Small artwork, and\n type set as an SVG, which the layer reads as one large glyph. */\n.wordmark { --sketch-icon-off: var(--sketch-icon-soft); }\n```\n\nThe **Blotch size** dial under Icons and SVG is a share of the glyph rather than\na px size, because no px size is right for both a 16px icon and a page-wide\ndrawing. At 100% every glyph gets one period of the field across it whatever its\nsize. Below that the field repeats inside the glyph and the blotches get finer.\nAbove it a glyph reads part of one blotch, so the mask thins the whole glyph\nunevenly instead of breaking it up. The fill's blotches stay in px, since a\ncomponent does have a size to state one against.\n",
@@ -22,7 +22,7 @@
22
22
  { id: 'typography', label: 'Typography', icon: 'fas fa-font' },
23
23
  { id: 'icon-sizes', label: 'Icon Sizes', icon: 'fas fa-expand' },
24
24
  { id: 'shadows', label: 'Shadows', icon: 'fas fa-cloud' },
25
- { id: 'overlays', label: 'Overlays', icon: 'fas fa-layer-group' },
25
+ { id: 'washes', label: 'Washes', icon: 'fas fa-layer-group' },
26
26
  { id: 'gradients', label: 'Gradients', icon: 'fas fa-droplet' },
27
27
  { id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
28
28
  ];
@@ -31,9 +31,9 @@
31
31
  {
32
32
  title: 'Overlays',
33
33
  swatches: [
34
- { name: 'Low', variable: '--overlay-low', description: 'Light backdrop / pressed wash' },
35
- { name: 'Overlay', variable: '--overlay', description: 'Standard backdrop' },
36
- { name: 'High', variable: '--overlay-high', description: 'Strong backdrop' }
34
+ { name: 'Low', variable: '--scrim-low', description: 'Light scrim' },
35
+ { name: 'Scrim', variable: '--scrim', description: 'Standard scrim' },
36
+ { name: 'High', variable: '--scrim-high', description: 'Strong scrim' }
37
37
  ]
38
38
  },
39
39
  {
@@ -286,6 +286,7 @@
286
286
  <div
287
287
  class="ui-token-selector"
288
288
  class:disabled
289
+ class:locked={selectionsLocked}
289
290
  data-token-variable={variable}
290
291
  bind:this={container}
291
292
  >
@@ -7,7 +7,7 @@
7
7
  import TextStylesSection from './TextStylesSection.svelte';
8
8
  import ColumnsSection from './sections/ColumnsSection.svelte';
9
9
  import TokenScaleTable from './sections/TokenScaleTable.svelte';
10
- import OverlaysSection from './sections/OverlaysSection.svelte';
10
+ import WashesSection from './sections/WashesSection.svelte';
11
11
  import GradientsSection from './sections/GradientsSection.svelte';
12
12
  import ShadowsSection from './sections/ShadowsSection.svelte';
13
13
  import {
@@ -138,7 +138,7 @@
138
138
 
139
139
 
140
140
  <!-- Overlays -->
141
- <OverlaysSection {copiedVar} oncopy={copyVariable} />
141
+ <WashesSection {copiedVar} oncopy={copyVariable} />
142
142
 
143
143
  <!-- Gradients -->
144
144
  <GradientsSection {copiedVar} oncopy={copyVariable} />
@@ -1,18 +1,19 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * Overlay + hover stops. Each stop binds an existing color token through
4
- * `UIPaletteSelector`; the picker handles family/step selection and
5
- * opacity, and writes route through `onwrite` into the overlays slice
6
- * (see editorStore.makeDefaultOverlaysState). The slice fans the
7
- * resulting color-mix expressions out to :root.
3
+ * Scrim + tint stops. A scrim dims what sits behind it; a tint shades the
4
+ * surface it sits on. Each stop binds an existing color token through
5
+ * `UIPaletteSelector`; the picker handles family/step selection and opacity,
6
+ * and writes route through `onwrite` into the washes slice
7
+ * (see editorStore.makeDefaultWashesState). The slice fans the resulting
8
+ * color-mix expressions out to :root.
8
9
  */
9
10
  import { editorState, mutate } from '../../core/store/editorStore';
10
- import type { OverlayToken } from '../../core/store/editorTypes';
11
+ import type { WashToken } from '../../core/store/editorTypes';
11
12
  import {
12
- makeDefaultOverlayTokens,
13
- makeDefaultHoverTokens,
14
- parseOverlayCss,
15
- } from '../../core/themes/slices/overlays';
13
+ makeDefaultScrimTokens,
14
+ makeDefaultTintTokens,
15
+ parseWashCss,
16
+ } from '../../core/themes/slices/washes';
16
17
  import UIPaletteSelector from '../UIPaletteSelector.svelte';
17
18
 
18
19
  interface Props {
@@ -22,7 +23,7 @@
22
23
 
23
24
  let { copiedVar = null, oncopy }: Props = $props();
24
25
 
25
- type Channel = 'overlay' | 'hover';
26
+ type Channel = 'scrim' | 'tint';
26
27
 
27
28
  function copy(v: string) { oncopy?.(v); }
28
29
 
@@ -31,41 +32,41 @@
31
32
  * - bare `--name` → 100% opacity on that alias.
32
33
  * - `color-mix(in srgb, var(--name) N%, transparent)` → alias + N%.
33
34
  * - other (incl. `transparent`) → ignored; the picker's "None" option
34
- * doesn't apply to overlays. */
35
+ * doesn't apply to a wash. */
35
36
  function handleWrite(channel: Channel, idx: number, value: string | null) {
36
37
  mutate(`${channel} ${idx} edit`, (s) => {
37
- const arr = channel === 'overlay' ? s.overlays.tokens : s.overlays.hoverTokens;
38
+ const arr = channel === 'scrim' ? s.washes.scrims : s.washes.tints;
38
39
  const t = arr[idx];
39
40
  if (!t) return;
40
41
  if (value === null) {
41
- const defaults = channel === 'overlay' ? makeDefaultOverlayTokens() : makeDefaultHoverTokens();
42
+ const defaults = channel === 'scrim' ? makeDefaultScrimTokens() : makeDefaultTintTokens();
42
43
  const d = defaults[idx];
43
44
  if (d) { t.alias = d.alias; t.opacity = d.opacity; }
44
45
  return;
45
46
  }
46
47
  if (value.startsWith('--')) { t.alias = value; t.opacity = 1; return; }
47
- const parsed = parseOverlayCss(value);
48
+ const parsed = parseWashCss(value);
48
49
  if (parsed) { t.alias = parsed.alias; t.opacity = parsed.opacity; }
49
50
  });
50
51
  }
51
52
 
52
- function copyCss(t: OverlayToken): string {
53
+ function copyCss(t: WashToken): string {
53
54
  const pct = Math.round(t.opacity * 100);
54
55
  return pct >= 100 ? `var(${t.alias})` : `color-mix(in srgb, var(${t.alias}) ${pct}%, transparent)`;
55
56
  }
56
57
  </script>
57
58
 
58
- <section class="section" id="overlays">
59
- <h2 class="section-title">Overlays</h2>
59
+ <section class="section" id="washes">
60
+ <h2 class="section-title">Washes</h2>
60
61
 
61
- <h3 class="group-title">Dark Overlays</h3>
62
- <div class="overlays-grid">
63
- {#each $editorState.overlays.tokens as token, i (token.variable)}
64
- <div class="overlay-row">
65
- <div class="overlay-swatch-wrap">
66
- <div class="overlay-swatch" style="background: var({token.variable});"></div>
62
+ <h3 class="group-title">Scrims</h3>
63
+ <div class="washes-grid">
64
+ {#each $editorState.washes.scrims as token, i (token.variable)}
65
+ <div class="wash-row">
66
+ <div class="wash-swatch-wrap">
67
+ <div class="wash-swatch" style="background: var({token.variable});"></div>
67
68
  </div>
68
- <div class="overlay-meta">
69
+ <div class="wash-meta">
69
70
  <button
70
71
  class="token-variable copyable"
71
72
  class:copied={copiedVar === token.variable}
@@ -73,11 +74,11 @@
73
74
  >{copiedVar === token.variable ? 'copied!' : token.variable}</button>
74
75
  <span class="token-value">{token.label} — {Math.round(token.opacity * 100)}%</span>
75
76
  </div>
76
- <div class="overlay-picker">
77
+ <div class="wash-picker">
77
78
  <UIPaletteSelector
78
79
  variable={token.variable}
79
80
  showNone={false}
80
- onwrite={(v) => handleWrite('overlay', i, v)}
81
+ onwrite={(v) => handleWrite('scrim', i, v)}
81
82
  />
82
83
  </div>
83
84
  <button
@@ -91,14 +92,14 @@
91
92
  {/each}
92
93
  </div>
93
94
 
94
- <h3 class="group-title">Hover Overlays</h3>
95
- <div class="overlays-grid">
96
- {#each $editorState.overlays.hoverTokens as token, i (token.variable)}
97
- <div class="overlay-row">
98
- <div class="overlay-swatch-wrap overlay-swatch-wrap--dark">
99
- <div class="overlay-swatch" style="background: var({token.variable});"></div>
95
+ <h3 class="group-title">Tints</h3>
96
+ <div class="washes-grid">
97
+ {#each $editorState.washes.tints as token, i (token.variable)}
98
+ <div class="wash-row">
99
+ <div class="wash-swatch-wrap wash-swatch-wrap--dark">
100
+ <div class="wash-swatch" style="background: var({token.variable});"></div>
100
101
  </div>
101
- <div class="overlay-meta">
102
+ <div class="wash-meta">
102
103
  <button
103
104
  class="token-variable copyable"
104
105
  class:copied={copiedVar === token.variable}
@@ -106,11 +107,11 @@
106
107
  >{copiedVar === token.variable ? 'copied!' : token.variable}</button>
107
108
  <span class="token-value">{token.label} — {Math.round(token.opacity * 100)}%</span>
108
109
  </div>
109
- <div class="overlay-picker">
110
+ <div class="wash-picker">
110
111
  <UIPaletteSelector
111
112
  variable={token.variable}
112
113
  showNone={false}
113
- onwrite={(v) => handleWrite('hover', i, v)}
114
+ onwrite={(v) => handleWrite('tint', i, v)}
114
115
  />
115
116
  </div>
116
117
  <button
@@ -148,13 +149,13 @@
148
149
  margin: 0;
149
150
  }
150
151
 
151
- .overlays-grid {
152
+ .washes-grid {
152
153
  display: flex;
153
154
  flex-direction: column;
154
155
  gap: var(--ui-space-8);
155
156
  }
156
157
 
157
- .overlay-row {
158
+ .wash-row {
158
159
  display: grid;
159
160
  grid-template-columns: 3.5rem minmax(10rem, 1fr) minmax(14rem, 1.5fr) auto;
160
161
  gap: var(--ui-space-12);
@@ -165,7 +166,7 @@
165
166
  border-radius: var(--ui-radius-md);
166
167
  }
167
168
 
168
- .overlay-swatch-wrap {
169
+ .wash-swatch-wrap {
169
170
  width: 3.5rem;
170
171
  height: 3.5rem;
171
172
  border-radius: var(--ui-radius-sm);
@@ -182,7 +183,7 @@
182
183
  background-color: #fff;
183
184
  }
184
185
 
185
- .overlay-swatch-wrap--dark {
186
+ .wash-swatch-wrap--dark {
186
187
  background-color: #222;
187
188
  background-image:
188
189
  linear-gradient(45deg, #333 25%, transparent 25%),
@@ -191,12 +192,12 @@
191
192
  linear-gradient(-45deg, transparent 75%, #333 75%);
192
193
  }
193
194
 
194
- .overlay-swatch {
195
+ .wash-swatch {
195
196
  position: absolute;
196
197
  inset: 0;
197
198
  }
198
199
 
199
- .overlay-meta {
200
+ .wash-meta {
200
201
  display: flex;
201
202
  flex-direction: column;
202
203
  gap: var(--ui-space-2);
@@ -220,7 +221,7 @@
220
221
  color: var(--ui-text-muted);
221
222
  }
222
223
 
223
- .overlay-picker { min-width: 0; }
224
+ .wash-picker { min-width: 0; }
224
225
 
225
226
  .copy-css-btn {
226
227
  all: unset;
@@ -2259,12 +2259,12 @@
2259
2259
  "--columns-max-width": "1440px",
2260
2260
  "--columns-gutter": "32px",
2261
2261
  "--columns-margin": "0px",
2262
- "--overlay-low": "color-mix(in srgb, var(--surface-neutral-lowest) 38%, transparent)",
2263
- "--overlay": "color-mix(in srgb, var(--surface-neutral-lowest) 51%, transparent)",
2264
- "--overlay-high": "color-mix(in srgb, var(--surface-neutral-lowest) 64%, transparent)",
2265
- "--hover-low": "color-mix(in srgb, var(--text-primary) 5%, transparent)",
2266
- "--hover": "color-mix(in srgb, var(--text-primary) 10%, transparent)",
2267
- "--hover-high": "color-mix(in srgb, var(--text-primary) 15%, transparent)",
2262
+ "--scrim-low": "color-mix(in srgb, var(--surface-neutral-lowest) 38%, transparent)",
2263
+ "--scrim": "color-mix(in srgb, var(--surface-neutral-lowest) 51%, transparent)",
2264
+ "--scrim-high": "color-mix(in srgb, var(--surface-neutral-lowest) 64%, transparent)",
2265
+ "--tint-low": "color-mix(in srgb, var(--text-primary) 5%, transparent)",
2266
+ "--tint": "color-mix(in srgb, var(--text-primary) 10%, transparent)",
2267
+ "--tint-high": "color-mix(in srgb, var(--text-primary) 15%, transparent)",
2268
2268
  "--shadow-sm": "1px 1px 2px hsla(237, 18%, 3%, 0.25)",
2269
2269
  "--shadow-md": "3px 3px 6px hsla(237, 18%, 3%, 0.25)",
2270
2270
  "--shadow-lg": "5px 5px 9px hsla(237, 18%, 3%, 0.25)",