@motion-proto/live-tokens 0.38.0 → 0.40.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.
@@ -7,10 +7,8 @@
7
7
 
8
8
 
9
9
  interface Props {
10
- color: string;
11
10
  title?: string | null;
12
11
  showRemoveOverride?: boolean;
13
- onColorChange?: (hex: string) => void;
14
12
  onConfirm?: () => void;
15
13
  onCancel?: () => void;
16
14
  onRemoveOverride?: () => void;
@@ -20,110 +18,67 @@
20
18
  * doesn't route slider writes through the editor store, leave this unset.
21
19
  */
22
20
  onSliderStart?: () => void;
23
- // Hue-chroma mode props (for neutral/gray base editing)
24
- mode?: 'hsl' | 'hue-chroma';
25
21
  hue?: number;
26
22
  chroma?: number;
27
- onHueChromaChange?: (hue: number, chroma: number) => void;
23
+ lightness?: number;
24
+ /** Upper bound of the chroma slider (full sRGB gamut by default). */
25
+ chromaMax?: number;
26
+ /** Optional marker on the chroma track flagging the typical-neutral zone. */
27
+ chromaHint?: number;
28
+ onHueChromaChange?: (hue: number, chroma: number, lightness: number) => void;
28
29
  actions?: import('svelte').Snippet;
29
30
  }
30
31
 
31
32
  let {
32
- color,
33
33
  title = null,
34
34
  showRemoveOverride = false,
35
- onColorChange = () => {},
36
35
  onConfirm = () => {},
37
36
  onCancel = () => {},
38
37
  onRemoveOverride = () => {},
39
38
  onSliderStart = () => {},
40
- mode = 'hsl',
41
39
  hue = 0,
42
40
  chroma = 0.04,
41
+ lightness = 55,
42
+ chromaMax = 0.4,
43
+ chromaHint,
43
44
  onHueChromaChange = () => {},
44
45
  actions
45
46
  }: Props = $props();
46
47
 
47
48
  const hasEyeDropper = typeof window !== 'undefined' && 'EyeDropper' in window;
48
- const PREVIEW_LIGHTNESS = 0.55;
49
- const CHROMA_MAX = 0.15;
50
-
51
- // --- HSL helpers (used in hsl mode) ---
52
-
53
- function hexToHsl(hex: string): [number, number, number] {
54
- const r = parseInt(hex.slice(1, 3), 16) / 255;
55
- const g = parseInt(hex.slice(3, 5), 16) / 255;
56
- const b = parseInt(hex.slice(5, 7), 16) / 255;
57
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
58
- let h = 0, s = 0;
59
- const l = (max + min) / 2;
60
- if (max !== min) {
61
- const d = max - min;
62
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
63
- if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
64
- else if (max === g) h = ((b - r) / d + 2) / 6;
65
- else h = ((r - g) / d + 4) / 6;
66
- }
67
- return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
68
- }
69
-
70
- function hslToHex(h: number, s: number, l: number): string {
71
- s /= 100; l /= 100;
72
- const a = s * Math.min(l, 1 - l);
73
- const f = (n: number) => {
74
- const k = (n + h / 30) % 12;
75
- const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
76
- return Math.round(255 * color).toString(16).padStart(2, '0');
77
- };
78
- return `#${f(0)}${f(8)}${f(4)}`;
79
- }
80
-
81
- // --- HSL mode reactives ---
82
-
83
- let hsl = $derived(hexToHsl(color));
84
-
85
- function hueGrad(s: number, l: number): string {
86
- return `linear-gradient(to right, ${
87
- [0, 60, 120, 180, 240, 300, 360].map(h => `hsl(${h},${s}%,${l}%)`).join(',')
88
- })`;
89
- }
90
-
91
- function satGrad(h: number, l: number): string {
92
- return `linear-gradient(to right, hsl(${h},0%,${l}%), hsl(${h},100%,${l}%))`;
93
- }
94
-
95
- function lightGrad(h: number, s: number): string {
96
- return `linear-gradient(to right, hsl(${h},${s}%,0%), hsl(${h},${s}%,50%), hsl(${h},${s}%,100%))`;
97
- }
98
-
99
- function updateHsl(component: 0 | 1 | 2, value: number) {
100
- const current = hexToHsl(color);
101
- current[component] = value;
102
- onColorChange(hslToHex(current[0], current[1], current[2]));
103
- }
104
49
 
105
- // --- Hue-chroma mode reactives ---
50
+ let lPct = $derived(lightness);
106
51
 
107
- let previewHex = $derived(mode === 'hue-chroma'
108
- ? (() => { const c = gamutClamp(PREVIEW_LIGHTNESS, chroma, hue); return oklchToHex(c.l, c.c, c.h); })()
109
- : color);
52
+ let previewHex = $derived((() => {
53
+ const c = gamutClamp(lPct / 100, chroma, hue);
54
+ return oklchToHex(c.l, c.c, c.h);
55
+ })());
110
56
 
111
57
  let hueGradient = $derived((() => {
112
- const _c = chroma;
113
- const displayChroma = Math.max(_c, CHROMA_MAX);
58
+ const _l = lPct / 100;
59
+ const displayChroma = Math.max(chroma, chromaMax);
114
60
  const stops = Array.from({ length: 13 }, (_, i) => {
115
61
  const h = (i / 12) * 360;
116
- const c = gamutClamp(PREVIEW_LIGHTNESS, displayChroma, h);
62
+ const c = gamutClamp(_l, displayChroma, h);
117
63
  return oklchToHex(c.l, c.c, c.h);
118
64
  });
119
65
  return `linear-gradient(to right, ${stops.join(',')})`;
120
66
  })());
121
67
 
122
68
  let chromaGradient = $derived((() => {
123
- const _h = hue;
69
+ const _h = hue, _l = lPct / 100;
124
70
  const stops = Array.from({ length: 8 }, (_, i) => {
125
- const c = (i / 7) * CHROMA_MAX;
126
- const clamped = gamutClamp(PREVIEW_LIGHTNESS, c, _h);
71
+ const c = (i / 7) * chromaMax;
72
+ const clamped = gamutClamp(_l, c, _h);
73
+ return oklchToHex(clamped.l, clamped.c, clamped.h);
74
+ });
75
+ return `linear-gradient(to right, ${stops.join(',')})`;
76
+ })());
77
+
78
+ let lightnessGradient = $derived((() => {
79
+ const _h = hue, _c = chroma;
80
+ const stops = Array.from({ length: 9 }, (_, i) => {
81
+ const clamped = gamutClamp(i / 8, _c, _h);
127
82
  return oklchToHex(clamped.l, clamped.c, clamped.h);
128
83
  });
129
84
  return `linear-gradient(to right, ${stops.join(',')})`;
@@ -137,12 +92,8 @@
137
92
  const dropper = new (window as any).EyeDropper();
138
93
  const result = await dropper.open();
139
94
  const hex = result.sRGBHex.toLowerCase();
140
- if (mode === 'hue-chroma') {
141
- const oklch = hexToOklch(hex);
142
- onHueChromaChange(Math.round(oklch.h), Math.round(oklch.c * 1000) / 1000);
143
- } else {
144
- onColorChange(hex);
145
- }
95
+ const oklch = hexToOklch(hex);
96
+ onHueChromaChange(Math.round(oklch.h), Math.round(oklch.c * 1000) / 1000, oklch.l * 100);
146
97
  } catch {
147
98
  // user cancelled the eyedropper
148
99
  }
@@ -162,12 +113,8 @@
162
113
  const v = hexDraft.startsWith('#') ? hexDraft : `#${hexDraft}`;
163
114
  if (/^#[0-9a-f]{6}$/i.test(v)) {
164
115
  const hex = v.toLowerCase();
165
- if (mode === 'hue-chroma') {
166
- const oklch = hexToOklch(hex);
167
- onHueChromaChange(Math.round(oklch.h), Math.round(oklch.c * 1000) / 1000);
168
- } else {
169
- onColorChange(hex);
170
- }
116
+ const oklch = hexToOklch(hex);
117
+ onHueChromaChange(Math.round(oklch.h), Math.round(oklch.c * 1000) / 1000, oklch.l * 100);
171
118
  }
172
119
  hexEditing = false;
173
120
  }
@@ -205,11 +152,7 @@
205
152
  {:else}
206
153
  <button class="hsl-hex" onclick={startHexEdit} title="Click to edit hex">{previewHex}</button>
207
154
  {/if}
208
- {#if mode === 'hue-chroma'}
209
- <code class="hsl-values">oklch({PREVIEW_LIGHTNESS}, {chroma.toFixed(3)}, {Math.round(hue)})</code>
210
- {:else}
211
- <code class="hsl-values">hsl({hsl[0]}, {hsl[1]}%, {hsl[2]}%)</code>
212
- {/if}
155
+ <code class="hsl-values">oklch({(lPct / 100).toFixed(2)}, {chroma.toFixed(3)}, {Math.round(hue)})</code>
213
156
  {@render actions?.()}
214
157
  <div class="hsl-panel-actions">
215
158
  {#if showRemoveOverride}
@@ -229,90 +172,58 @@
229
172
  </div>
230
173
  </div>
231
174
  <div class="hsl-sliders">
232
- {#if mode === 'hue-chroma'}
233
- <div class="hsl-slider-row">
234
- <span class="hsl-slider-label">H</span>
235
- <!-- svelte-ignore a11y_no_static_element_interactions -->
236
- <div class="slider-track" style="background: {hueGradient}" onpointerdown={onSliderStart}>
237
- <input type="range" min="0" max="360" value={hue}
238
- oninput={(e) => onHueChromaChange(+e.currentTarget.value, chroma)} />
239
- </div>
240
- <input
241
- class="hsl-slider-input"
242
- type="number"
243
- min="0"
244
- max="360"
245
- value={hue}
246
- onchange={(e) => onHueChromaChange(Math.min(360, Math.max(0, +e.currentTarget.value)), chroma)}
247
- /><span class="hsl-slider-unit">&deg;</span>
248
- </div>
249
- <div class="hsl-slider-row">
250
- <span class="hsl-slider-label">C</span>
251
- <!-- svelte-ignore a11y_no_static_element_interactions -->
252
- <div class="slider-track" style="background: {chromaGradient}" onpointerdown={onSliderStart}>
253
- <input type="range" min="0" max={CHROMA_MAX} step="0.001" value={chroma}
254
- oninput={(e) => onHueChromaChange(hue, +e.currentTarget.value)} />
255
- </div>
256
- <input
257
- class="hsl-slider-input chroma-input"
258
- type="number"
259
- min="0"
260
- max={CHROMA_MAX}
261
- step="0.001"
262
- value={chroma.toFixed(3)}
263
- onchange={(e) => onHueChromaChange(hue, Math.min(CHROMA_MAX, Math.max(0, +e.currentTarget.value)))}
264
- />
175
+ <div class="hsl-slider-row">
176
+ <span class="hsl-slider-label">H</span>
177
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
178
+ <div class="slider-track" style="background: {hueGradient}" onpointerdown={onSliderStart}>
179
+ <input type="range" min="0" max="360" value={hue}
180
+ oninput={(e) => onHueChromaChange(+e.currentTarget.value, chroma, lPct)} />
265
181
  </div>
266
- {:else}
267
- <div class="hsl-slider-row">
268
- <span class="hsl-slider-label">H</span>
269
- <!-- svelte-ignore a11y_no_static_element_interactions -->
270
- <div class="slider-track" style="background: {hueGrad(hsl[1], hsl[2])}" onpointerdown={onSliderStart}>
271
- <input type="range" min="0" max="360" value={hsl[0]}
272
- oninput={(e) => updateHsl(0, +e.currentTarget.value)} />
273
- </div>
274
- <input
275
- class="hsl-slider-input"
276
- type="number"
277
- min="0"
278
- max="360"
279
- value={hsl[0]}
280
- onchange={(e) => updateHsl(0, Math.min(360, Math.max(0, +e.currentTarget.value)))}
281
- /><span class="hsl-slider-unit">&deg;</span>
282
- </div>
283
- <div class="hsl-slider-row">
284
- <span class="hsl-slider-label">S</span>
285
- <!-- svelte-ignore a11y_no_static_element_interactions -->
286
- <div class="slider-track" style="background: {satGrad(hsl[0], hsl[2])}" onpointerdown={onSliderStart}>
287
- <input type="range" min="0" max="100" value={hsl[1]}
288
- oninput={(e) => updateHsl(1, +e.currentTarget.value)} />
289
- </div>
290
- <input
291
- class="hsl-slider-input"
292
- type="number"
293
- min="0"
294
- max="100"
295
- value={hsl[1]}
296
- onchange={(e) => updateHsl(1, Math.min(100, Math.max(0, +e.currentTarget.value)))}
297
- /><span class="hsl-slider-unit">%</span>
182
+ <input
183
+ class="hsl-slider-input"
184
+ type="number"
185
+ min="0"
186
+ max="360"
187
+ value={hue}
188
+ onchange={(e) => onHueChromaChange(Math.min(360, Math.max(0, +e.currentTarget.value)), chroma, lPct)}
189
+ /><span class="hsl-slider-unit">&deg;</span>
190
+ </div>
191
+ <div class="hsl-slider-row">
192
+ <span class="hsl-slider-label">C</span>
193
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
194
+ <div class="slider-track" style="background: {chromaGradient}" onpointerdown={onSliderStart}>
195
+ {#if chromaHint !== undefined && chromaHint < chromaMax}
196
+ <div class="chroma-hint" style="left: {(chromaHint / chromaMax) * 100}%" title="Typical neutral range"></div>
197
+ {/if}
198
+ <input type="range" min="0" max={chromaMax} step="0.001" value={chroma}
199
+ oninput={(e) => onHueChromaChange(hue, +e.currentTarget.value, lPct)} />
298
200
  </div>
299
- <div class="hsl-slider-row">
300
- <span class="hsl-slider-label">L</span>
301
- <!-- svelte-ignore a11y_no_static_element_interactions -->
302
- <div class="slider-track" style="background: {lightGrad(hsl[0], hsl[1])}" onpointerdown={onSliderStart}>
303
- <input type="range" min="0" max="100" value={hsl[2]}
304
- oninput={(e) => updateHsl(2, +e.currentTarget.value)} />
305
- </div>
306
- <input
307
- class="hsl-slider-input"
308
- type="number"
309
- min="0"
310
- max="100"
311
- value={hsl[2]}
312
- onchange={(e) => updateHsl(2, Math.min(100, Math.max(0, +e.currentTarget.value)))}
313
- /><span class="hsl-slider-unit">%</span>
201
+ <input
202
+ class="hsl-slider-input chroma-input"
203
+ type="number"
204
+ min="0"
205
+ max={chromaMax}
206
+ step="0.001"
207
+ value={chroma.toFixed(3)}
208
+ onchange={(e) => onHueChromaChange(hue, Math.min(chromaMax, Math.max(0, +e.currentTarget.value)), lPct)}
209
+ />
210
+ </div>
211
+ <div class="hsl-slider-row">
212
+ <span class="hsl-slider-label">L</span>
213
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
214
+ <div class="slider-track" style="background: {lightnessGradient}" onpointerdown={onSliderStart}>
215
+ <input type="range" min="0" max="100" value={lPct}
216
+ oninput={(e) => onHueChromaChange(hue, chroma, +e.currentTarget.value)} />
314
217
  </div>
315
- {/if}
218
+ <input
219
+ class="hsl-slider-input"
220
+ type="number"
221
+ min="0"
222
+ max="100"
223
+ value={Math.round(lPct)}
224
+ onchange={(e) => onHueChromaChange(hue, chroma, Math.min(100, Math.max(0, +e.currentTarget.value)))}
225
+ /><span class="hsl-slider-unit">%</span>
226
+ </div>
316
227
  </div>
317
228
  </div>
318
229
 
@@ -443,6 +354,16 @@
443
354
  min-width: 6rem;
444
355
  }
445
356
 
357
+ .chroma-hint {
358
+ position: absolute;
359
+ top: -2px;
360
+ bottom: -2px;
361
+ width: 1px;
362
+ background: var(--ui-text-secondary);
363
+ opacity: 0.5;
364
+ pointer-events: none;
365
+ }
366
+
446
367
  .slider-track input[type="range"] {
447
368
  -webkit-appearance: none;
448
369
  appearance: none;