@motion-proto/live-tokens 0.56.1 → 0.57.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 (90) hide show
  1. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +2 -2
  2. package/CHANGELOG.md +124 -0
  3. package/README.md +1 -1
  4. package/bin/cli.mjs +2 -1
  5. package/dist-plugin/adjust/index.cjs +1 -0
  6. package/dist-plugin/adjust/index.d.cts +2 -2
  7. package/dist-plugin/adjust/index.d.ts +2 -2
  8. package/dist-plugin/adjust/index.js +1 -1
  9. package/dist-plugin/{chunk-TKZBVIW5.js → chunk-E5QYON4L.js} +59 -2
  10. package/dist-plugin/{chunk-PB2JTK2H.js → chunk-LW4SR7AZ.js} +39 -6
  11. package/dist-plugin/{chunk-YR6GPXW2.js → chunk-XWXIMTWZ.js} +0 -5
  12. package/dist-plugin/{chunk-D3ZVKOR4.js → chunk-Y5CNFSSV.js} +1 -0
  13. package/dist-plugin/{dataPaths-DBN0RPuT.d.cts → dataPaths-CRfD1LdA.d.cts} +3 -0
  14. package/dist-plugin/{dataPaths-DBN0RPuT.d.ts → dataPaths-CRfD1LdA.d.ts} +3 -0
  15. package/dist-plugin/fontPairing/index.cjs +4 -2
  16. package/dist-plugin/fontPairing/index.d.cts +3 -3
  17. package/dist-plugin/fontPairing/index.d.ts +3 -3
  18. package/dist-plugin/fontPairing/index.js +4 -3
  19. package/dist-plugin/generateColorsAndType/index.cjs +22 -9
  20. package/dist-plugin/generateColorsAndType/index.d.cts +2 -2
  21. package/dist-plugin/generateColorsAndType/index.d.ts +2 -2
  22. package/dist-plugin/generateColorsAndType/index.js +8 -5
  23. package/dist-plugin/index.cjs +158 -9
  24. package/dist-plugin/index.d.cts +1 -1
  25. package/dist-plugin/index.d.ts +1 -1
  26. package/dist-plugin/index.js +71 -4
  27. package/dist-plugin/migrateData/index.cjs +1 -0
  28. package/dist-plugin/migrateData/index.d.cts +1 -1
  29. package/dist-plugin/migrateData/index.d.ts +1 -1
  30. package/dist-plugin/migrateData/index.js +1 -1
  31. package/dist-plugin/{themeTypes-BAqtv4XO.d.cts → themeTypes-DSV3Zisf.d.cts} +12 -1
  32. package/dist-plugin/{themeTypes-BAqtv4XO.d.ts → themeTypes-DSV3Zisf.d.ts} +12 -1
  33. package/dist-plugin/tokensCssMigrations/index.cjs +58 -1
  34. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  36. package/dist-plugin/tokensCssMigrations/index.js +3 -3
  37. package/package.json +3 -1
  38. package/src/editor/core/fonts/applyFontPairing.ts +3 -2
  39. package/src/editor/core/fonts/fontLoader.ts +1 -0
  40. package/src/editor/core/fonts/fontMigration.ts +31 -1
  41. package/src/editor/core/palettes/paletteDerivation.ts +9 -2
  42. package/src/editor/core/sketch/maskField.ts +381 -0
  43. package/src/editor/core/sketch/sketchLayer.ts +1028 -0
  44. package/src/editor/core/sketch/sketchPresetService.ts +65 -0
  45. package/src/editor/core/sketch/sketchPresets.ts +328 -0
  46. package/src/editor/core/sketch/sketchStore.ts +190 -0
  47. package/src/editor/core/store/editorPersistence.ts +28 -12
  48. package/src/editor/core/store/editorTypes.ts +4 -1
  49. package/src/editor/core/store/editorViewStore.ts +2 -2
  50. package/src/editor/core/store/gradientSource.ts +15 -1
  51. package/src/editor/core/themes/parsers/gradient.ts +62 -4
  52. package/src/editor/core/themes/slices/gradients.ts +75 -14
  53. package/src/editor/core/themes/themeTypes.ts +6 -1
  54. package/src/editor/docs/Docs.svelte +4 -3
  55. package/src/editor/docs/chapters.ts +1 -0
  56. package/src/editor/docs/content/01-overview.md +2 -1
  57. package/src/editor/docs/content/editing-tokens.md +5 -1
  58. package/src/editor/docs/content/sketch-mode.md +89 -0
  59. package/src/editor/docs/content.generated.ts +3 -2
  60. package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
  61. package/src/editor/pages/EditorShell.svelte +43 -0
  62. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  63. package/src/editor/ui/FontStackEditor.svelte +3 -0
  64. package/src/editor/ui/GradientEditor.svelte +38 -1
  65. package/src/editor/ui/UIReveal.svelte +7 -1
  66. package/src/editor/ui/UISegmentedControl.svelte +4 -8
  67. package/src/editor/ui/sections/GradientsSection.svelte +47 -1
  68. package/src/editor/ui/sketch/SketchDial.svelte +124 -0
  69. package/src/editor/ui/sketch/SketchPreview.svelte +119 -0
  70. package/src/editor/ui/sketch/SketchRange.svelte +185 -0
  71. package/src/editor/ui/sketch/SketchTab.svelte +1263 -0
  72. package/src/live-tokens/data/colors-and-type/autumn.json +17 -0
  73. package/src/live-tokens/data/colors-and-type/default.json +17 -0
  74. package/src/live-tokens/data/colors-and-type/halloween.json +17 -0
  75. package/src/live-tokens/data/colors-and-type/midnight-study.json +17 -0
  76. package/src/live-tokens/data/colors-and-type/ocean.json +17 -0
  77. package/src/live-tokens/data/colors-and-type/royal-velvet.json +17 -0
  78. package/src/live-tokens/data/colors-and-type/sketches.json +2520 -0
  79. package/src/live-tokens/data/colors-and-type/spring-meadow.json +17 -0
  80. package/src/live-tokens/data/colors-and-type/sunset.json +17 -0
  81. package/src/live-tokens/data/themes/autumn.json +17 -0
  82. package/src/live-tokens/data/themes/halloween.json +17 -0
  83. package/src/live-tokens/data/themes/midnight-study.json +17 -0
  84. package/src/live-tokens/data/themes/ocean.json +17 -0
  85. package/src/live-tokens/data/themes/royal-velvet.json +17 -0
  86. package/src/live-tokens/data/themes/sketches.json +3984 -0
  87. package/src/live-tokens/data/themes/spring-meadow.json +17 -0
  88. package/src/live-tokens/data/themes/sunset.json +17 -0
  89. package/src/live-tokens/data/tokens.generated.css +1 -0
  90. package/src/system/styles/tokens.css +15 -2
@@ -6,6 +6,7 @@
6
6
  import ColorsTab from '../ui/colors/ColorsTab.svelte';
7
7
  import EditorViewSwitcher from '../ui/EditorViewSwitcher.svelte';
8
8
  import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
9
+ import SketchTab from '../ui/sketch/SketchTab.svelte';
9
10
  import ThemePanel from '../ui/ThemePanel.svelte';
10
11
  import { scrollSectionIntoView } from '../ui/scrollSection';
11
12
  import { editorView, sidebarCondensed, selectedComponent } from '../core/store/editorViewStore';
@@ -31,11 +32,22 @@
31
32
  { id: 'colors-story', label: 'Story', icon: 'fas fa-layer-group' }
32
33
  ];
33
34
 
35
+ const sketchNavItems = [
36
+ { id: 'sketch-border', label: 'Border', icon: 'fas fa-pen-nib' },
37
+ { id: 'sketch-fill', label: 'Fill', icon: 'fas fa-fill-drip' },
38
+ { id: 'sketch-shape', label: 'Shape', icon: 'fas fa-draw-polygon' },
39
+ { id: 'sketch-icons', label: 'Icons', icon: 'fas fa-shapes' },
40
+ { id: 'sketch-noise', label: 'Noise', icon: 'fas fa-wave-square' },
41
+ { id: 'sketch-preview', label: 'Preview', icon: 'fas fa-eye' }
42
+ ];
43
+
34
44
  const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
35
45
  const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
36
46
  const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
37
47
 
38
48
  let selectedTokenSection: string | null = $state(null);
49
+ /** Rebuilt per click so a panel can react to the same target twice. */
50
+ let sectionJump: { id: string } | null = $state(null);
39
51
 
40
52
  // 'auto' = open by default. Auto-condensing on shellWidth caused a bounce
41
53
  // as the overlay panel grew past the threshold mid-animation.
@@ -70,10 +82,19 @@
70
82
 
71
83
  function scrollToSection(sectionId: string) {
72
84
  selectedTokenSection = sectionId;
85
+ sectionJump = { id: sectionId };
73
86
  const el = document.getElementById(sectionId);
74
87
  if (el) scrollSectionIntoView(el);
75
88
  }
76
89
 
90
+ /** The Sketch panel is an accordion, so it opens the target and scrolls once
91
+ the reveal has settled. Scrolling from here would read offsetTop before
92
+ the other sections have collapsed. */
93
+ function jumpToSketchSection(sectionId: string) {
94
+ selectedTokenSection = sectionId;
95
+ sectionJump = { id: sectionId };
96
+ }
97
+
77
98
  function selectComponent(id: string) {
78
99
  selectedComponent.set(id);
79
100
  hideHint();
@@ -129,6 +150,26 @@
129
150
  <ThemePanel />
130
151
  </div>
131
152
  {/if}
153
+ {:else if $editorView === 'sketch'}
154
+ <div class="nav-items">
155
+ {#each sketchNavItems as item (item.id)}
156
+ <button
157
+ class="nav-item"
158
+ class:active={selectedTokenSection === item.id}
159
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
160
+ onmouseleave={hideHint}
161
+ onclick={() => jumpToSketchSection(item.id)}
162
+ >
163
+ <i class={item.icon}></i>
164
+ <span class="nav-label">{item.label}</span>
165
+ </button>
166
+ {/each}
167
+ </div>
168
+ {#if !condensed}
169
+ <div class="sidebar-footer">
170
+ <ThemePanel />
171
+ </div>
172
+ {/if}
132
173
  {:else if $editorView === 'colors'}
133
174
  <div class="nav-items">
134
175
  {#each colorsNavItems as item (item.id)}
@@ -202,6 +243,8 @@
202
243
  <VariablesTab />
203
244
  {:else if $editorView === 'colors'}
204
245
  <ColorsTab />
246
+ {:else if $editorView === 'sketch'}
247
+ <SketchTab {sectionJump} />
205
248
  {:else}
206
249
  <ComponentsTab selectedComponent={$selectedComponent} />
207
250
  {/if}
@@ -18,19 +18,21 @@
18
18
  let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
19
19
  let colorsDisabled = $derived($parentRoute === DEFAULT_COLORS_PATH);
20
20
 
21
- // Editing flow order: Tokens → Colors → Components. The condensed rail cycles
21
+ // Editing flow order: Tokens → Colors → Components → Sketch. The condensed rail cycles
22
22
  // through these; a view is skipped while it's disabled (already on that
23
23
  // page) so the cycle never lands on a dead view.
24
- const CYCLE: readonly EditorView[] = ['tokens', 'colors', 'components'];
24
+ const CYCLE: readonly EditorView[] = ['tokens', 'colors', 'components', 'sketch'];
25
25
  const ICONS: Record<EditorView, string> = {
26
26
  tokens: 'fa-sliders',
27
27
  colors: 'fa-palette',
28
28
  components: 'fa-cubes',
29
+ sketch: 'fa-pen-nib',
29
30
  };
30
31
  const LABELS: Record<EditorView, string> = {
31
32
  tokens: 'Tokens',
32
33
  colors: 'Colors',
33
34
  components: 'Components',
35
+ sketch: 'Sketch',
34
36
  };
35
37
 
36
38
  function set(v: EditorView) {
@@ -103,6 +105,17 @@
103
105
  <span class="radio" aria-hidden="true"></span>
104
106
  <span>Components</span>
105
107
  </button>
108
+ <button
109
+ type="button"
110
+ role="tab"
111
+ class="seg-btn"
112
+ class:active={$editorView === 'sketch'}
113
+ aria-selected={$editorView === 'sketch'}
114
+ onclick={() => set('sketch')}
115
+ >
116
+ <span class="radio" aria-hidden="true"></span>
117
+ <span>Sketch</span>
118
+ </button>
106
119
  </div>
107
120
  </div>
108
121
  {/if}
@@ -25,6 +25,7 @@
25
25
  '--font-sans',
26
26
  '--font-serif',
27
27
  '--font-mono',
28
+ '--font-editorial',
28
29
  ];
29
30
 
30
31
  // Each stack's terminal fallback (bottom slot). It's locked to system or
@@ -35,6 +36,7 @@
35
36
  '--font-sans': 'sans-serif',
36
37
  '--font-serif': 'serif',
37
38
  '--font-mono': 'monospace',
39
+ '--font-editorial': 'sans-serif',
38
40
  };
39
41
 
40
42
  // The single matching System UI preset paired with each stack — the only two
@@ -44,6 +46,7 @@
44
46
  '--font-sans': 'system-ui-sans',
45
47
  '--font-serif': 'system-ui-serif',
46
48
  '--font-mono': 'system-ui-mono',
49
+ '--font-editorial': 'system-ui-sans',
47
50
  };
48
51
 
49
52
  let fontSourcesList = $derived($editorState.fonts.sources);
@@ -6,6 +6,7 @@
6
6
  import { tick, onMount } from 'svelte';
7
7
  import { get } from 'svelte/store';
8
8
  import type { GradientType, GradientTokenStop } from '../core/store/editorTypes';
9
+ import { LINEAR_DIRECTIONS, type LinearDirection } from '../core/themes/parsers/gradient';
9
10
  import {
10
11
  colorsAndTypeGradientSource,
11
12
  snapshotGradient,
@@ -87,6 +88,11 @@
87
88
  gradientSource.setAngle(detail.value);
88
89
  }
89
90
 
91
+ function onDirectionChange(e: Event) {
92
+ const v = (e.currentTarget as HTMLSelectElement).value;
93
+ gradientSource.setDirection(v === '' ? undefined : (v as LinearDirection));
94
+ }
95
+
90
96
  function onAspectChange(detail: { x: number; y: number }) {
91
97
  gradientSource.setAspect(detail);
92
98
  }
@@ -375,7 +381,26 @@
375
381
  </div>
376
382
  {:else if gradient.type === 'linear'}
377
383
  <div class="ribbon-pad ribbon-pad-linear">
378
- <AngleDial value={gradient.angle} size={64} orientation="vertical" label="" onchange={onAngleChange} />
384
+ <!-- A direction keyword overrides the dial: CSS angles it off the box's
385
+ own diagonal, so the dial has no single value to show. -->
386
+ <AngleDial
387
+ value={gradient.angle}
388
+ size={64}
389
+ orientation="vertical"
390
+ label=""
391
+ onchange={onAngleChange}
392
+ />
393
+ <select
394
+ class="direction-select"
395
+ aria-label="Gradient direction"
396
+ value={gradient.direction ?? ''}
397
+ onchange={onDirectionChange}
398
+ >
399
+ <option value="">{gradient.angle}deg</option>
400
+ {#each LINEAR_DIRECTIONS as d (d)}
401
+ <option value={d}>{d}</option>
402
+ {/each}
403
+ </select>
379
404
  </div>
380
405
  {/if}
381
406
 
@@ -440,6 +465,18 @@
440
465
  {/if}
441
466
 
442
467
  <style>
468
+ .direction-select {
469
+ margin-top: var(--ui-space-2, 8px);
470
+ max-width: 8.5rem;
471
+ font: inherit;
472
+ font-size: 0.75rem;
473
+ color: var(--ui-gray-1000, #eee);
474
+ background: var(--ui-gray-300, #212121);
475
+ border: 1px solid var(--ui-gray-450, #3a3a3a);
476
+ border-radius: 4px;
477
+ padding: 2px 4px;
478
+ }
479
+
443
480
  /* Header labels share grid tracks with the ribbon + pad below them. */
444
481
  .gradient-editor {
445
482
  display: grid;
@@ -1,3 +1,9 @@
1
+ <script module lang="ts">
2
+ /** Shared so a caller that must wait for the pane to settle — scrolling to a
3
+ section it just opened, say — does not hard-code the number. */
4
+ export const REVEAL_MS = 260;
5
+ </script>
6
+
1
7
  <script lang="ts">
2
8
  import type { Snippet } from 'svelte';
3
9
  import { slide } from 'svelte/transition';
@@ -22,7 +28,7 @@
22
28
  children: Snippet;
23
29
  }
24
30
 
25
- let { open, style = undefined, duration = 260, children }: Props = $props();
31
+ let { open, style = undefined, duration = REVEAL_MS, children }: Props = $props();
26
32
 
27
33
  const reduceMotion = typeof window !== 'undefined'
28
34
  && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
@@ -65,7 +65,7 @@
65
65
  align-items: center;
66
66
  gap: var(--ui-space-6, 6px);
67
67
  padding: var(--ui-space-4, 4px) var(--ui-space-12, 12px);
68
- color: var(--ui-text-secondary, rgba(255, 255, 255, 0.65));
68
+ color: var(--ui-text-primary, #fff);
69
69
  font-family: inherit;
70
70
  font-size: var(--ui-font-size-sm, 14px);
71
71
  font-weight: var(--ui-font-weight-medium, 500);
@@ -92,12 +92,12 @@
92
92
 
93
93
  .ui-seg-item:hover:not(.active) {
94
94
  background: rgba(255, 255, 255, 0.06);
95
- color: var(--ui-text-primary, #fff);
96
95
  }
97
96
 
97
+ /* Every label sits at full brightness, so the pill fill is the only thing
98
+ carrying selection. It has to stay clearly lighter than a hover. */
98
99
  .ui-seg-item.active {
99
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.25) 100%);
100
- color: var(--ui-text-primary, #fff);
100
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.3) 100%);
101
101
  }
102
102
 
103
103
  .ui-seg-item:has(input:focus-visible) {
@@ -106,9 +106,5 @@
106
106
 
107
107
  .ui-seg-item i {
108
108
  font-size: var(--ui-font-size-xs, 12px);
109
- color: rgba(255, 255, 255, 0.65);
110
- }
111
- .ui-seg-item.active i {
112
- color: rgba(255, 255, 255, 0.85);
113
109
  }
114
110
  </style>
@@ -9,6 +9,12 @@
9
9
  */
10
10
  import { editorState } from '../../core/store/editorStore';
11
11
  import GradientEditor from '../GradientEditor.svelte';
12
+ import UIPillButton from '../UIPillButton.svelte';
13
+ import {
14
+ addGradientToken,
15
+ removeGradientToken,
16
+ nextGradientSlot,
17
+ } from '../../core/themes/slices/gradients';
12
18
 
13
19
  interface Props {
14
20
  copiedVar?: string | null;
@@ -20,11 +26,37 @@
20
26
  function copy(v: string) { oncopy?.(v); }
21
27
 
22
28
  let editingGradient: string | null = $state(null);
29
+
30
+ /** A new slot seeds from the library's last entry, so it lands as a visible
31
+ * gradient the user edits rather than an invisible transparent one. */
32
+ function addGradient() {
33
+ const tokens = $editorState.gradients.tokens;
34
+ const seed = tokens[tokens.length - 1];
35
+ const variable = nextGradientSlot(tokens);
36
+ addGradientToken({
37
+ variable,
38
+ type: 'linear',
39
+ angle: seed?.angle ?? 90,
40
+ ...(seed?.direction ? { direction: seed.direction } : {}),
41
+ stops: seed
42
+ ? seed.stops.map((st) => ({ ...st }))
43
+ : [
44
+ { position: 0, color: '--color-brand-500' },
45
+ { position: 100, color: '--color-accent-500' },
46
+ ],
47
+ });
48
+ editingGradient = variable;
49
+ }
50
+
51
+ function removeGradient(variable: string) {
52
+ if (editingGradient === variable) editingGradient = null;
53
+ removeGradientToken(variable);
54
+ }
23
55
  </script>
24
56
 
25
57
  <section class="section" id="gradients">
26
58
  <h2 class="section-title">Gradients</h2>
27
- <p class="editor-intro">Each stop references a color token, so palette edits flow through. Add or remove stops; switch between linear and radial.</p>
59
+ <p class="editor-intro">Each stop references a color token, so palette edits flow through. Add or remove stops; switch between linear and radial; point a gradient at an angle or at a corner.</p>
28
60
  <div class="gradients-grid">
29
61
  {#each $editorState.gradients.tokens as token (token.variable)}
30
62
  {@const isEditing = editingGradient === token.variable}
@@ -36,6 +68,13 @@
36
68
  <button class="token-variable copyable" class:copied={copiedVar === token.variable} onclick={() => copy(token.variable)}>{copiedVar === token.variable ? 'copied!' : token.variable}</button>
37
69
  {#if !isEditing}
38
70
  <button class="gradient-edit-btn" onclick={() => editingGradient = token.variable}>Edit</button>
71
+ {#if $editorState.gradients.tokens.length > 1}
72
+ <button
73
+ class="gradient-edit-btn"
74
+ aria-label={`Remove ${token.variable}`}
75
+ onclick={() => removeGradient(token.variable)}
76
+ >Remove</button>
77
+ {/if}
39
78
  {/if}
40
79
  </div>
41
80
  {#if isEditing}
@@ -50,9 +89,16 @@
50
89
  </div>
51
90
  {/each}
52
91
  </div>
92
+ <div class="gradients-actions">
93
+ <UIPillButton onclick={addGradient}>Add gradient</UIPillButton>
94
+ </div>
53
95
  </section>
54
96
 
55
97
  <style>
98
+ .gradients-actions {
99
+ display: flex;
100
+ }
101
+
56
102
  .section {
57
103
  display: flex;
58
104
  flex-direction: column;
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ label: string;
4
+ value: number;
5
+ min: number;
6
+ max: number;
7
+ step: number;
8
+ readout: string;
9
+ hint: string;
10
+ /** Captions under the two ends of the track, for a dial with no natural unit. */
11
+ ends?: [string, string];
12
+ onchange: (value: number) => void;
13
+ }
14
+
15
+ let { label, value, min, max, step, readout, hint, ends, onchange }: Props = $props();
16
+
17
+ let filled = $derived(`${((value - min) / (max - min)) * 100}%`);
18
+ </script>
19
+
20
+ <label class="dial" data-hint={hint} style:--filled={filled}>
21
+ <span class="dial-label">{label}</span>
22
+ <span class="dial-value">{readout}</span>
23
+ <input
24
+ type="range"
25
+ {min}
26
+ {max}
27
+ {step}
28
+ {value}
29
+ oninput={(e) => onchange(+e.currentTarget.value)}
30
+ />
31
+ {#if ends}
32
+ <span class="ends"><span>{ends[0]}</span><span>{ends[1]}</span></span>
33
+ {/if}
34
+ </label>
35
+
36
+ <style>
37
+ .dial {
38
+ position: relative;
39
+ display: grid;
40
+ grid-template-columns: 1fr auto;
41
+ gap: 0 var(--ui-space-12);
42
+ align-items: baseline;
43
+ }
44
+
45
+ .dial-label {
46
+ font-size: var(--ui-font-size-md);
47
+ color: var(--ui-text-primary);
48
+ }
49
+
50
+ .dial-value {
51
+ font-size: var(--ui-font-size-md);
52
+ font-family: var(--ui-font-mono);
53
+ color: var(--ui-text-primary);
54
+ }
55
+
56
+ /* Painted rather than left to accent-color: the browser's own range chrome
57
+ renders the track and thumb too dark to read against --ui-surface-low. */
58
+ input[type='range'] {
59
+ grid-column: 1 / -1;
60
+ width: 100%;
61
+ margin-top: var(--ui-space-2);
62
+ height: var(--ui-space-16);
63
+ -webkit-appearance: none;
64
+ appearance: none;
65
+ background:
66
+ linear-gradient(
67
+ to right,
68
+ var(--ui-text-primary) var(--filled),
69
+ var(--ui-border-high) var(--filled)
70
+ )
71
+ center / 100% 4px no-repeat;
72
+ cursor: ew-resize;
73
+ }
74
+
75
+ input[type='range']::-webkit-slider-thumb {
76
+ -webkit-appearance: none;
77
+ width: var(--ui-space-16);
78
+ height: var(--ui-space-16);
79
+ border-radius: var(--ui-radius-full);
80
+ background: var(--ui-text-primary);
81
+ box-shadow: var(--ui-shadow-sm);
82
+ }
83
+
84
+ input[type='range']::-moz-range-thumb {
85
+ width: var(--ui-space-16);
86
+ height: var(--ui-space-16);
87
+ border: none;
88
+ border-radius: var(--ui-radius-full);
89
+ background: var(--ui-text-primary);
90
+ box-shadow: var(--ui-shadow-sm);
91
+ }
92
+
93
+ .ends {
94
+ grid-column: 1 / -1;
95
+ display: flex;
96
+ justify-content: space-between;
97
+ font-size: var(--ui-font-size-xs);
98
+ color: var(--ui-text-tertiary);
99
+ }
100
+
101
+ input[type='range']:focus-visible {
102
+ outline: 2px solid var(--ui-highlight);
103
+ outline-offset: 4px;
104
+ }
105
+
106
+ /* Absolutely positioned so a hint never reflows the stack of dials under it. */
107
+ .dial:hover::after {
108
+ content: attr(data-hint);
109
+ position: absolute;
110
+ top: calc(100% + var(--ui-space-2));
111
+ left: 0;
112
+ right: 0;
113
+ z-index: 2;
114
+ padding: var(--ui-space-8) var(--ui-space-10);
115
+ background: var(--ui-surface-high);
116
+ border: 1px solid var(--ui-border-low);
117
+ border-radius: var(--ui-radius-md);
118
+ box-shadow: var(--ui-shadow-md);
119
+ color: var(--ui-text-secondary);
120
+ font-size: var(--ui-font-size-sm);
121
+ line-height: var(--ui-line-height-relaxed);
122
+ pointer-events: none;
123
+ }
124
+ </style>
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import Button from '../../../system/components/Button.svelte';
3
+ import Card from '../../../system/components/Card.svelte';
4
+ import Input from '../../../system/components/Input.svelte';
5
+ import Badge from '../../../system/components/Badge.svelte';
6
+ import Callout from '../../../system/components/Callout.svelte';
7
+ import Toggle from '../../../system/components/Toggle.svelte';
8
+ import { setSketchScope } from '../../core/sketch/sketchLayer';
9
+ import type { SketchSettings } from '../../core/sketch/sketchPresets';
10
+
11
+ interface Props {
12
+ settings: SketchSettings;
13
+ enabled: boolean;
14
+ }
15
+
16
+ let { settings, enabled }: Props = $props();
17
+
18
+ let stage = $state<HTMLDivElement | undefined>(undefined);
19
+
20
+ // The scope attribute goes on this container rather than the editor document
21
+ // root, so the editor's own chrome never picks the effect up.
22
+ $effect(() => {
23
+ setSketchScope(stage ?? null, enabled ? settings : null);
24
+ });
25
+ </script>
26
+
27
+ <div class="stage" bind:this={stage}>
28
+ <section class="item">
29
+ <h4>Buttons</h4>
30
+ <div class="cluster">
31
+ <Button variant="primary">Primary</Button>
32
+ <Button variant="secondary">Secondary</Button>
33
+ <Button variant="outline">Outline</Button>
34
+ <Button variant="danger">Danger</Button>
35
+ </div>
36
+ </section>
37
+
38
+ <section class="item">
39
+ <h4>Cards</h4>
40
+ <div class="cluster">
41
+ <Card title="Rough draft">
42
+ <p>Fill and outline displace on different noise seeds, so they disagree at the edges.</p>
43
+ </Card>
44
+ <Card title="Second card">
45
+ <p>Same size and same preset. A different seed keeps the silhouette from repeating.</p>
46
+ </Card>
47
+ </div>
48
+ </section>
49
+
50
+ <section class="item">
51
+ <h4>Badges</h4>
52
+ <div class="cluster">
53
+ <Badge variant="info">Info</Badge>
54
+ <Badge variant="success">Success</Badge>
55
+ <Badge variant="warning">Warning</Badge>
56
+ <Badge variant="danger">Danger</Badge>
57
+ </div>
58
+ </section>
59
+
60
+ <section class="item">
61
+ <h4>Form</h4>
62
+ <div class="cluster stack">
63
+ <Input label="Name" placeholder="Ada Lovelace" hint="Helper text stays crisp" />
64
+ <Toggle label="Enabled" checked />
65
+ </div>
66
+ </section>
67
+
68
+ <section class="item">
69
+ <h4>Callouts</h4>
70
+ <div class="cluster stack">
71
+ <Callout variant="info" label="Note">Body text is never filtered, so it stays fully legible.</Callout>
72
+ <Callout variant="warning" label="Careful">Only the fill and outline layers move.</Callout>
73
+ </div>
74
+ </section>
75
+ </div>
76
+
77
+ <style>
78
+ .stage {
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: var(--ui-space-24);
82
+ padding: var(--ui-space-24);
83
+ background: var(--surface-neutral-lowest);
84
+ border: 1px solid var(--ui-border-low);
85
+ border-radius: var(--ui-radius-md);
86
+ }
87
+
88
+ .item {
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: var(--ui-space-10);
92
+ }
93
+
94
+ h4 {
95
+ margin: 0;
96
+ font-family: var(--ui-font-sans);
97
+ font-size: var(--ui-font-size-xs);
98
+ font-weight: var(--ui-font-weight-semibold);
99
+ letter-spacing: 0.08em;
100
+ text-transform: uppercase;
101
+ color: var(--ui-text-muted);
102
+ }
103
+
104
+ .cluster {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: var(--ui-space-16);
108
+ align-items: flex-start;
109
+ }
110
+
111
+ .cluster.stack {
112
+ flex-direction: column;
113
+ max-width: 28rem;
114
+ }
115
+
116
+ .cluster :global(.card) {
117
+ max-width: 20rem;
118
+ }
119
+ </style>