@motion-proto/live-tokens 0.7.1 → 0.9.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 (96) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +34 -0
  3. package/dist-plugin/index.cjs +707 -90
  4. package/dist-plugin/index.d.cts +1 -0
  5. package/dist-plugin/index.d.ts +1 -0
  6. package/dist-plugin/index.js +707 -90
  7. package/package.json +6 -2
  8. package/src/app/site.css +1 -1
  9. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
  10. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  11. package/src/editor/component-editor/NotificationEditor.svelte +3 -1
  12. package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
  13. package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
  14. package/src/editor/component-editor/editors.d.ts +10 -0
  15. package/src/editor/component-editor/index.ts +16 -1
  16. package/src/editor/component-editor/registry.ts +103 -26
  17. package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
  18. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
  19. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  20. package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
  21. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  22. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
  23. package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
  24. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
  25. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
  26. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
  27. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
  28. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  29. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  30. package/src/editor/component-editor/scaffolding/types.ts +11 -0
  31. package/src/editor/core/components/componentConfigKeys.ts +22 -3
  32. package/src/editor/core/components/componentConfigService.ts +2 -2
  33. package/src/editor/core/components/componentPersist.ts +7 -5
  34. package/src/editor/core/manifests/manifestService.ts +58 -3
  35. package/src/editor/core/palettes/familySwap.ts +99 -0
  36. package/src/editor/core/palettes/paletteDerivation.ts +69 -0
  37. package/src/editor/core/palettes/tokenRegistry.ts +4 -1
  38. package/src/editor/core/store/editorStore.ts +206 -12
  39. package/src/editor/core/store/editorTypes.ts +55 -12
  40. package/src/editor/core/store/gradientSource.ts +192 -0
  41. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  42. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  43. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  44. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  45. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  46. package/src/editor/core/themes/migrations/index.ts +10 -0
  47. package/src/editor/core/themes/slices/components.ts +27 -4
  48. package/src/editor/core/themes/slices/gradients.ts +88 -13
  49. package/src/editor/core/themes/themeInit.ts +2 -2
  50. package/src/editor/core/themes/themeTypes.ts +56 -1
  51. package/src/editor/index.ts +10 -1
  52. package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
  53. package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
  54. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  55. package/src/editor/pages/EditorShell.svelte +53 -3
  56. package/src/editor/styles/ui-editor.css +1 -0
  57. package/src/editor/styles/ui-form-controls.css +19 -20
  58. package/src/editor/ui/BezierCurveEditor.svelte +114 -63
  59. package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
  60. package/src/editor/ui/FileLoadList.svelte +22 -5
  61. package/src/editor/ui/FontStackEditor.svelte +214 -76
  62. package/src/editor/ui/GradientEditor.svelte +435 -215
  63. package/src/editor/ui/GradientStopPicker.svelte +11 -3
  64. package/src/editor/ui/ManifestFileManager.svelte +71 -4
  65. package/src/editor/ui/PaletteEditor.svelte +52 -79
  66. package/src/editor/ui/ProjectFontsSection.svelte +328 -293
  67. package/src/editor/ui/ThemeFileManager.svelte +0 -4
  68. package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
  69. package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
  70. package/src/editor/ui/UIInfoPopover.svelte +0 -1
  71. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  72. package/src/editor/ui/UIPaletteSelector.svelte +31 -4
  73. package/src/editor/ui/UIPillButton.svelte +33 -3
  74. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  75. package/src/editor/ui/UITokenSelector.svelte +4 -1
  76. package/src/editor/ui/VariablesTab.svelte +41 -35
  77. package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
  78. package/src/editor/ui/palette/PaletteBase.svelte +3 -3
  79. package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
  80. package/src/editor/ui/sections/GradientsSection.svelte +1 -1
  81. package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
  82. package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
  83. package/src/system/components/Button.svelte +2 -2
  84. package/src/system/components/Card.svelte +29 -1
  85. package/src/system/components/CollapsibleSection.svelte +25 -2
  86. package/src/system/components/Dialog.svelte +24 -4
  87. package/src/system/components/FloatingTokenTags.css +43 -24
  88. package/src/system/components/FloatingTokenTags.svelte +88 -137
  89. package/src/system/components/Notification.svelte +8 -1
  90. package/src/system/components/SectionDivider.svelte +532 -381
  91. package/src/system/styles/CONVENTIONS.md +1 -1
  92. package/src/system/styles/fonts.css +3 -16
  93. package/src/system/styles/tokens.css +356 -1199
  94. package/src/system/styles/tokens.generated.css +544 -0
  95. package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
  96. package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
@@ -1,4 +1,4 @@
1
- import type { Theme } from './themeTypes';
1
+ import type { AliasDiskValue, Theme } from './themeTypes';
2
2
  import { activeFileName } from '../store/editorConfigStore';
3
3
  import { migrateThemeFonts } from '../fonts/fontMigration';
4
4
  import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
@@ -52,7 +52,7 @@ export async function initializeTheme(): Promise<void> {
52
52
  if (list && Array.isArray(list.components)) {
53
53
  const configs: Record<
54
54
  string,
55
- { activeFile: string; aliases: Record<string, string>; config?: Record<string, unknown>; schemaVersion?: number }
55
+ { activeFile: string; aliases: Record<string, AliasDiskValue>; config?: Record<string, unknown>; schemaVersion?: number }
56
56
  > = {};
57
57
  await Promise.all(
58
58
  list.components.map(async (c) => {
@@ -27,6 +27,19 @@ export interface PaletteConfig {
27
27
  gradientStops?: GradientStop[];
28
28
  gradientSize?: 'page' | 'window';
29
29
  anchorToBase?: boolean;
30
+ /**
31
+ * Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
32
+ * without owning the typed-state curves. The storage-layer reconciler uses
33
+ * it as an opt-in switch: snap `baseColor` (or `tintHue`+`tintChroma` for
34
+ * gray palettes) to the imported `--color-{ns}-500` anchor and clear the
35
+ * flag. Editor-authored themes never set this, so the reconciler is a
36
+ * strict no-op for them.
37
+ *
38
+ * Persists on disk for first-load reconciliation. After reconcile strips
39
+ * the palette-derived keys from `cssVariables`, subsequent reconciles find
40
+ * no anchor and become idempotent no-ops regardless of the flag's value.
41
+ */
42
+ _imported?: boolean;
30
43
  }
31
44
 
32
45
  export type FontSourceKind = 'google' | 'typekit' | 'css-url' | 'font-face';
@@ -92,12 +105,20 @@ export interface ThemeMeta {
92
105
  isActive: boolean;
93
106
  }
94
107
 
108
+ /** On-disk shape of a single alias entry. Plain strings carry the bulk of
109
+ * aliases (token refs like `--surface-canvas-low` or literal CSS like `4px`);
110
+ * the gradient object shape is the structured payload for component-owned
111
+ * gradients that can't compress to a single string. */
112
+ export type AliasDiskValue =
113
+ | string
114
+ | { kind: 'gradient'; value: { type: 'linear' | 'radial' | 'solid' | 'none'; angle: number; radius?: number; centerX?: number; aspectX?: number; aspectY?: number; stops: { position: number; color: string; opacity?: number }[] } };
115
+
95
116
  export interface ComponentConfig {
96
117
  name: string;
97
118
  component: string;
98
119
  createdAt: string;
99
120
  updatedAt: string;
100
- aliases: Record<string, string>;
121
+ aliases: Record<string, AliasDiskValue>;
101
122
  config?: Record<string, unknown>;
102
123
  /**
103
124
  * Server-attached file-name marker. Same role as `Theme._fileName`. Set by
@@ -139,6 +160,40 @@ export interface Manifest {
139
160
  _fileName?: string;
140
161
  }
141
162
 
163
+ /**
164
+ * Transport artifact for sharing a manifest with someone else. Self-contained:
165
+ * the bundle inlines the referenced theme and every non-default component
166
+ * config so the receiver doesn't need anything else on disk to apply it.
167
+ *
168
+ * Bundles are *not* stored under `manifests/` — they're transient downloads /
169
+ * uploads. Local manifests stay lightweight pointer files; bundles are the
170
+ * import/export envelope. See temp/manifest-robustness-plan.md §11.
171
+ *
172
+ * `componentConfigs` is keyed by `${component}/${configName}` so a single map
173
+ * carries multiple components. Entries whose manifest value is `"default"`
174
+ * are deliberately omitted — the receiver's local `default.json` is the
175
+ * live-tokens package's canonical default, and shipping the sender's default
176
+ * would risk version-divergence with no clean conflict story.
177
+ */
178
+ export interface ManifestBundle {
179
+ /** Discriminator for safe identification of bundle JSON files. */
180
+ kind: 'manifest-bundle';
181
+ /** Bumps when the bundle envelope shape changes. Start at 1. */
182
+ schemaVersion: 1;
183
+ /** Sender's `@motion-proto/live-tokens` package version. Receiver can
184
+ * compare to its own to warn about compatibility drift. */
185
+ liveTokensVersion: string;
186
+ /** ISO timestamp of when the bundle was exported. */
187
+ exportedAt: string;
188
+ /** Full pointer-form manifest (same shape as on-disk manifest files). */
189
+ manifest: Manifest;
190
+ /** Full content of the theme that `manifest.theme` references. */
191
+ theme: Theme;
192
+ /** Full content of each non-default component config referenced by
193
+ * `manifest.componentConfigs`, keyed by `${component}/${configName}`. */
194
+ componentConfigs: Record<string, ComponentConfig>;
195
+ }
196
+
142
197
  export interface ManifestMeta {
143
198
  name: string;
144
199
  fileName: string;
@@ -7,7 +7,13 @@ export { configureEditor, storageKey } from './core/store/editorConfig';
7
7
  export { activeFileName } from './core/store/editorConfigStore';
8
8
  export { init as initRouter, route, navigate } from './core/routing/router';
9
9
  export { init as initCssVarSync } from './core/cssVarSync';
10
- export { init as initEditorStore } from './core/store/editorStore';
10
+ export {
11
+ init as initEditorStore,
12
+ editorState,
13
+ setComponentAlias,
14
+ setComponentConfig,
15
+ registerComponentSchema,
16
+ } from './core/store/editorStore';
11
17
 
12
18
  export { setCssVar, removeCssVar } from './core/cssVarSync';
13
19
 
@@ -67,3 +73,6 @@ export { hexToOklch, oklchToHex, gamutClamp } from './core/palettes/oklch';
67
73
  export type { Oklch } from './core/palettes/oklch';
68
74
 
69
75
  export { initializeTheme } from './core/themes/themeInit';
76
+
77
+ export { registerComponent } from './component-editor/registry';
78
+ export type { RegisterComponentEntry, RegistryEntry, ComponentId } from './component-editor/registry';
@@ -118,7 +118,6 @@
118
118
  font-family: var(--ui-font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace);
119
119
  font-size: 10px;
120
120
  color: rgba(255, 255, 255, 0.75);
121
- letter-spacing: 0.02em;
122
121
  white-space: nowrap;
123
122
  }
124
123
 
@@ -422,7 +422,7 @@
422
422
 
423
423
  .lt-overlay.no-transition,
424
424
  .lt-overlay.no-transition .frame-wrap {
425
- transition: none !important;
425
+ transition: none;
426
426
  }
427
427
 
428
428
  .header {
@@ -451,7 +451,6 @@
451
451
  font-size: var(--ui-font-size-md, 16px);
452
452
  font-weight: var(--ui-font-weight-semibold, 600);
453
453
  color: rgba(255, 255, 255, 0.85);
454
- letter-spacing: 0.02em;
455
454
  }
456
455
 
457
456
  .spacer { flex: 1; }
@@ -460,7 +459,6 @@
460
459
  font-size: var(--ui-font-size-md, 16px);
461
460
  font-weight: var(--ui-font-weight-medium, 500);
462
461
  color: rgba(255, 255, 255, 0.4);
463
- letter-spacing: 0.02em;
464
462
  margin-left: var(--ui-space-2, 2px);
465
463
  user-select: none;
466
464
  }
@@ -523,7 +521,6 @@
523
521
  .seg-label {
524
522
  font-size: var(--ui-font-size-md, 16px);
525
523
  font-weight: var(--ui-font-weight-semibold, 600);
526
- letter-spacing: 0.02em;
527
524
  color: var(--ui-text-primary, #fff);
528
525
  }
529
526
 
@@ -5,7 +5,7 @@
5
5
  import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
6
6
  import ManifestFileManager from '../ui/ManifestFileManager.svelte';
7
7
  import { navigate } from '../core/routing/router';
8
- import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
8
+ import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
9
9
  import { listComponents } from '../core/components/componentConfigService';
10
10
  import { selectedComponent } from '../core/store/editorViewStore';
11
11
  import { componentDirty } from '../core/store/editorStore';
@@ -96,7 +96,9 @@
96
96
  window.removeEventListener('keydown', handleKeydown);
97
97
  });
98
98
 
99
- const componentNavItems = componentRegistryEntries.map(({ id, label, icon }) => ({ id, label, icon }));
99
+ const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
100
+ const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
101
+ const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
100
102
  </script>
101
103
 
102
104
  <!--
@@ -146,7 +148,7 @@
146
148
  {/if}
147
149
  </div>
148
150
  <div class="nav-items">
149
- {#each componentNavItems as item}
151
+ {#each systemNavItems as item}
150
152
  <button
151
153
  class="nav-item"
152
154
  class:active={$selectedComponent === item.id}
@@ -162,6 +164,27 @@
162
164
  {/if}
163
165
  </button>
164
166
  {/each}
167
+ {#if customNavItems.length > 0}
168
+ <div class="nav-divider">
169
+ <span class="nav-divider-label">Custom</span>
170
+ </div>
171
+ {#each customNavItems as item}
172
+ <button
173
+ class="nav-item"
174
+ class:active={$selectedComponent === item.id}
175
+ class:dirty={$componentDirty[item.id]}
176
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
177
+ onmouseleave={hideHint}
178
+ onclick={() => selectComponent(item.id)}
179
+ >
180
+ <i class={item.icon}></i>
181
+ <span class="rail-label">{item.label}</span>
182
+ {#if $componentDirty[item.id]}
183
+ <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
184
+ {/if}
185
+ </button>
186
+ {/each}
187
+ {/if}
165
188
  </div>
166
189
  {#if drawerOpen}
167
190
  <div class="sidebar-footer">
@@ -331,6 +354,33 @@
331
354
  background: black;
332
355
  }
333
356
 
357
+ /* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
358
+ dimmer border token (sub-element separator), with an uppercase eyebrow
359
+ label that fades out when the rail is collapsed so the line still reads. */
360
+ .nav-divider {
361
+ display: grid;
362
+ grid-template-columns: 48px 1fr;
363
+ align-items: center;
364
+ height: 28px;
365
+ margin-top: var(--ui-space-8);
366
+ border-top: 1px solid var(--ui-border-low);
367
+ }
368
+
369
+ .nav-divider-label {
370
+ grid-column: 2;
371
+ font-size: var(--ui-font-size-xs);
372
+ font-weight: var(--ui-font-weight-semibold);
373
+ color: var(--ui-text-tertiary);
374
+ text-transform: uppercase;
375
+ letter-spacing: 0.04em;
376
+ opacity: 0;
377
+ transition: opacity 180ms ease;
378
+ }
379
+
380
+ .components-shell.rail-expanded .nav-divider-label {
381
+ opacity: 1;
382
+ }
383
+
334
384
  .sidebar-footer {
335
385
  flex-shrink: 0;
336
386
  margin-top: auto;
@@ -13,7 +13,7 @@
13
13
  import { editorState } from '../core/store/editorStore';
14
14
  import { editorView, sidebarCondensed, selectedComponent } from '../core/store/editorViewStore';
15
15
  import { componentDirty } from '../core/store/editorStore';
16
- import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
16
+ import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
17
17
  import { listComponents } from '../core/components/componentConfigService';
18
18
 
19
19
  const tokenNavItems = [
@@ -29,7 +29,9 @@
29
29
  { id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
30
30
  ];
31
31
 
32
- const componentNavItems = componentRegistryEntries.map(({ id, label, icon }) => ({ id, label, icon }));
32
+ const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
33
+ const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
34
+ const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
33
35
 
34
36
  let selectedTokenSection: string | null = $state(null);
35
37
  let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
@@ -149,7 +151,7 @@
149
151
  {/if}
150
152
  {:else}
151
153
  <div class="nav-items">
152
- {#each componentNavItems as item}
154
+ {#each systemNavItems as item}
153
155
  <button
154
156
  class="nav-item"
155
157
  class:active={$selectedComponent === item.id}
@@ -165,6 +167,27 @@
165
167
  {/if}
166
168
  </button>
167
169
  {/each}
170
+ {#if customNavItems.length > 0}
171
+ <div class="nav-divider">
172
+ <span class="nav-divider-label">Custom</span>
173
+ </div>
174
+ {#each customNavItems as item}
175
+ <button
176
+ class="nav-item"
177
+ class:active={$selectedComponent === item.id}
178
+ class:dirty={$componentDirty[item.id]}
179
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
180
+ onmouseleave={hideHint}
181
+ onclick={() => selectComponent(item.id)}
182
+ >
183
+ <i class={item.icon}></i>
184
+ <span class="nav-label">{item.label}</span>
185
+ {#if $componentDirty[item.id]}
186
+ <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
187
+ {/if}
188
+ </button>
189
+ {/each}
190
+ {/if}
168
191
  </div>
169
192
  {#if !condensed}
170
193
  <div class="sidebar-footer">
@@ -248,6 +271,33 @@
248
271
  flex-shrink: 0;
249
272
  }
250
273
 
274
+ /* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
275
+ dimmer border token (sub-element separator), with an uppercase eyebrow
276
+ label that fades out when the rail is condensed so the line still reads. */
277
+ .nav-divider {
278
+ display: grid;
279
+ grid-template-columns: 48px 1fr;
280
+ align-items: center;
281
+ height: 28px;
282
+ margin-top: var(--ui-space-8);
283
+ border-top: 1px solid var(--ui-border-low);
284
+ }
285
+
286
+ .nav-divider-label {
287
+ grid-column: 2;
288
+ font-size: var(--ui-font-size-xs);
289
+ font-weight: var(--ui-font-weight-semibold);
290
+ color: var(--ui-text-tertiary);
291
+ text-transform: uppercase;
292
+ letter-spacing: 0.04em;
293
+ opacity: 1;
294
+ transition: opacity 180ms ease;
295
+ }
296
+
297
+ .layout.condensed .nav-divider-label {
298
+ opacity: 0;
299
+ }
300
+
251
301
  .nav-item {
252
302
  position: relative;
253
303
  display: grid;
@@ -96,6 +96,7 @@
96
96
  --ui-font-size-3xl: 1.75rem; /* 28px */
97
97
  --ui-font-size-4xl: 2.25rem; /* 36px */
98
98
 
99
+ --ui-font-weight-light: 200;
99
100
  --ui-font-weight-normal: 400;
100
101
  --ui-font-weight-medium: 500;
101
102
  --ui-font-weight-semibold: 600;
@@ -59,11 +59,12 @@
59
59
  .ui-form-select {
60
60
  /* No vertical padding - let min-height and line-height center text naturally */
61
61
  padding: 0 var(--ui-space-16);
62
+ padding-right: var(--ui-space-32);
62
63
  min-height: 2.375rem; /* ~38px to match button height */
63
- background: var(--ui-surface-lowest) !important;
64
- border: 1px solid var(--ui-border) !important;
64
+ background-color: var(--ui-surface-lowest);
65
+ border: 1px solid var(--ui-border);
65
66
  border-radius: var(--ui-radius-md);
66
- color: var(--ui-text-primary) !important;
67
+ color: var(--ui-text-primary);
67
68
  font-family: var(--ui-font-sans);
68
69
  font-size: var(--ui-font-size-md);
69
70
  font-weight: var(--ui-font-weight-normal);
@@ -71,28 +72,26 @@
71
72
  vertical-align: middle;
72
73
  cursor: pointer;
73
74
  transition: all var(--ui-transition-fast);
74
- /* Prevent clipping */
75
- overflow: visible !important;
76
- box-sizing: border-box !important;
75
+ overflow: visible;
76
+ box-sizing: border-box;
77
77
  /* Reset browser defaults */
78
78
  -webkit-appearance: none;
79
79
  -moz-appearance: none;
80
80
  appearance: none;
81
81
  /* Custom dropdown arrow */
82
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E") !important;
83
- background-repeat: no-repeat !important;
84
- background-position: right var(--ui-space-12) center !important;
85
- padding-right: var(--ui-space-32) !important;
82
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
83
+ background-repeat: no-repeat;
84
+ background-position: right var(--ui-space-12) center;
86
85
  }
87
86
 
88
87
  .ui-form-select:hover:not(:disabled) {
89
- background-color: var(--ui-surface-low) !important;
90
- border-color: var(--ui-border-high) !important;
88
+ background-color: var(--ui-surface-low);
89
+ border-color: var(--ui-border-high);
91
90
  }
92
91
 
93
92
  .ui-form-select:focus {
94
93
  outline: none;
95
- border-color: var(--ui-border-higher) !important;
94
+ border-color: var(--ui-border-higher);
96
95
  box-shadow: 0 0 0 2px hsla(0, 58%, 50%, 0.2);
97
96
  }
98
97
 
@@ -102,13 +101,13 @@
102
101
  }
103
102
 
104
103
  .ui-form-select:active:not(:disabled) {
105
- background-color: var(--ui-surface) !important;
104
+ background-color: var(--ui-surface);
106
105
  }
107
106
 
108
107
  .ui-form-select:disabled {
109
- background-color: var(--ui-surface-lowest) !important;
110
- border-color: var(--ui-border-low) !important;
111
- color: var(--ui-text-disabled) !important;
108
+ background-color: var(--ui-surface-lowest);
109
+ border-color: var(--ui-border-low);
110
+ color: var(--ui-text-disabled);
112
111
  cursor: not-allowed;
113
112
  }
114
113
 
@@ -116,8 +115,8 @@
116
115
  /* Note: Most option styling is controlled by browser/OS and cannot be fully overridden */
117
116
  /* These styles apply where browsers allow (limited support in Chrome/Firefox) */
118
117
  .ui-form-select option {
119
- background-color: var(--ui-surface-lowest) !important;
120
- color: var(--ui-text-primary) !important;
118
+ background-color: var(--ui-surface-lowest);
119
+ color: var(--ui-text-primary);
121
120
  padding: var(--ui-space-8) var(--ui-space-12);
122
121
  min-height: 2rem;
123
122
  font-size: var(--ui-font-size-md);
@@ -127,7 +126,7 @@
127
126
 
128
127
  /* Disabled options */
129
128
  .ui-form-select option:disabled {
130
- color: var(--ui-text-disabled) !important;
129
+ color: var(--ui-text-disabled);
131
130
  }
132
131
 
133
132
  /* Input Field Styling */
@@ -8,6 +8,7 @@
8
8
  evalBezier, buildCurvePath, curveTemplates,
9
9
  serializeCurve, deserializeCurve,
10
10
  } from './curveEngine';
11
+ import UIPillButton from './UIPillButton.svelte';
11
12
 
12
13
  interface Props {
13
14
  anchors: CurveAnchor[];
@@ -257,8 +258,31 @@
257
258
  <div class="curve-panel">
258
259
  <div class="curve-panel-header">
259
260
  <span class="curve-panel-label">{cfg.label}</span>
261
+ <div class="curve-help">
262
+ <button class="curve-help-badge" type="button" aria-label="Curve editor help">
263
+ <i class="fas fa-circle-info" aria-hidden="true"></i>
264
+ </button>
265
+ <div class="curve-help-popover" role="tooltip">
266
+ <div><strong>Click</strong> path to add a point</div>
267
+ <div><strong>&#x2325; Click</strong> a point to remove</div>
268
+ <div><strong>Double-click</strong> a point to toggle smooth/corner</div>
269
+ </div>
270
+ </div>
260
271
  </div>
261
- <div class="curve-container" style="padding-inline: calc(50% / {stepCount})">
272
+ <div class="curve-container">
273
+ <div class="curve-chart-overlay">
274
+ <UIPillButton
275
+ size="compact"
276
+ variant={shiftActive ? 'default' : 'outline'}
277
+ title="Vertical offset"
278
+ onclick={() => shiftActive = !shiftActive}
279
+ >
280
+ <svg viewBox="0 0 12 20" class="curve-tool-icon">
281
+ <path d="M6,2 L10,7 L7,7 L7,13 L10,13 L6,18 L2,13 L5,13 L5,7 L2,7 Z" />
282
+ </svg>
283
+ <span>Offset{offset !== 0 ? ` ${offset > 0 ? '+' : ''}${offset}` : ''}</span>
284
+ </UIPillButton>
285
+ </div>
262
286
  <svg
263
287
  bind:this={svgEl}
264
288
  class="curve-svg"
@@ -383,22 +407,9 @@
383
407
  </svg>
384
408
  </div>
385
409
  <div class="curve-toolbar">
386
- <div class="curve-toolbar-left">
387
- <button
388
- class="curve-tool-btn"
389
- class:active={shiftActive}
390
- type="button"
391
- title="Vertical offset"
392
- onclick={() => shiftActive = !shiftActive}
393
- >
394
- <svg viewBox="0 0 12 20" class="curve-tool-icon">
395
- <path d="M6,2 L10,7 L7,7 L7,13 L10,13 L6,18 L2,13 L5,13 L5,7 L2,7 Z" />
396
- </svg>
397
- <span>Offset{offset !== 0 ? ` ${offset > 0 ? '+' : ''}${offset}` : ''}</span>
398
- </button>
399
- <span class="curve-hint">&x2325;-click to remove point</span>
400
- <button class="curve-tool-btn" type="button" title="Copy curve" onclick={copyToClipboard}>Copy</button>
401
- <button class="curve-tool-btn" type="button" title="Paste curve" onclick={pasteFromClipboard}>Paste</button>
410
+ <div class="curve-toolbar-group">
411
+ <UIPillButton size="compact" variant="outline" title="Copy curve" onclick={copyToClipboard}>Copy</UIPillButton>
412
+ <UIPillButton size="compact" variant="outline" title="Paste curve" onclick={pasteFromClipboard}>Paste</UIPillButton>
402
413
  </div>
403
414
  <div class="curve-templates">
404
415
  {#each curveTemplates as tpl}
@@ -413,13 +424,12 @@
413
424
  </svg>
414
425
  </button>
415
426
  {/each}
427
+ </div>
428
+ <div class="curve-toolbar-group">
416
429
  {#if defaultAnchors}
417
- <button
418
- class="curve-tool-btn"
419
- type="button"
420
- title="Reset to default"
421
- onclick={resetToDefault}
422
- >Reset</button>
430
+ <UIPillButton size="compact" variant="outline" title="Reset to default" onclick={resetToDefault}>
431
+ Reset
432
+ </UIPillButton>
423
433
  {/if}
424
434
  </div>
425
435
  </div>
@@ -435,7 +445,7 @@
435
445
  .curve-panel-header {
436
446
  display: flex;
437
447
  align-items: center;
438
- justify-content: space-between;
448
+ gap: var(--ui-space-6);
439
449
  }
440
450
 
441
451
  .curve-panel-label {
@@ -445,11 +455,89 @@
445
455
  }
446
456
 
447
457
  .curve-container {
458
+ position: relative;
448
459
  width: 100%;
449
460
  height: 250px;
450
461
  box-sizing: border-box;
451
462
  }
452
463
 
464
+ .curve-chart-overlay {
465
+ position: absolute;
466
+ inset: var(--ui-space-8);
467
+ display: flex;
468
+ align-items: flex-end;
469
+ pointer-events: none;
470
+ }
471
+
472
+ .curve-chart-overlay > :global(*) {
473
+ pointer-events: auto;
474
+ }
475
+
476
+ .curve-help {
477
+ position: relative;
478
+ margin-left: auto;
479
+ }
480
+
481
+ .curve-help-badge {
482
+ display: inline-flex;
483
+ align-items: center;
484
+ justify-content: center;
485
+ width: 1.25rem;
486
+ height: 1.25rem;
487
+ padding: 0;
488
+ border: none;
489
+ background: transparent;
490
+ color: var(--ui-text-muted);
491
+ cursor: help;
492
+ border-radius: var(--ui-radius-full);
493
+ transition: color var(--ui-transition-fast, 120ms ease);
494
+ }
495
+
496
+ .curve-help-badge:hover,
497
+ .curve-help-badge:focus-visible {
498
+ color: var(--ui-text-primary);
499
+ outline: none;
500
+ }
501
+
502
+ .curve-help-badge i {
503
+ font-size: var(--ui-font-size-md);
504
+ }
505
+
506
+ .curve-help-popover {
507
+ position: absolute;
508
+ top: calc(100% + var(--ui-space-4));
509
+ right: 0;
510
+ display: grid;
511
+ gap: var(--ui-space-4);
512
+ min-width: 14rem;
513
+ padding: var(--ui-space-8) var(--ui-space-12);
514
+ background: var(--ui-surface-highest);
515
+ border: 1px solid var(--ui-border-low);
516
+ border-radius: var(--ui-radius-sm);
517
+ color: var(--ui-text-secondary);
518
+ font-size: var(--ui-font-size-sm);
519
+ line-height: 1.4;
520
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
521
+ opacity: 0;
522
+ transform: translateY(-2px);
523
+ pointer-events: none;
524
+ transition:
525
+ opacity var(--ui-transition-fast, 120ms ease),
526
+ transform var(--ui-transition-fast, 120ms ease);
527
+ z-index: 2;
528
+ }
529
+
530
+ .curve-help-popover strong {
531
+ color: var(--ui-text-primary);
532
+ font-weight: var(--ui-font-weight-medium);
533
+ }
534
+
535
+ .curve-help:hover .curve-help-popover,
536
+ .curve-help:focus-within .curve-help-popover {
537
+ opacity: 1;
538
+ transform: translateY(0);
539
+ }
540
+
453
541
  .curve-svg {
454
542
  width: 100%;
455
543
  height: 100%;
@@ -565,48 +653,17 @@
565
653
  align-items: center;
566
654
  justify-content: space-between;
567
655
  flex-wrap: wrap;
568
- gap: var(--ui-space-2);
656
+ gap: var(--ui-space-8);
569
657
  padding-top: var(--ui-space-2);
570
658
  }
571
659
 
572
- .curve-toolbar-left {
660
+ .curve-toolbar-group {
573
661
  display: flex;
574
662
  align-items: center;
575
663
  gap: var(--ui-space-4);
576
664
  flex-wrap: wrap;
577
665
  }
578
666
 
579
- .curve-tool-btn {
580
- display: flex;
581
- align-items: center;
582
- gap: var(--ui-space-4);
583
- padding: var(--ui-space-2) var(--ui-space-6);
584
- border: 1px solid var(--ui-border-low);
585
- border-radius: var(--ui-radius-sm);
586
- background: var(--ui-surface-lowest);
587
- cursor: pointer;
588
- color: var(--ui-text-muted);
589
- font-size: var(--ui-font-size-md);
590
- }
591
-
592
- .curve-tool-btn:hover {
593
- border-color: var(--ui-border-high);
594
- color: var(--ui-text-secondary);
595
- background: var(--ui-surface-high);
596
- }
597
-
598
- .curve-tool-btn.active {
599
- border-color: var(--ui-border-high);
600
- background: var(--ui-surface-highest);
601
- color: var(--ui-text-primary);
602
- }
603
-
604
- .curve-tool-btn:disabled {
605
- opacity: 0.35;
606
- cursor: default;
607
- pointer-events: none;
608
- }
609
-
610
667
  .curve-tool-icon {
611
668
  width: 0.625rem;
612
669
  height: 1rem;
@@ -616,12 +673,6 @@
616
673
  fill: currentColor;
617
674
  }
618
675
 
619
- .curve-hint {
620
- font-size: var(--ui-font-size-md);
621
- color: var(--ui-text-muted);
622
- opacity: 0.6;
623
- }
624
-
625
676
  .curve-templates {
626
677
  display: flex;
627
678
  gap: var(--ui-space-2);