@motion-proto/live-tokens 0.1.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 (68) hide show
  1. package/README.md +41 -0
  2. package/dist-plugin/index.cjs +444 -0
  3. package/dist-plugin/index.d.cts +12 -0
  4. package/dist-plugin/index.d.ts +12 -0
  5. package/dist-plugin/index.js +407 -0
  6. package/package.json +86 -0
  7. package/src/components/Badge.svelte +82 -0
  8. package/src/components/Button.svelte +333 -0
  9. package/src/components/Card.svelte +83 -0
  10. package/src/components/CollapsibleSection.svelte +82 -0
  11. package/src/components/DetailNav.svelte +78 -0
  12. package/src/components/Dialog.svelte +269 -0
  13. package/src/components/InlineEditActions.svelte +73 -0
  14. package/src/components/Notification.svelte +308 -0
  15. package/src/components/ProgressBar.svelte +99 -0
  16. package/src/components/RadioButton.svelte +87 -0
  17. package/src/components/SectionDivider.svelte +121 -0
  18. package/src/components/TabBar.svelte +92 -0
  19. package/src/components/Toggle.svelte +86 -0
  20. package/src/components/Tooltip.svelte +64 -0
  21. package/src/lib/ColumnsOverlay.svelte +120 -0
  22. package/src/lib/LiveEditorOverlay.svelte +467 -0
  23. package/src/lib/columnsOverlay.ts +26 -0
  24. package/src/lib/cssVarSync.ts +72 -0
  25. package/src/lib/editorConfig.ts +9 -0
  26. package/src/lib/editorConfigStore.ts +14 -0
  27. package/src/lib/index.ts +51 -0
  28. package/src/lib/oklch.ts +129 -0
  29. package/src/lib/pageSource.ts +6 -0
  30. package/src/lib/tokenInit.ts +29 -0
  31. package/src/lib/tokenService.ts +144 -0
  32. package/src/lib/tokenTypes.ts +45 -0
  33. package/src/pages/Admin.svelte +100 -0
  34. package/src/pages/ShowcasePage.svelte +146 -0
  35. package/src/showcase/BackupBrowser.svelte +617 -0
  36. package/src/showcase/BezierCurveEditor.svelte +648 -0
  37. package/src/showcase/ColorEditPanel.svelte +498 -0
  38. package/src/showcase/ComponentsTab.svelte +107 -0
  39. package/src/showcase/EditorDialog.svelte +137 -0
  40. package/src/showcase/PaletteEditor.svelte +2579 -0
  41. package/src/showcase/PaletteSelector.svelte +627 -0
  42. package/src/showcase/SurfacesTab.svelte +409 -0
  43. package/src/showcase/TextTab.svelte +205 -0
  44. package/src/showcase/TokenFileManager.svelte +683 -0
  45. package/src/showcase/TokenMap.svelte +54 -0
  46. package/src/showcase/VariablesTab.svelte +2657 -0
  47. package/src/showcase/VisualsTab.svelte +233 -0
  48. package/src/showcase/curveEngine.ts +190 -0
  49. package/src/showcase/demos/BadgeDemo.svelte +58 -0
  50. package/src/showcase/demos/CardDemo.svelte +52 -0
  51. package/src/showcase/demos/ChoiceButtonsDemo.svelte +194 -0
  52. package/src/showcase/demos/CollapsibleSectionDemo.svelte +56 -0
  53. package/src/showcase/demos/DialogDemo.svelte +42 -0
  54. package/src/showcase/demos/InlineEditActionsDemo.svelte +27 -0
  55. package/src/showcase/demos/NotificationDemo.svelte +149 -0
  56. package/src/showcase/demos/ProgressBarDemo.svelte +56 -0
  57. package/src/showcase/demos/RadioButtonDemo.svelte +58 -0
  58. package/src/showcase/demos/SectionDividerDemo.svelte +79 -0
  59. package/src/showcase/demos/StandardButtonsDemo.svelte +457 -0
  60. package/src/showcase/demos/TabBarDemo.svelte +60 -0
  61. package/src/showcase/demos/TooltipDemo.svelte +54 -0
  62. package/src/showcase/editor.css +93 -0
  63. package/src/showcase/index.ts +17 -0
  64. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  65. package/src/styles/fonts/Domine/OFL.txt +97 -0
  66. package/src/styles/fonts/Domine/README.txt +66 -0
  67. package/src/styles/fonts.css +18 -0
  68. package/src/styles/form-controls.css +190 -0
@@ -0,0 +1,648 @@
1
+ <script lang="ts">
2
+ import {
3
+ type CurveAnchor, type CurveConfig,
4
+ CURVE_H, CURVE_PAD_Y, CURVE_Y_PAD,
5
+ isCornerAnchor, curveXToSvg, curveYToSvg, svgToX, svgToY,
6
+ evalBezier, buildCurvePath, curveTemplates,
7
+ serializeCurve, deserializeCurve,
8
+ } from './curveEngine';
9
+
10
+ export let anchors: CurveAnchor[];
11
+ export let cfg: CurveConfig;
12
+ export let stepCount: number;
13
+ export let padX: number = 0;
14
+ export let offset: number = 0;
15
+ export let defaultAnchors: CurveAnchor[] | null = null;
16
+ export let lockedAnchorIndex: number | null = null;
17
+ export let onAnchorsChange: (anchors: CurveAnchor[]) => void = () => {};
18
+ export let onOffsetChange: (offset: number) => void = () => {};
19
+
20
+ function resetToDefault() {
21
+ if (!defaultAnchors) return;
22
+ onAnchorsChange(defaultAnchors.map(a => ({ ...a })));
23
+ onOffsetChange(0);
24
+ }
25
+
26
+ const CURVE_W_DEFAULT = 720;
27
+ let svgEl: SVGSVGElement | null = null;
28
+ let dims = CURVE_W_DEFAULT;
29
+ let shiftActive = false;
30
+
31
+ const clipId = `curve-clip-${Math.random().toString(36).slice(2, 8)}`;
32
+
33
+ $: w = dims;
34
+ $: offsetPx = -(offset / ((cfg.yMax - cfg.yMin) * (1 + 2 * CURVE_Y_PAD))) * (CURVE_H - 2 * CURVE_PAD_Y);
35
+
36
+ function stepToX(index: number): number {
37
+ return stepCount > 1 ? (index / (stepCount - 1)) * 100 : 50;
38
+ }
39
+
40
+ function dynamicViewBox(node: SVGSVGElement) {
41
+ const update = () => {
42
+ const cw = node.clientWidth;
43
+ const ch = node.clientHeight;
44
+ if (cw > 0 && ch > 0) {
45
+ const newW = CURVE_H * (cw / ch);
46
+ if (Math.abs(dims - newW) > 0.5) {
47
+ dims = newW;
48
+ }
49
+ }
50
+ };
51
+ const ro = new ResizeObserver(update);
52
+ ro.observe(node);
53
+ update();
54
+ return { destroy() { ro.disconnect(); } };
55
+ }
56
+
57
+ function svgCoords(e: MouseEvent | PointerEvent): { x: number; y: number } {
58
+ if (!svgEl) return { x: 0, y: 0 };
59
+ const rect = svgEl.getBoundingClientRect();
60
+ return {
61
+ x: (e.clientX - rect.left) * (w / rect.width),
62
+ y: (e.clientY - rect.top) * (CURVE_H / rect.height),
63
+ };
64
+ }
65
+
66
+ // --- Anchor drag ---
67
+
68
+ type DragTarget = {
69
+ kind: 'anchor' | 'handleIn' | 'handleOut';
70
+ index: number;
71
+ breakHandle: boolean;
72
+ };
73
+
74
+ let drag: DragTarget | null = null;
75
+
76
+ function handlePointerDown(e: PointerEvent, target: Omit<DragTarget, 'breakHandle'>) {
77
+ if (e.altKey && target.kind === 'anchor') {
78
+ if (target.index === lockedAnchorIndex) return; // can't delete locked anchor
79
+ e.stopPropagation();
80
+ removePoint(target.index);
81
+ return;
82
+ }
83
+ const isHandle = target.kind !== 'anchor';
84
+ const a = anchors[target.index];
85
+ const alreadyBroken = isHandle && a && (Math.abs(a.inDx + a.outDx) > 0.01 || Math.abs(a.inDy + a.outDy) > 0.01);
86
+ drag = { ...target, breakHandle: isHandle && (e.altKey || alreadyBroken) };
87
+ (e.currentTarget as SVGElement).setPointerCapture(e.pointerId);
88
+ e.stopPropagation();
89
+ e.preventDefault();
90
+ }
91
+
92
+ function handlePointerMove(e: PointerEvent) {
93
+ if (!drag) return;
94
+ const { kind, index: idx, breakHandle } = drag;
95
+ const { x, y } = svgCoords(e);
96
+ const newX = svgToX(x, w, padX);
97
+ const newY = Math.round(svgToY(y, cfg));
98
+ const updated = [...anchors];
99
+
100
+ if (kind === 'anchor') {
101
+ if (idx === lockedAnchorIndex) return; // locked: no x/y dragging
102
+ let ax = Math.round(newX);
103
+ if (idx === 0) ax = 0;
104
+ else if (idx === updated.length - 1) ax = 100;
105
+ else ax = Math.max(updated[idx - 1].x + 1, Math.min(updated[idx + 1].x - 1, ax));
106
+ updated[idx] = { ...updated[idx], x: ax, y: newY };
107
+ } else if (kind === 'handleOut') {
108
+ const a = updated[idx];
109
+ const dx = newX - a.x, dy = newY - a.y;
110
+ updated[idx] = breakHandle
111
+ ? { ...a, outDx: dx, outDy: dy }
112
+ : { ...a, outDx: dx, outDy: dy, inDx: -dx, inDy: -dy };
113
+ } else {
114
+ const a = updated[idx];
115
+ const dx = newX - a.x, dy = newY - a.y;
116
+ updated[idx] = breakHandle
117
+ ? { ...a, inDx: dx, inDy: dy }
118
+ : { ...a, inDx: dx, inDy: dy, outDx: -dx, outDy: -dy };
119
+ }
120
+ onAnchorsChange(updated);
121
+ }
122
+
123
+ function handlePointerUp() {
124
+ drag = null;
125
+ }
126
+
127
+ // --- Insert point ---
128
+
129
+ function insertPointOnPath(e: MouseEvent) {
130
+ e.stopPropagation();
131
+ const { x } = svgCoords(e);
132
+ const clickX = svgToX(x, w, padX);
133
+
134
+ if (anchors.some(p => Math.abs(p.x - clickX) < 4)) return;
135
+
136
+ let seg = 0;
137
+ for (let i = 0; i < anchors.length - 1; i++) {
138
+ if (clickX >= anchors[i].x && clickX <= anchors[i + 1].x) { seg = i; break; }
139
+ }
140
+
141
+ const a0 = anchors[seg], a1 = anchors[seg + 1];
142
+ const p0x = a0.x, p0y = a0.y;
143
+ const c0x = a0.x + a0.outDx, c0y = a0.y + a0.outDy;
144
+ const c1x = a1.x + a1.inDx, c1y = a1.y + a1.inDy;
145
+ const p1x = a1.x, p1y = a1.y;
146
+
147
+ let lo = 0, hi = 1;
148
+ for (let i = 0; i < 20; i++) {
149
+ const mid = (lo + hi) / 2;
150
+ if (evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, mid).x < clickX) lo = mid; else hi = mid;
151
+ }
152
+ const t = (lo + hi) / 2;
153
+
154
+ const q0x = p0x + t * (c0x - p0x), q0y = p0y + t * (c0y - p0y);
155
+ const q1x = c0x + t * (c1x - c0x), q1y = c0y + t * (c1y - c0y);
156
+ const q2x = c1x + t * (p1x - c1x), q2y = c1y + t * (p1y - c1y);
157
+ const r0x = q0x + t * (q1x - q0x), r0y = q0y + t * (q1y - q0y);
158
+ const r1x = q1x + t * (q2x - q1x), r1y = q1y + t * (q2y - q1y);
159
+ const mx = r0x + t * (r1x - r0x), my = r0y + t * (r1y - r0y);
160
+
161
+ const updated = [...anchors];
162
+ updated[seg] = { ...updated[seg], outDx: q0x - p0x, outDy: q0y - p0y };
163
+ updated[seg + 1] = { ...updated[seg + 1], inDx: q2x - p1x, inDy: q2y - p1y };
164
+ updated.splice(seg + 1, 0, {
165
+ x: Math.round(mx), y: Math.round(my),
166
+ inDx: r0x - mx, inDy: r0y - my, outDx: r1x - mx, outDy: r1y - my,
167
+ });
168
+ onAnchorsChange(updated);
169
+ }
170
+
171
+ function toggleAnchorSmooth(index: number) {
172
+ const a = anchors[index];
173
+ const updated = [...anchors];
174
+ if (isCornerAnchor(a)) {
175
+ updated[index] = { ...a, inDx: -15, inDy: 0, outDx: 15, outDy: 0 };
176
+ } else {
177
+ updated[index] = { ...a, inDx: 0, inDy: 0, outDx: 0, outDy: 0 };
178
+ }
179
+ onAnchorsChange(updated);
180
+ }
181
+
182
+ function removePoint(index: number) {
183
+ if (anchors.length <= 2) return;
184
+ onAnchorsChange(anchors.filter((_, i) => i !== index));
185
+ }
186
+
187
+ function applyTemplate(tpl: typeof curveTemplates[0]) {
188
+ onAnchorsChange(tpl.anchors(cfg));
189
+ }
190
+
191
+ // --- Shift (vertical offset) drag ---
192
+
193
+ let shiftDrag: { startClientY: number; startOffset: number; pxPerUnit: number } | null = null;
194
+
195
+ function handleShiftPointerDown(e: PointerEvent) {
196
+ if (!svgEl) return;
197
+ const rect = svgEl.getBoundingClientRect();
198
+ const pxPerUnit = rect.height / ((cfg.yMax - cfg.yMin) * (1 + 2 * CURVE_Y_PAD));
199
+ shiftDrag = {
200
+ startClientY: e.clientY,
201
+ startOffset: offset,
202
+ pxPerUnit,
203
+ };
204
+ (e.currentTarget as SVGElement).setPointerCapture(e.pointerId);
205
+ }
206
+
207
+ function handleShiftPointerMove(e: PointerEvent) {
208
+ if (!shiftDrag) return;
209
+ const deltaY = shiftDrag.startClientY - e.clientY;
210
+ const deltaValue = deltaY / shiftDrag.pxPerUnit;
211
+ onOffsetChange(Math.round(shiftDrag.startOffset + deltaValue));
212
+ }
213
+
214
+ function handleShiftPointerUp() {
215
+ shiftDrag = null;
216
+ }
217
+
218
+ // --- Clipboard (system clipboard for cross-editor paste) ---
219
+
220
+ async function copyToClipboard() {
221
+ try {
222
+ await navigator.clipboard.writeText(serializeCurve(anchors, offset));
223
+ } catch {
224
+ // clipboard write failed silently
225
+ }
226
+ }
227
+
228
+ async function pasteFromClipboard() {
229
+ try {
230
+ const text = await navigator.clipboard.readText();
231
+ const data = deserializeCurve(text);
232
+ if (!data) return;
233
+ onAnchorsChange(data.anchors.map(a => ({ ...a })));
234
+ onOffsetChange(data.offset);
235
+ } catch {
236
+ // clipboard read failed or permission denied
237
+ }
238
+ }
239
+ </script>
240
+
241
+ <div class="curve-panel">
242
+ <div class="curve-panel-header">
243
+ <span class="curve-panel-label">{cfg.label}</span>
244
+ </div>
245
+ <div class="curve-container" style="padding-inline: calc(50% / {stepCount})">
246
+ <svg
247
+ bind:this={svgEl}
248
+ class="curve-svg"
249
+ viewBox="0 0 {w} {CURVE_H}"
250
+ use:dynamicViewBox
251
+ >
252
+ <defs>
253
+ <clipPath id={clipId}>
254
+ <rect x="0" y="0" width={w} height={CURVE_H} />
255
+ </clipPath>
256
+ </defs>
257
+
258
+ <!-- Out-of-range shading -->
259
+ <rect x="0" y="0" width={w} height={curveYToSvg(cfg.yMax, cfg)} class="curve-out-of-range" />
260
+ <rect x="0" y={curveYToSvg(cfg.yMin, cfg)} width={w} height={CURVE_H - curveYToSvg(cfg.yMin, cfg)} class="curve-out-of-range" />
261
+
262
+ <!-- Step divider lines -->
263
+ {#each Array(stepCount) as _, si}
264
+ <line
265
+ x1={curveXToSvg(stepToX(si), w, padX)}
266
+ y1={CURVE_PAD_Y}
267
+ x2={curveXToSvg(stepToX(si), w, padX)}
268
+ y2={CURVE_H - CURVE_PAD_Y}
269
+ class="curve-step-line"
270
+ />
271
+ {/each}
272
+
273
+ <!-- Horizontal grid lines -->
274
+ {#each cfg.gridLines as gl}
275
+ <line x1={padX} y1={curveYToSvg(gl, cfg)} x2={w - padX} y2={curveYToSvg(gl, cfg)} class="curve-grid" />
276
+ {/each}
277
+ {#each cfg.dashedLines as dl}
278
+ <line x1={padX} y1={curveYToSvg(dl, cfg)} x2={w - padX} y2={curveYToSvg(dl, cfg)} class="curve-grid dashed" />
279
+ {/each}
280
+
281
+ <!-- Curve content group — offset vertically, clipped -->
282
+ <g transform="translate(0,{offsetPx})" clip-path="url(#{clipId})">
283
+ {#if shiftActive}
284
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
285
+ <rect
286
+ x="0" y={-CURVE_H} width={w} height={CURVE_H * 3}
287
+ class="shift-overlay"
288
+ on:pointerdown={handleShiftPointerDown}
289
+ on:pointermove={handleShiftPointerMove}
290
+ on:pointerup={handleShiftPointerUp}
291
+ />
292
+ {:else}
293
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
294
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
295
+ <path
296
+ d={buildCurvePath(anchors, cfg, w, padX)}
297
+ class="curve-hit"
298
+ on:click={insertPointOnPath}
299
+ />
300
+ {/if}
301
+
302
+ <!-- Visible curve path -->
303
+ <path d={buildCurvePath(anchors, cfg, w, padX)} class="curve-line" />
304
+
305
+ <!-- Bezier tangent handles -->
306
+ {#if !shiftActive}
307
+ {#each anchors as pt, i}
308
+ {#if i > 0 && !isCornerAnchor(pt)}
309
+ <line
310
+ x1={curveXToSvg(pt.x, w, padX)} y1={curveYToSvg(pt.y, cfg)}
311
+ x2={curveXToSvg(pt.x + pt.inDx, w, padX)} y2={curveYToSvg(pt.y + pt.inDy, cfg)}
312
+ class="handle-line"
313
+ />
314
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
315
+ <circle
316
+ cx={curveXToSvg(pt.x + pt.inDx, w, padX)} cy={curveYToSvg(pt.y + pt.inDy, cfg)}
317
+ r="3.5" class="handle-grip"
318
+ on:pointerdown={(e) => handlePointerDown(e, { kind: 'handleIn', index: i })}
319
+ on:pointermove={handlePointerMove}
320
+ on:pointerup={handlePointerUp}
321
+ />
322
+ {/if}
323
+ {#if i < anchors.length - 1 && !isCornerAnchor(pt)}
324
+ <line
325
+ x1={curveXToSvg(pt.x, w, padX)} y1={curveYToSvg(pt.y, cfg)}
326
+ x2={curveXToSvg(pt.x + pt.outDx, w, padX)} y2={curveYToSvg(pt.y + pt.outDy, cfg)}
327
+ class="handle-line"
328
+ />
329
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
330
+ <circle
331
+ cx={curveXToSvg(pt.x + pt.outDx, w, padX)} cy={curveYToSvg(pt.y + pt.outDy, cfg)}
332
+ r="3.5" class="handle-grip"
333
+ on:pointerdown={(e) => handlePointerDown(e, { kind: 'handleOut', index: i })}
334
+ on:pointermove={handlePointerMove}
335
+ on:pointerup={handlePointerUp}
336
+ />
337
+ {/if}
338
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
339
+ {#if i === lockedAnchorIndex}
340
+ <path
341
+ d="M{curveXToSvg(pt.x, w, padX)},{curveYToSvg(pt.y, cfg) - 6} l5,6 l-5,6 l-5,-6 Z"
342
+ class="curve-handle locked"
343
+ />
344
+ {:else if isCornerAnchor(pt)}
345
+ <rect
346
+ x={curveXToSvg(pt.x, w, padX) - 4} y={curveYToSvg(pt.y, cfg) - 4}
347
+ width="8" height="8"
348
+ class="curve-handle corner"
349
+ on:pointerdown={(e) => handlePointerDown(e, { kind: 'anchor', index: i })}
350
+ on:pointermove={handlePointerMove}
351
+ on:pointerup={handlePointerUp}
352
+ on:dblclick|stopPropagation={() => toggleAnchorSmooth(i)}
353
+ />
354
+ {:else}
355
+ <circle
356
+ cx={curveXToSvg(pt.x, w, padX)} cy={curveYToSvg(pt.y, cfg)}
357
+ r="5" class="curve-handle"
358
+ on:pointerdown={(e) => handlePointerDown(e, { kind: 'anchor', index: i })}
359
+ on:pointermove={handlePointerMove}
360
+ on:pointerup={handlePointerUp}
361
+ on:dblclick|stopPropagation={() => toggleAnchorSmooth(i)}
362
+ />
363
+ {/if}
364
+ {/each}
365
+ {/if}
366
+ </g>
367
+ </svg>
368
+ </div>
369
+ <div class="curve-toolbar">
370
+ <div class="curve-toolbar-left">
371
+ <button
372
+ class="curve-tool-btn"
373
+ class:active={shiftActive}
374
+ type="button"
375
+ title="Vertical offset"
376
+ on:click={() => shiftActive = !shiftActive}
377
+ >
378
+ <svg viewBox="0 0 12 20" class="curve-tool-icon">
379
+ <path d="M6,2 L10,7 L7,7 L7,13 L10,13 L6,18 L2,13 L5,13 L5,7 L2,7 Z" />
380
+ </svg>
381
+ <span>Offset{offset !== 0 ? ` ${offset > 0 ? '+' : ''}${offset}` : ''}</span>
382
+ </button>
383
+ <span class="curve-hint">&x2325;-click to remove point</span>
384
+ <button class="curve-tool-btn" type="button" title="Copy curve" on:click={copyToClipboard}>Copy</button>
385
+ <button class="curve-tool-btn" type="button" title="Paste curve" on:click={pasteFromClipboard}>Paste</button>
386
+ </div>
387
+ <div class="curve-templates">
388
+ {#each curveTemplates as tpl}
389
+ <button
390
+ class="curve-template-btn"
391
+ type="button"
392
+ title={tpl.name}
393
+ on:click={() => applyTemplate(tpl)}
394
+ >
395
+ <svg viewBox="0 0 20 12" class="curve-template-icon">
396
+ <path d={tpl.icon} />
397
+ </svg>
398
+ </button>
399
+ {/each}
400
+ {#if defaultAnchors}
401
+ <button
402
+ class="curve-tool-btn"
403
+ type="button"
404
+ title="Reset to default"
405
+ on:click={resetToDefault}
406
+ >Reset</button>
407
+ {/if}
408
+ </div>
409
+ </div>
410
+ </div>
411
+
412
+ <style>
413
+ .curve-panel {
414
+ display: flex;
415
+ flex-direction: column;
416
+ gap: var(--space-4);
417
+ }
418
+
419
+ .curve-panel-header {
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: space-between;
423
+ }
424
+
425
+ .curve-panel-label {
426
+ font-size: var(--font-md);
427
+ font-weight: var(--font-weight-semibold);
428
+ color: var(--ui-text-tertiary);
429
+ }
430
+
431
+ .curve-container {
432
+ width: 100%;
433
+ height: 250px;
434
+ box-sizing: border-box;
435
+ }
436
+
437
+ .curve-svg {
438
+ width: 100%;
439
+ height: 100%;
440
+ background: transparent;
441
+ border: 1px solid var(--ui-border-subtle);
442
+ border-radius: var(--radius-sm);
443
+ cursor: crosshair;
444
+ display: block;
445
+ }
446
+
447
+ .curve-out-of-range {
448
+ fill: white;
449
+ opacity: 0.04;
450
+ pointer-events: none;
451
+ }
452
+
453
+ .curve-grid {
454
+ stroke: var(--ui-border-faint);
455
+ stroke-width: 0.5;
456
+ vector-effect: non-scaling-stroke;
457
+ }
458
+
459
+ .curve-grid.dashed {
460
+ stroke-dasharray: 3 3;
461
+ }
462
+
463
+ .curve-step-line {
464
+ stroke: var(--ui-border-faint);
465
+ stroke-width: 0.5;
466
+ vector-effect: non-scaling-stroke;
467
+
468
+ }
469
+
470
+ .shift-overlay {
471
+ fill: transparent;
472
+ cursor: ns-resize;
473
+ touch-action: none;
474
+ }
475
+
476
+ .curve-hit {
477
+ fill: none;
478
+ stroke: transparent;
479
+ stroke-width: 12;
480
+ cursor: copy;
481
+ pointer-events: stroke;
482
+ vector-effect: non-scaling-stroke;
483
+ }
484
+
485
+ .curve-line {
486
+ fill: none;
487
+ stroke: var(--ui-text-secondary);
488
+ stroke-width: 1.5;
489
+ pointer-events: none;
490
+ vector-effect: non-scaling-stroke;
491
+ }
492
+
493
+ .handle-line {
494
+ stroke: var(--ui-text-muted);
495
+ stroke-width: 0.75;
496
+ pointer-events: none;
497
+ vector-effect: non-scaling-stroke;
498
+ }
499
+
500
+ .handle-grip {
501
+ fill: var(--ui-surface-high);
502
+ stroke: var(--ui-text-tertiary);
503
+ stroke-width: 1.5px;
504
+ paint-order: stroke fill;
505
+ cursor: grab;
506
+ touch-action: none;
507
+ vector-effect: non-scaling-stroke;
508
+ }
509
+
510
+ .handle-grip:hover {
511
+ fill: var(--ui-text-accent);
512
+ stroke: var(--ui-text-primary);
513
+ }
514
+
515
+ .handle-grip:active {
516
+ cursor: grabbing;
517
+ }
518
+
519
+ .curve-handle {
520
+ fill: var(--ui-surface-highest);
521
+ stroke: var(--ui-text-primary);
522
+ stroke-width: 2px;
523
+ paint-order: stroke fill;
524
+ cursor: grab;
525
+ touch-action: none;
526
+ vector-effect: non-scaling-stroke;
527
+ }
528
+
529
+ .curve-handle:hover {
530
+ fill: var(--ui-text-accent);
531
+ }
532
+
533
+ .curve-handle.corner {
534
+ rx: 1;
535
+ }
536
+
537
+ .curve-handle:active {
538
+ cursor: grabbing;
539
+ }
540
+
541
+ .curve-handle.locked {
542
+ fill: var(--ui-toggle);
543
+ stroke: none;
544
+ cursor: default;
545
+ }
546
+
547
+ .curve-toolbar {
548
+ display: flex;
549
+ align-items: center;
550
+ justify-content: space-between;
551
+ flex-wrap: wrap;
552
+ gap: var(--space-2);
553
+ padding-top: var(--space-2);
554
+ }
555
+
556
+ .curve-toolbar-left {
557
+ display: flex;
558
+ align-items: center;
559
+ gap: var(--space-4);
560
+ flex-wrap: wrap;
561
+ }
562
+
563
+ .curve-tool-btn {
564
+ display: flex;
565
+ align-items: center;
566
+ gap: var(--space-4);
567
+ padding: var(--space-2) var(--space-6);
568
+ border: 1px solid var(--ui-border-subtle);
569
+ border-radius: var(--radius-sm);
570
+ background: var(--ui-surface-lowest);
571
+ cursor: pointer;
572
+ color: var(--ui-text-muted);
573
+ font-size: var(--font-md);
574
+ }
575
+
576
+ .curve-tool-btn:hover {
577
+ border-color: var(--ui-border-medium);
578
+ color: var(--ui-text-secondary);
579
+ background: var(--ui-surface-high);
580
+ }
581
+
582
+ .curve-tool-btn.active {
583
+ border-color: var(--ui-border-medium);
584
+ background: var(--ui-surface-highest);
585
+ color: var(--ui-text-primary);
586
+ }
587
+
588
+ .curve-tool-btn:disabled {
589
+ opacity: 0.35;
590
+ cursor: default;
591
+ pointer-events: none;
592
+ }
593
+
594
+ .curve-tool-icon {
595
+ width: 0.625rem;
596
+ height: 1rem;
597
+ }
598
+
599
+ .curve-tool-icon path {
600
+ fill: currentColor;
601
+ }
602
+
603
+ .curve-hint {
604
+ font-size: var(--font-md);
605
+ color: var(--ui-text-muted);
606
+ opacity: 0.6;
607
+ }
608
+
609
+ .curve-templates {
610
+ display: flex;
611
+ gap: var(--space-2);
612
+ }
613
+
614
+ .curve-template-btn {
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: center;
618
+ width: 1.5rem;
619
+ height: 1rem;
620
+ padding: 0;
621
+ border: 1px solid var(--ui-border-subtle);
622
+ border-radius: var(--radius-sm);
623
+ background: var(--ui-surface-lowest);
624
+ cursor: pointer;
625
+ }
626
+
627
+ .curve-template-btn:hover {
628
+ border-color: var(--ui-border-medium);
629
+ background: var(--ui-surface-high);
630
+ }
631
+
632
+ .curve-template-icon {
633
+ width: 100%;
634
+ height: 100%;
635
+ }
636
+
637
+ .curve-template-icon path {
638
+ fill: none;
639
+ stroke: var(--ui-text-tertiary);
640
+ stroke-width: 1.5;
641
+ stroke-linecap: round;
642
+ stroke-linejoin: round;
643
+ }
644
+
645
+ .curve-template-btn:hover .curve-template-icon path {
646
+ stroke: var(--ui-text-primary);
647
+ }
648
+ </style>