@motion-proto/live-tokens 0.1.1 → 0.3.2

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 (224) hide show
  1. package/README.md +168 -21
  2. package/dist-plugin/index.cjs +823 -336
  3. package/dist-plugin/index.d.cts +9 -7
  4. package/dist-plugin/index.d.ts +9 -7
  5. package/dist-plugin/index.js +822 -335
  6. package/package.json +46 -20
  7. package/src/assets/newspaper.webp +0 -0
  8. package/src/assets/offering.webp +0 -0
  9. package/src/component-editor/BadgeEditor.svelte +170 -0
  10. package/src/component-editor/CalloutEditor.svelte +103 -0
  11. package/src/component-editor/CardEditor.svelte +184 -0
  12. package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
  13. package/src/component-editor/CornerBadgeEditor.svelte +207 -0
  14. package/src/component-editor/DialogEditor.svelte +172 -0
  15. package/src/component-editor/ImageEditor.svelte +72 -0
  16. package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
  17. package/src/component-editor/NotificationEditor.svelte +160 -0
  18. package/src/component-editor/ProgressBarEditor.svelte +124 -0
  19. package/src/component-editor/RadioButtonEditor.svelte +140 -0
  20. package/src/component-editor/SectionDividerEditor.svelte +263 -0
  21. package/src/component-editor/SegmentedControlEditor.svelte +154 -0
  22. package/src/component-editor/StandardButtonsEditor.svelte +178 -0
  23. package/src/component-editor/TabBarEditor.svelte +137 -0
  24. package/src/component-editor/TableEditor.svelte +128 -0
  25. package/src/component-editor/TooltipEditor.svelte +122 -0
  26. package/src/component-editor/editorTokens.test.ts +93 -0
  27. package/src/component-editor/groupKeySlots.test.ts +67 -0
  28. package/src/component-editor/groupKeySnapshot.test.ts +52 -0
  29. package/src/component-editor/index.ts +5 -0
  30. package/src/component-editor/registry.ts +246 -0
  31. package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
  32. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
  33. package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
  34. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
  35. package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
  36. package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
  37. package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
  38. package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
  39. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
  40. package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
  41. package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
  42. package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
  43. package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
  44. package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
  45. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
  46. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
  47. package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
  48. package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
  49. package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
  50. package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
  51. package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
  52. package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
  53. package/src/component-editor/scaffolding/componentSources.ts +9 -0
  54. package/src/component-editor/scaffolding/defaultSections.ts +16 -0
  55. package/src/component-editor/scaffolding/editorContext.ts +44 -0
  56. package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
  57. package/src/component-editor/scaffolding/siblings.ts +33 -0
  58. package/src/component-editor/scaffolding/types.ts +39 -0
  59. package/src/components/Badge.svelte +231 -42
  60. package/src/components/Button.svelte +324 -124
  61. package/src/components/Callout.svelte +145 -0
  62. package/src/components/Card.svelte +123 -25
  63. package/src/components/CollapsibleSection.svelte +213 -35
  64. package/src/components/CornerBadge.svelte +224 -0
  65. package/src/components/Dialog.svelte +137 -114
  66. package/src/components/Image.svelte +43 -0
  67. package/src/components/InlineEditActions.svelte +74 -14
  68. package/src/components/Notification.svelte +184 -163
  69. package/src/components/ProgressBar.svelte +216 -22
  70. package/src/components/RadioButton.svelte +110 -40
  71. package/src/components/SectionDivider.svelte +428 -74
  72. package/src/components/SegmentedControl.svelte +203 -0
  73. package/src/components/TabBar.svelte +146 -21
  74. package/src/components/Table.svelte +102 -0
  75. package/src/components/Tooltip.svelte +45 -19
  76. package/src/components/types.ts +51 -0
  77. package/src/data/google-fonts.json +75 -0
  78. package/src/lib/ColumnsOverlay.svelte +20 -7
  79. package/src/lib/LiveEditorOverlay.svelte +257 -78
  80. package/src/lib/columnsOverlay.ts +21 -17
  81. package/src/lib/componentConfig.test.ts +204 -0
  82. package/src/lib/componentConfigKeys.ts +19 -0
  83. package/src/lib/componentConfigService.ts +88 -0
  84. package/src/lib/copyPopover.ts +30 -0
  85. package/src/lib/cssVarSync.ts +59 -7
  86. package/src/lib/editorConfigStore.ts +0 -10
  87. package/src/lib/editorCore.ts +402 -0
  88. package/src/lib/editorKeybindings.ts +52 -0
  89. package/src/lib/editorPersistence.ts +106 -0
  90. package/src/lib/editorRenderer.ts +74 -0
  91. package/src/lib/editorStore.test.ts +328 -0
  92. package/src/lib/editorStore.ts +412 -0
  93. package/src/lib/editorTypes.ts +100 -0
  94. package/src/lib/editorViewStore.ts +55 -0
  95. package/src/lib/files/versionedFileResource.ts +140 -0
  96. package/src/lib/fontLoader.ts +130 -0
  97. package/src/lib/fontMigration.ts +140 -0
  98. package/src/lib/fontParse.ts +168 -0
  99. package/src/lib/index.ts +48 -30
  100. package/src/lib/lazyConfig.test.ts +54 -0
  101. package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
  102. package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
  103. package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
  104. package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
  105. package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
  106. package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
  107. package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
  108. package/src/lib/migrations/index.ts +93 -0
  109. package/src/lib/migrations/migrations.test.ts +341 -0
  110. package/src/lib/navLinkTypes.ts +1 -0
  111. package/src/lib/overlayState.ts +3 -0
  112. package/src/lib/paletteDerivation.ts +300 -0
  113. package/src/lib/parentRouteStore.ts +42 -0
  114. package/src/lib/parsers/globalRootBlock.ts +32 -0
  115. package/src/lib/presetService.ts +94 -0
  116. package/src/lib/router.ts +42 -10
  117. package/src/lib/scrollSection.ts +45 -0
  118. package/src/lib/slices/columns.ts +59 -0
  119. package/src/lib/slices/components.ts +362 -0
  120. package/src/lib/slices/domainVars.ts +15 -0
  121. package/src/lib/slices/fonts.ts +30 -0
  122. package/src/lib/slices/gradients.ts +153 -0
  123. package/src/lib/slices/overlays.ts +132 -0
  124. package/src/lib/slices/palettes.ts +26 -0
  125. package/src/lib/slices/shadows.ts +123 -0
  126. package/src/lib/storage.ts +88 -0
  127. package/src/lib/themeInit.ts +74 -0
  128. package/src/lib/themeService.ts +101 -0
  129. package/src/lib/themeTypes.ts +146 -0
  130. package/src/lib/tokenRegistry.ts +148 -0
  131. package/src/pages/ComponentEditorPage.svelte +384 -0
  132. package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
  133. package/src/pages/Editor.svelte +98 -0
  134. package/src/pages/Editor.svelte.d.ts +2 -0
  135. package/src/pages/EditorShell.svelte +348 -0
  136. package/src/styles/_padding.scss +34 -0
  137. package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  138. package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  139. package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  140. package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  141. package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  142. package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
  143. package/src/styles/fonts.css +22 -10
  144. package/src/styles/form-controls.css +14 -16
  145. package/src/styles/tokens.css +1322 -0
  146. package/src/styles/ui-editor.css +126 -0
  147. package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
  148. package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
  149. package/src/ui/EditorViewSwitcher.svelte +180 -0
  150. package/src/ui/FontStackEditor.svelte +360 -0
  151. package/src/ui/GradientEditor.svelte +461 -0
  152. package/src/ui/GradientStopPicker.svelte +74 -0
  153. package/src/ui/PaletteEditor.svelte +1590 -0
  154. package/src/ui/PaletteEditor.test.ts +108 -0
  155. package/src/ui/PresetFileManager.svelte +567 -0
  156. package/src/ui/ProjectFontsSection.svelte +645 -0
  157. package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
  158. package/src/{showcase → ui}/TextTab.svelte +27 -27
  159. package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
  160. package/src/ui/Toggle.svelte +108 -0
  161. package/src/ui/UICopyPopover.svelte +78 -0
  162. package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
  163. package/src/ui/UIFontFamilySelector.svelte +309 -0
  164. package/src/ui/UIFontSizeSelector.svelte +165 -0
  165. package/src/ui/UIFontWeightSelector.svelte +52 -0
  166. package/src/ui/UILineHeightSelector.svelte +47 -0
  167. package/src/ui/UILinkToggle.svelte +60 -0
  168. package/src/ui/UIOptionItem.svelte +74 -0
  169. package/src/ui/UIOptionList.svelte +27 -0
  170. package/src/ui/UIPaddingSelector.svelte +661 -0
  171. package/src/ui/UIPaletteSelector.svelte +1084 -0
  172. package/src/ui/UIRadio.svelte +72 -0
  173. package/src/ui/UIRadioGroup.svelte +59 -0
  174. package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
  175. package/src/ui/UITokenSelector.svelte +509 -0
  176. package/src/ui/UIVariantSelector.svelte +145 -0
  177. package/src/ui/VariablesTab.svelte +252 -0
  178. package/src/ui/index.ts +31 -0
  179. package/src/ui/keepInViewport.ts +84 -0
  180. package/src/ui/palette/GradientStopEditor.svelte +482 -0
  181. package/src/ui/palette/OverridesPanel.svelte +526 -0
  182. package/src/ui/palette/PaletteBase.svelte +165 -0
  183. package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
  184. package/src/ui/palette/paletteEditorState.ts +89 -0
  185. package/src/ui/sections/ColumnsSection.svelte +273 -0
  186. package/src/ui/sections/GradientsSection.svelte +147 -0
  187. package/src/ui/sections/OverlaysSection.svelte +670 -0
  188. package/src/ui/sections/ShadowsSection.svelte +1250 -0
  189. package/src/ui/sections/TokenScaleTable.svelte +332 -0
  190. package/src/ui/sections/tokenScales.ts +81 -0
  191. package/src/ui/variantScales.ts +108 -0
  192. package/src/components/DetailNav.svelte +0 -78
  193. package/src/components/Toggle.svelte +0 -86
  194. package/src/lib/tokenInit.ts +0 -29
  195. package/src/lib/tokenService.ts +0 -144
  196. package/src/lib/tokenTypes.ts +0 -45
  197. package/src/pages/Admin.svelte +0 -100
  198. package/src/pages/ShowcasePage.svelte +0 -144
  199. package/src/showcase/BackupBrowser.svelte +0 -617
  200. package/src/showcase/ComponentsTab.svelte +0 -105
  201. package/src/showcase/PaletteEditor.svelte +0 -2579
  202. package/src/showcase/PaletteSelector.svelte +0 -627
  203. package/src/showcase/TokenMap.svelte +0 -54
  204. package/src/showcase/VariablesTab.svelte +0 -2655
  205. package/src/showcase/VisualsTab.svelte +0 -231
  206. package/src/showcase/demos/BadgeDemo.svelte +0 -56
  207. package/src/showcase/demos/CardDemo.svelte +0 -50
  208. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
  209. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
  210. package/src/showcase/demos/DialogDemo.svelte +0 -42
  211. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
  212. package/src/showcase/demos/NotificationDemo.svelte +0 -147
  213. package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
  214. package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
  215. package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
  216. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
  217. package/src/showcase/demos/TabBarDemo.svelte +0 -58
  218. package/src/showcase/demos/TooltipDemo.svelte +0 -52
  219. package/src/showcase/editor.css +0 -93
  220. package/src/showcase/index.ts +0 -17
  221. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  222. package/src/styles/fonts/Domine/OFL.txt +0 -97
  223. package/src/styles/fonts/Domine/README.txt +0 -66
  224. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -0,0 +1,1250 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Shadows section lifted from VariablesTab.
4
+ *
5
+ * Owns: the global light controls (angle dial + min/max ranges with
6
+ * locked single-value mode), the per-token override state (each scale
7
+ * shadow remembers which fields the user has manually pinned), the dial
8
+ * drag scopes, and the bg-picker menu used to preview shadows on
9
+ * different surface tones.
10
+ *
11
+ * Mutations flow through mutate()/beginScope()/commitScope() so undo
12
+ * captures coherent edits. The store's subscriber fans values to :root
13
+ * via cssVarSync.
14
+ */
15
+ import { onMount, createEventDispatcher } from 'svelte';
16
+ import {
17
+ editorState, mutate, beginScope, commitScope, beginSliderGesture,
18
+ seedShadowsFromDom, shadowTokenCss, computeShadowXY,
19
+ SCALE_SHADOW_VARIABLES, defaultShadowOverride,
20
+ type Scope,
21
+ } from '../../lib/editorStore';
22
+ import type { ShadowToken, ShadowOverrideFlags, EditorState } from '../../lib/editorTypes';
23
+
24
+ export let copiedVar: string | null = null;
25
+
26
+ const dispatch = createEventDispatcher<{ copy: string }>();
27
+ function copy(v: string) { dispatch('copy', v); }
28
+
29
+ function getShadowOverride(s: EditorState, variable: string): ShadowOverrideFlags {
30
+ let ov = s.shadows.overrides[variable];
31
+ if (!ov) {
32
+ ov = defaultShadowOverride();
33
+ s.shadows.overrides[variable] = ov;
34
+ }
35
+ return ov;
36
+ }
37
+
38
+ function shadowTokenValueLabel(t: ShadowToken): string {
39
+ return `${t.x} ${t.y} ${t.blur}px`;
40
+ }
41
+
42
+ function interpolateScale<T extends ShadowToken>(
43
+ tokens: T[],
44
+ overrides: Record<string, ShadowOverrideFlags>,
45
+ flag: keyof ShadowOverrideFlags,
46
+ mutateFn: (t: T, frac: number) => void,
47
+ ) {
48
+ const eligible = tokens.filter((t) => SCALE_SHADOW_VARIABLES.has(t.variable) && !(overrides[t.variable]?.[flag]));
49
+ const last = eligible.length - 1;
50
+ eligible.forEach((t, i) => mutateFn(t, last > 0 ? i / last : 0.5));
51
+ }
52
+
53
+ function setGlobalAngle(value: number) {
54
+ mutate('set shadow angle', (s) => {
55
+ s.shadows.globals.angle = ((Math.round(value) % 360) + 360) % 360;
56
+ for (const t of s.shadows.tokens) {
57
+ if (!SCALE_SHADOW_VARIABLES.has(t.variable)) continue;
58
+ if (getShadowOverride(s, t.variable).angle) continue;
59
+ t.angle = s.shadows.globals.angle;
60
+ const { x, y } = computeShadowXY(t.angle, t.distance);
61
+ t.x = x; t.y = y;
62
+ }
63
+ });
64
+ }
65
+
66
+ function setGlobalOpacity(field: 'opacityMin' | 'opacityMax', value01: number) {
67
+ mutate(`set shadow ${field}`, (s) => {
68
+ const v = Math.max(0, Math.min(1, Math.round(value01 * 100) / 100));
69
+ const g = s.shadows.globals;
70
+ g[field] = v;
71
+ if (g.opacityLocked) g[field === 'opacityMin' ? 'opacityMax' : 'opacityMin'] = v;
72
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'opacity', (t, frac) => {
73
+ t.opacity = Math.round((g.opacityMin + frac * (g.opacityMax - g.opacityMin)) * 100) / 100;
74
+ });
75
+ });
76
+ }
77
+
78
+ function setGlobalDistance(field: 'distanceMin' | 'distanceMax', value: number) {
79
+ mutate(`set shadow ${field}`, (s) => {
80
+ const g = s.shadows.globals;
81
+ g[field] = Math.max(0, Math.round(value));
82
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'distance', (t, frac) => {
83
+ t.distance = Math.round(g.distanceMin + frac * (g.distanceMax - g.distanceMin));
84
+ const { x, y } = computeShadowXY(t.angle, t.distance);
85
+ t.x = x; t.y = y;
86
+ });
87
+ });
88
+ }
89
+
90
+ function setGlobalBlur(field: 'blurMin' | 'blurMax', value: number) {
91
+ mutate(`set shadow ${field}`, (s) => {
92
+ const g = s.shadows.globals;
93
+ g[field] = Math.max(0, Math.round(value));
94
+ if (g.blurLocked) g[field === 'blurMin' ? 'blurMax' : 'blurMin'] = g[field];
95
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'blur', (t, frac) => {
96
+ t.blur = Math.round(g.blurMin + frac * (g.blurMax - g.blurMin));
97
+ });
98
+ });
99
+ }
100
+
101
+ function setGlobalSize(field: 'sizeMin' | 'sizeMax', value: number) {
102
+ mutate(`set shadow ${field}`, (s) => {
103
+ const g = s.shadows.globals;
104
+ g[field] = Math.max(-50, Math.min(50, Math.round(value)));
105
+ if (g.sizeLocked) g[field === 'sizeMin' ? 'sizeMax' : 'sizeMin'] = g[field];
106
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'size', (t, frac) => {
107
+ t.spread = Math.round(g.sizeMin + frac * (g.sizeMax - g.sizeMin));
108
+ });
109
+ });
110
+ }
111
+
112
+ function setGlobalColor(field: 'hue' | 'saturation' | 'lightness', value: number) {
113
+ mutate(`set shadow ${field}`, (s) => {
114
+ const g = s.shadows.globals;
115
+ const hi = field === 'hue' ? 360 : 100;
116
+ g[field] = Math.max(0, Math.min(hi, Math.round(value)));
117
+ for (const t of s.shadows.tokens) {
118
+ if (!SCALE_SHADOW_VARIABLES.has(t.variable)) continue;
119
+ if (getShadowOverride(s, t.variable).color) continue;
120
+ t.hue = g.hue; t.saturation = g.saturation; t.lightness = g.lightness;
121
+ }
122
+ });
123
+ }
124
+
125
+ function toggleLock(field: 'opacityLocked' | 'blurLocked' | 'sizeLocked') {
126
+ mutate(`toggle shadow ${field}`, (s) => {
127
+ const g = s.shadows.globals;
128
+ const next = !g[field];
129
+ g[field] = next;
130
+ if (next) {
131
+ // Clamp max to min when re-locking and re-broadcast the scale edit.
132
+ if (field === 'opacityLocked') {
133
+ g.opacityMax = g.opacityMin;
134
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'opacity', (t, frac) => {
135
+ t.opacity = Math.round((g.opacityMin + frac * (g.opacityMax - g.opacityMin)) * 100) / 100;
136
+ });
137
+ } else if (field === 'blurLocked') {
138
+ g.blurMax = g.blurMin;
139
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'blur', (t, frac) => {
140
+ t.blur = Math.round(g.blurMin + frac * (g.blurMax - g.blurMin));
141
+ });
142
+ } else {
143
+ g.sizeMax = g.sizeMin;
144
+ interpolateScale(s.shadows.tokens, s.shadows.overrides, 'size', (t, frac) => {
145
+ t.spread = Math.round(g.sizeMin + frac * (g.sizeMax - g.sizeMin));
146
+ });
147
+ }
148
+ }
149
+ });
150
+ }
151
+
152
+ /** Locate token at `idx` and run `mut` inside a single `mutate(...)` action.
153
+ * Token-field handlers below collapse to one or two lines via this helper —
154
+ * per-field clamp/round semantics stay distinct, but the mutate/find/early-
155
+ * return prelude doesn't repeat. */
156
+ function withShadowToken(label: string, idx: number, mut: (t: ShadowToken, s: EditorState) => void) {
157
+ mutate(`set shadow token ${label}`, (s) => {
158
+ const t = s.shadows.tokens[idx];
159
+ if (!t) return;
160
+ mut(t, s);
161
+ });
162
+ }
163
+
164
+ // Per-token edits set the override flag for the edited field so future
165
+ // global broadcasts skip this token, preserving the user's manual value.
166
+ function setTokenField(idx: number, field: 'angle' | 'distance' | 'spread' | 'blur' | 'opacity', value: number) {
167
+ withShadowToken(field, idx, (t, s) => {
168
+ const ov = getShadowOverride(s, t.variable);
169
+ if (field === 'angle') {
170
+ t.angle = ((Math.round(value) % 360) + 360) % 360;
171
+ const { x, y } = computeShadowXY(t.angle, t.distance);
172
+ t.x = x; t.y = y;
173
+ ov.angle = true;
174
+ } else if (field === 'distance') {
175
+ t.distance = Math.max(0, Math.round(value));
176
+ const { x, y } = computeShadowXY(t.angle, t.distance);
177
+ t.x = x; t.y = y;
178
+ ov.distance = true;
179
+ } else if (field === 'spread') {
180
+ t.spread = Math.max(-50, Math.min(50, Math.round(value)));
181
+ ov.size = true;
182
+ } else if (field === 'blur') {
183
+ t.blur = Math.max(0, Math.round(value));
184
+ ov.blur = true;
185
+ } else {
186
+ t.opacity = Math.max(0, Math.min(1, Math.round(value * 100) / 100));
187
+ ov.opacity = true;
188
+ }
189
+ });
190
+ }
191
+
192
+ function setTokenColor(idx: number, field: 'hue' | 'saturation' | 'lightness', value: number) {
193
+ withShadowToken(field, idx, (t, s) => {
194
+ const hi = field === 'hue' ? 360 : 100;
195
+ t[field] = Math.max(0, Math.min(hi, Math.round(value)));
196
+ getShadowOverride(s, t.variable).color = true;
197
+ });
198
+ }
199
+
200
+ function resetToGlobal() {
201
+ if (!editingToken || !editingIsScale) return;
202
+ const variable = editingToken.variable;
203
+ mutate('reset shadow token to global', (s) => {
204
+ const scale = s.shadows.tokens.filter((t) => SCALE_SHADOW_VARIABLES.has(t.variable));
205
+ const t = scale.find((x) => x.variable === variable);
206
+ if (!t) return;
207
+ const pos = scale.indexOf(t);
208
+ const last = scale.length - 1;
209
+ const frac = last > 0 ? pos / last : 0.5;
210
+ const g = s.shadows.globals;
211
+ t.hue = g.hue; t.saturation = g.saturation; t.lightness = g.lightness;
212
+ t.angle = g.angle;
213
+ t.distance = Math.round(g.distanceMin + frac * (g.distanceMax - g.distanceMin));
214
+ t.blur = Math.round(g.blurMin + frac * (g.blurMax - g.blurMin));
215
+ t.spread = Math.round(g.sizeMin + frac * (g.sizeMax - g.sizeMin));
216
+ t.opacity = Math.round((g.opacityMin + frac * (g.opacityMax - g.opacityMin)) * 100) / 100;
217
+ const { x, y } = computeShadowXY(t.angle, t.distance);
218
+ t.x = x; t.y = y;
219
+ s.shadows.overrides[variable] = defaultShadowOverride();
220
+ });
221
+ }
222
+
223
+ // HSL gradient helpers (read from globals for their background-color cues).
224
+ $: sg = $editorState.shadows.globals;
225
+ $: shadowHueGrad = `linear-gradient(to right, ${
226
+ [0, 60, 120, 180, 240, 300, 360].map(h => `hsl(${h},${sg.saturation}%,${sg.lightness}%)`).join(',')
227
+ })`;
228
+ $: shadowSatGrad = `linear-gradient(to right, hsl(${sg.hue},0%,${sg.lightness}%), hsl(${sg.hue},100%,${sg.lightness}%))`;
229
+ $: shadowLightGrad = `linear-gradient(to right, hsl(${sg.hue},${sg.saturation}%,0%), hsl(${sg.hue},${sg.saturation}%,50%), hsl(${sg.hue},${sg.saturation}%,100%))`;
230
+
231
+ let editingShadow: string | null = null;
232
+
233
+ $: shadowTokens = $editorState.shadows.tokens;
234
+ $: editingToken = editingShadow ? shadowTokens.find(t => t.variable === editingShadow) ?? null : null;
235
+ $: editingIdx = editingToken ? shadowTokens.indexOf(editingToken) : -1;
236
+ $: editingIsScale = editingToken ? SCALE_SHADOW_VARIABLES.has(editingToken.variable) : false;
237
+
238
+ function angleFromPointer(event: PointerEvent, dialEl: SVGSVGElement): number {
239
+ const rect = dialEl.getBoundingClientRect();
240
+ const cx = rect.left + rect.width / 2;
241
+ const cy = rect.top + rect.height / 2;
242
+ const dx = event.clientX - cx;
243
+ const dy = event.clientY - cy;
244
+ let angle = Math.atan2(-dy, dx) * (180 / Math.PI);
245
+ if (angle < 0) angle += 360;
246
+ return Math.round(angle);
247
+ }
248
+
249
+ // Dial drag: open a scope, stream angle edits into it, commit on pointerup
250
+ // (observed on window so an off-dial release still commits).
251
+ let dialDragIdx: number | null = null;
252
+ let dialDragScope: Scope | null = null;
253
+ function handleDialDown(event: PointerEvent, idx: number) {
254
+ dialDragIdx = idx;
255
+ const svg = event.currentTarget as SVGSVGElement;
256
+ svg.setPointerCapture(event.pointerId);
257
+ dialDragScope = beginScope({ label: 'drag shadow angle', collapseToOne: true, clipUndoFloor: false });
258
+ setTokenField(idx, 'angle', angleFromPointer(event, svg));
259
+ }
260
+ function handleDialMove(event: PointerEvent, idx: number) {
261
+ if (dialDragIdx !== idx) return;
262
+ const svg = event.currentTarget as SVGSVGElement;
263
+ setTokenField(idx, 'angle', angleFromPointer(event, svg));
264
+ }
265
+ function handleDialUp() {
266
+ if (dialDragIdx === null) return;
267
+ dialDragIdx = null;
268
+ if (dialDragScope) { commitScope(dialDragScope); dialDragScope = null; }
269
+ }
270
+
271
+ let globalDialDrag = false;
272
+ let globalDialScope: Scope | null = null;
273
+ function handleGlobalDialDown(event: PointerEvent) {
274
+ globalDialDrag = true;
275
+ const svg = event.currentTarget as SVGSVGElement;
276
+ svg.setPointerCapture(event.pointerId);
277
+ globalDialScope = beginScope({ label: 'drag global shadow angle', collapseToOne: true, clipUndoFloor: false });
278
+ setGlobalAngle(angleFromPointer(event, svg));
279
+ }
280
+ function handleGlobalDialMove(event: PointerEvent) {
281
+ if (!globalDialDrag) return;
282
+ const svg = event.currentTarget as SVGSVGElement;
283
+ setGlobalAngle(angleFromPointer(event, svg));
284
+ }
285
+ function handleGlobalDialUp() {
286
+ if (!globalDialDrag) return;
287
+ globalDialDrag = false;
288
+ if (globalDialScope) { commitScope(globalDialScope); globalDialScope = null; }
289
+ }
290
+
291
+ // Background picker for the shadows preview canvas.
292
+ interface ColorGroup {
293
+ label: string;
294
+ colors: { label: string; value: string }[];
295
+ }
296
+
297
+ const bgColorGroups: ColorGroup[] = [
298
+ {
299
+ label: 'Surfaces',
300
+ colors: [
301
+ { label: 'Neutral Lowest', value: 'var(--surface-neutral-lowest)' },
302
+ { label: 'Neutral Lower', value: 'var(--surface-neutral-lower)' },
303
+ { label: 'Neutral Low', value: 'var(--surface-neutral-low)' },
304
+ { label: 'Neutral', value: 'var(--surface-neutral)' },
305
+ { label: 'Neutral High', value: 'var(--surface-neutral-high)' },
306
+ { label: 'Neutral Higher', value: 'var(--surface-neutral-higher)' },
307
+ { label: 'Neutral Highest', value: 'var(--surface-neutral-highest)' },
308
+ ]
309
+ },
310
+ {
311
+ label: 'Alternate',
312
+ colors: [
313
+ { label: 'Lowest', value: 'var(--surface-alternate-lowest)' },
314
+ { label: 'Lower', value: 'var(--surface-alternate-lower)' },
315
+ { label: 'Low', value: 'var(--surface-alternate-low)' },
316
+ { label: 'Base', value: 'var(--surface-alternate)' },
317
+ { label: 'High', value: 'var(--surface-alternate-high)' },
318
+ { label: 'Higher', value: 'var(--surface-alternate-higher)' },
319
+ { label: 'Highest', value: 'var(--surface-alternate-highest)' },
320
+ ]
321
+ },
322
+ {
323
+ label: 'Background',
324
+ colors: [
325
+ { label: 'Lowest', value: 'var(--surface-canvas-lowest)' },
326
+ { label: 'Lower', value: 'var(--surface-canvas-lower)' },
327
+ { label: 'Low', value: 'var(--surface-canvas-low)' },
328
+ { label: 'Base', value: 'var(--surface-canvas)' },
329
+ { label: 'High', value: 'var(--surface-canvas-high)' },
330
+ { label: 'Higher', value: 'var(--surface-canvas-higher)' },
331
+ { label: 'Highest', value: 'var(--surface-canvas-highest)' },
332
+ ]
333
+ },
334
+ {
335
+ label: 'Brand',
336
+ colors: [
337
+ { label: 'Lowest', value: 'var(--surface-brand-lowest)' },
338
+ { label: 'Lower', value: 'var(--surface-brand-lower)' },
339
+ { label: 'Low', value: 'var(--surface-brand-low)' },
340
+ { label: 'Base', value: 'var(--surface-brand)' },
341
+ { label: 'High', value: 'var(--surface-brand-high)' },
342
+ { label: 'Higher', value: 'var(--surface-brand-higher)' },
343
+ { label: 'Highest', value: 'var(--surface-brand-highest)' },
344
+ ]
345
+ },
346
+ {
347
+ label: 'Accent',
348
+ colors: [
349
+ { label: 'Lowest', value: 'var(--surface-accent-lowest)' },
350
+ { label: 'Lower', value: 'var(--surface-accent-lower)' },
351
+ { label: 'Low', value: 'var(--surface-accent-low)' },
352
+ { label: 'Base', value: 'var(--surface-accent)' },
353
+ { label: 'High', value: 'var(--surface-accent-high)' },
354
+ { label: 'Higher', value: 'var(--surface-accent-higher)' },
355
+ { label: 'Highest', value: 'var(--surface-accent-highest)' },
356
+ ]
357
+ },
358
+ {
359
+ label: 'Special',
360
+ colors: [
361
+ { label: 'Lowest', value: 'var(--surface-special-lowest)' },
362
+ { label: 'Lower', value: 'var(--surface-special-lower)' },
363
+ { label: 'Low', value: 'var(--surface-special-low)' },
364
+ { label: 'Base', value: 'var(--surface-special)' },
365
+ { label: 'High', value: 'var(--surface-special-high)' },
366
+ { label: 'Higher', value: 'var(--surface-special-higher)' },
367
+ { label: 'Highest', value: 'var(--surface-special-highest)' },
368
+ ]
369
+ },
370
+ {
371
+ label: 'Color Ramps',
372
+ colors: [
373
+ { label: 'Neutral 800', value: 'var(--color-neutral-800)' },
374
+ { label: 'Neutral 700', value: 'var(--color-neutral-700)' },
375
+ { label: 'Neutral 600', value: 'var(--color-neutral-600)' },
376
+ { label: 'Neutral 500', value: 'var(--color-neutral-500)' },
377
+ { label: 'Neutral 400', value: 'var(--color-neutral-400)' },
378
+ { label: 'Neutral 300', value: 'var(--color-neutral-300)' },
379
+ { label: 'Neutral 200', value: 'var(--color-neutral-200)' },
380
+ { label: 'Neutral 100', value: 'var(--color-neutral-100)' },
381
+ { label: 'White', value: '#ffffff' },
382
+ ]
383
+ },
384
+ ];
385
+
386
+ let shadowBg = 'var(--ui-surface-highest)';
387
+ let bgPickerOpen = false;
388
+ let expandedGroup: string | null = null;
389
+
390
+ function pickBg(value: string) {
391
+ shadowBg = value;
392
+ bgPickerOpen = false;
393
+ expandedGroup = null;
394
+ }
395
+
396
+ function toggleBgPicker() {
397
+ bgPickerOpen = !bgPickerOpen;
398
+ if (!bgPickerOpen) expandedGroup = null;
399
+ }
400
+
401
+ // Seed the shadows store from tokens.css on first mount so the editor
402
+ // starts from the runtime baseline.
403
+ onMount(() => {
404
+ seedShadowsFromDom();
405
+ });
406
+ </script>
407
+
408
+ <section class="section shadows-section" id="shadows" style="background: {shadowBg};">
409
+ <h2 class="section-title">Shadows</h2>
410
+
411
+ <div class="shadows-layout">
412
+ <div class="shadows-main">
413
+ <div class="shadows-grid">
414
+ {#each shadowTokens.filter(t => SCALE_SHADOW_VARIABLES.has(t.variable)) as token}
415
+ <div class="shadow-item" class:active={editingShadow === token.variable}>
416
+ <div class="shadow-box" style="box-shadow: {shadowTokenCss(token)};"></div>
417
+ <div class="token-info">
418
+ <button class="token-variable copyable" class:copied={copiedVar === token.variable} on:click={() => copy(token.variable)}>{copiedVar === token.variable ? 'copied!' : token.variable}</button>
419
+ <span class="token-value">{shadowTokenValueLabel(token)}</span>
420
+ </div>
421
+ <button class="shadow-edit-btn" on:click={() => editingShadow = editingShadow === token.variable ? null : token.variable}>
422
+ {editingShadow === token.variable ? 'Close' : 'Edit'}
423
+ </button>
424
+ </div>
425
+ {/each}
426
+ </div>
427
+
428
+ <div class="shadows-grid">
429
+ {#each shadowTokens.filter(t => !SCALE_SHADOW_VARIABLES.has(t.variable)) as token}
430
+ <div class="shadow-item" class:active={editingShadow === token.variable}>
431
+ <div class="shadow-box" style="box-shadow: {shadowTokenCss(token)};"></div>
432
+ <div class="token-info">
433
+ <button class="token-variable copyable" class:copied={copiedVar === token.variable} on:click={() => copy(token.variable)}>{copiedVar === token.variable ? 'copied!' : token.variable}</button>
434
+ <span class="token-value">{shadowTokenValueLabel(token)}</span>
435
+ </div>
436
+ <button class="shadow-edit-btn" on:click={() => editingShadow = editingShadow === token.variable ? null : token.variable}>
437
+ {editingShadow === token.variable ? 'Close' : 'Edit'}
438
+ </button>
439
+ </div>
440
+ {/each}
441
+ </div>
442
+
443
+ </div><!-- /.shadows-main -->
444
+
445
+ <!-- Shadow editor sidebar -->
446
+ <div class="global-shadow-editor">
447
+ {#if editingToken}
448
+ <div class="editor-header">
449
+ <h4 class="global-shadow-title">{editingToken.variable}</h4>
450
+ <button class="shadow-edit-btn" on:click={() => editingShadow = null}>Close</button>
451
+ </div>
452
+ {#if editingIsScale}
453
+ <button class="reset-btn" on:click={resetToGlobal}>Reset to Global</button>
454
+ {/if}
455
+ <div class="global-shadow-row">
456
+ <span class="shadow-slider-label" title="Direction the light source is coming from — controls which side the shadow falls on">Angle</span>
457
+ <svg class="angle-dial" viewBox="0 0 48 48" width="48" height="48"
458
+ on:pointerdown={(e) => handleDialDown(e, editingIdx)}
459
+ on:pointermove={(e) => handleDialMove(e, editingIdx)}
460
+ on:pointerup={handleDialUp}
461
+ >
462
+ <circle cx="24" cy="24" r="20" class="dial-ring" />
463
+ <line x1="24" y1="24"
464
+ x2={24 + 18 * Math.cos(editingToken.angle * Math.PI / 180)}
465
+ y2={24 - 18 * Math.sin(editingToken.angle * Math.PI / 180)}
466
+ class="dial-line" />
467
+ <circle
468
+ cx={24 + 18 * Math.cos(editingToken.angle * Math.PI / 180)}
469
+ cy={24 - 18 * Math.sin(editingToken.angle * Math.PI / 180)}
470
+ r="3" class="dial-handle" />
471
+ </svg>
472
+ <input class="shadow-slider-input" type="number" min="0" max="360"
473
+ value={editingToken.angle}
474
+ on:change={(e) => setTokenField(editingIdx, 'angle', +e.currentTarget.value)} />
475
+ <span class="shadow-slider-unit">&deg;</span>
476
+ </div>
477
+ <div class="global-shadow-row">
478
+ <span class="shadow-slider-label" title="How far the shadow is cast from the element — simulates height off the surface">Dist</span>
479
+ <input type="range" min="0" max="60" value={editingToken.distance}
480
+ on:pointerdown={() => beginSliderGesture('edit shadow distance')}
481
+ on:input={(e) => setTokenField(editingIdx, 'distance', +e.currentTarget.value)} />
482
+ <input class="shadow-slider-input" type="number" min="0" max="100"
483
+ value={editingToken.distance}
484
+ on:change={(e) => setTokenField(editingIdx, 'distance', +e.currentTarget.value)} />
485
+ <span class="shadow-slider-unit">px</span>
486
+ </div>
487
+ <div class="global-shadow-row">
488
+ <span class="shadow-slider-label" title="Grows or shrinks the shadow before blurring — positive makes it larger than the element, negative makes it smaller">Spread</span>
489
+ <input type="range" min="-50" max="50" value={editingToken.spread}
490
+ on:pointerdown={() => beginSliderGesture('edit shadow spread')}
491
+ on:input={(e) => setTokenField(editingIdx, 'spread', +e.currentTarget.value)} />
492
+ <input class="shadow-slider-input" type="number" min="-50" max="50"
493
+ value={editingToken.spread}
494
+ on:change={(e) => setTokenField(editingIdx, 'spread', +e.currentTarget.value)} />
495
+ <span class="shadow-slider-unit">px</span>
496
+ </div>
497
+ <div class="global-shadow-row">
498
+ <span class="shadow-slider-label" title="How soft the shadow edge is — higher values make a wider, more diffused shadow">Blur</span>
499
+ <input type="range" min="0" max="100" value={editingToken.blur}
500
+ on:pointerdown={() => beginSliderGesture('edit shadow blur')}
501
+ on:input={(e) => setTokenField(editingIdx, 'blur', +e.currentTarget.value)} />
502
+ <input class="shadow-slider-input" type="number" min="0" max="100"
503
+ value={editingToken.blur}
504
+ on:change={(e) => setTokenField(editingIdx, 'blur', +e.currentTarget.value)} />
505
+ <span class="shadow-slider-unit">px</span>
506
+ </div>
507
+ <div class="global-shadow-row">
508
+ <span class="shadow-slider-label" title="How visible the shadow is — 0% is invisible, 100% is fully opaque">Op.</span>
509
+ <input type="range" min="0" max="100" value={Math.round(editingToken.opacity * 100)}
510
+ on:pointerdown={() => beginSliderGesture('edit shadow opacity')}
511
+ on:input={(e) => setTokenField(editingIdx, 'opacity', +e.currentTarget.value / 100)} />
512
+ <input class="shadow-slider-input" type="number" min="0" max="100"
513
+ value={Math.round(editingToken.opacity * 100)}
514
+ on:change={(e) => setTokenField(editingIdx, 'opacity', +e.currentTarget.value / 100)} />
515
+ <span class="shadow-slider-unit">%</span>
516
+ </div>
517
+ <div class="global-color-group">
518
+ <div class="global-color-swatch" style="background: hsl({editingToken.hue}, {editingToken.saturation}%, {editingToken.lightness}%);"></div>
519
+ <div class="global-color-sliders">
520
+ <div class="global-shadow-row">
521
+ <span class="shadow-slider-label" title="Hue — the base color of the shadow (0°=red, 120°=green, 240°=blue)">H</span>
522
+ <div class="slider-track" style="background: {shadowHueGrad}">
523
+ <input type="range" min="0" max="360" value={editingToken.hue}
524
+ on:pointerdown={() => beginSliderGesture('edit shadow hue')}
525
+ on:input={(e) => setTokenColor(editingIdx, 'hue', +e.currentTarget.value)} />
526
+ </div>
527
+ <input class="shadow-slider-input" type="number" min="0" max="360"
528
+ value={editingToken.hue}
529
+ on:change={(e) => setTokenColor(editingIdx, 'hue', +e.currentTarget.value)} />
530
+ <span class="shadow-slider-unit">&deg;</span>
531
+ </div>
532
+ <div class="global-shadow-row">
533
+ <span class="shadow-slider-label" title="Saturation — 0% is gray, 100% is full color intensity">S</span>
534
+ <div class="slider-track" style="background: linear-gradient(to right, hsl({editingToken.hue},0%,{editingToken.lightness}%), hsl({editingToken.hue},100%,{editingToken.lightness}%))">
535
+ <input type="range" min="0" max="100" value={editingToken.saturation}
536
+ on:pointerdown={() => beginSliderGesture('edit shadow saturation')}
537
+ on:input={(e) => setTokenColor(editingIdx, 'saturation', +e.currentTarget.value)} />
538
+ </div>
539
+ <input class="shadow-slider-input" type="number" min="0" max="100"
540
+ value={editingToken.saturation}
541
+ on:change={(e) => setTokenColor(editingIdx, 'saturation', +e.currentTarget.value)} />
542
+ <span class="shadow-slider-unit">%</span>
543
+ </div>
544
+ <div class="global-shadow-row">
545
+ <span class="shadow-slider-label" title="Lightness — 0% is black, 100% is white">L</span>
546
+ <div class="slider-track" style="background: linear-gradient(to right, hsl({editingToken.hue},{editingToken.saturation}%,0%), hsl({editingToken.hue},{editingToken.saturation}%,50%), hsl({editingToken.hue},{editingToken.saturation}%,100%))">
547
+ <input type="range" min="0" max="100" value={editingToken.lightness}
548
+ on:pointerdown={() => beginSliderGesture('edit shadow lightness')}
549
+ on:input={(e) => setTokenColor(editingIdx, 'lightness', +e.currentTarget.value)} />
550
+ </div>
551
+ <input class="shadow-slider-input" type="number" min="0" max="100"
552
+ value={editingToken.lightness}
553
+ on:change={(e) => setTokenColor(editingIdx, 'lightness', +e.currentTarget.value)} />
554
+ <span class="shadow-slider-unit">%</span>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ <div class="shadow-css-output">
559
+ <code>{shadowTokenCss(editingToken)}</code>
560
+ <button class="shadow-copy-btn" on:click={() => copy(shadowTokenCss(editingToken))}>
561
+ {copiedVar === shadowTokenCss(editingToken) ? 'Copied!' : 'Copy CSS'}
562
+ </button>
563
+ </div>
564
+ {:else}
565
+ <h4 class="global-shadow-title">Global Light</h4>
566
+ <div class="global-shadow-row">
567
+ <span class="shadow-slider-label" title="Direction the light source is coming from — controls which side the shadow falls on">Angle</span>
568
+ <svg class="angle-dial" viewBox="0 0 48 48" width="48" height="48"
569
+ on:pointerdown={handleGlobalDialDown}
570
+ on:pointermove={handleGlobalDialMove}
571
+ on:pointerup={handleGlobalDialUp}
572
+ >
573
+ <circle cx="24" cy="24" r="20" class="dial-ring" />
574
+ <line x1="24" y1="24"
575
+ x2={24 + 18 * Math.cos(sg.angle * Math.PI / 180)}
576
+ y2={24 - 18 * Math.sin(sg.angle * Math.PI / 180)}
577
+ class="dial-line" />
578
+ <circle
579
+ cx={24 + 18 * Math.cos(sg.angle * Math.PI / 180)}
580
+ cy={24 - 18 * Math.sin(sg.angle * Math.PI / 180)}
581
+ r="3" class="dial-handle" />
582
+ </svg>
583
+ <input class="shadow-slider-input" type="number" min="0" max="360"
584
+ value={sg.angle}
585
+ on:change={(e) => setGlobalAngle(+e.currentTarget.value)} />
586
+ <span class="shadow-slider-unit">&deg;</span>
587
+ </div>
588
+ <div class="global-shadow-row">
589
+ <span class="shadow-slider-label" title="How far the shadow is cast — simulates height off the surface">Dist Min</span>
590
+ <input type="range" min="0" max="60" value={sg.distanceMin}
591
+ on:pointerdown={() => beginSliderGesture('drag global dist min')}
592
+ on:input={(e) => setGlobalDistance('distanceMin', +e.currentTarget.value)} />
593
+ <input class="shadow-slider-input" type="number" min="0" max="100"
594
+ value={sg.distanceMin}
595
+ on:change={(e) => setGlobalDistance('distanceMin', +e.currentTarget.value)} />
596
+ <span class="shadow-slider-unit">px</span>
597
+ </div>
598
+ <div class="global-shadow-row">
599
+ <span class="shadow-slider-label" title="How far the shadow is cast — simulates height off the surface">Dist Max</span>
600
+ <input type="range" min="0" max="60" value={sg.distanceMax}
601
+ on:pointerdown={() => beginSliderGesture('drag global dist max')}
602
+ on:input={(e) => setGlobalDistance('distanceMax', +e.currentTarget.value)} />
603
+ <input class="shadow-slider-input" type="number" min="0" max="100"
604
+ value={sg.distanceMax}
605
+ on:change={(e) => setGlobalDistance('distanceMax', +e.currentTarget.value)} />
606
+ <span class="shadow-slider-unit">px</span>
607
+ </div>
608
+ {#if sg.sizeLocked}
609
+ <div class="global-shadow-row">
610
+ <span class="shadow-slider-label" title="Grows or shrinks the shadow before blurring — positive makes it larger than the element, negative makes it smaller">Spread</span>
611
+ <input type="range" min="-50" max="50" value={sg.sizeMin}
612
+ on:pointerdown={() => beginSliderGesture('drag global spread')}
613
+ on:input={(e) => setGlobalSize('sizeMin', +e.currentTarget.value)} />
614
+ <input class="shadow-slider-input" type="number" min="-50" max="50"
615
+ value={sg.sizeMin}
616
+ on:change={(e) => setGlobalSize('sizeMin', +e.currentTarget.value)} />
617
+ <span class="shadow-slider-unit">px</span>
618
+ <button class="lock-btn" title="Unlock min/max" on:click={() => toggleLock('sizeLocked')}>
619
+ <svg viewBox="0 0 16 16" width="14" height="14"><path d="M4 7V5a4 4 0 118 0v2h1a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1V8a1 1 0 011-1h1zm2 0h4V5a2 2 0 10-4 0v2z" fill="currentColor"/></svg>
620
+ </button>
621
+ </div>
622
+ {:else}
623
+ <div class="global-shadow-row">
624
+ <span class="shadow-slider-label" title="Grows or shrinks the shadow before blurring — positive makes it larger than the element, negative makes it smaller">Spread Min</span>
625
+ <input type="range" min="-50" max="50" value={sg.sizeMin}
626
+ on:pointerdown={() => beginSliderGesture('drag global spread min')}
627
+ on:input={(e) => setGlobalSize('sizeMin', +e.currentTarget.value)} />
628
+ <input class="shadow-slider-input" type="number" min="-50" max="50"
629
+ value={sg.sizeMin}
630
+ on:change={(e) => setGlobalSize('sizeMin', +e.currentTarget.value)} />
631
+ <span class="shadow-slider-unit">px</span>
632
+ <button class="lock-btn unlocked" title="Lock to single value" on:click={() => toggleLock('sizeLocked')}>
633
+ <svg viewBox="0 0 16 16" width="14" height="14"><path d="M10 7V5a2 2 0 10-4 0v.5H4V5a4 4 0 118 0v2h1a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1V8a1 1 0 011-1h7z" fill="currentColor"/></svg>
634
+ </button>
635
+ </div>
636
+ <div class="global-shadow-row">
637
+ <span class="shadow-slider-label" title="Grows or shrinks the shadow before blurring — positive makes it larger than the element, negative makes it smaller">Spread Max</span>
638
+ <input type="range" min="-50" max="50" value={sg.sizeMax}
639
+ on:pointerdown={() => beginSliderGesture('drag global spread max')}
640
+ on:input={(e) => setGlobalSize('sizeMax', +e.currentTarget.value)} />
641
+ <input class="shadow-slider-input" type="number" min="-50" max="50"
642
+ value={sg.sizeMax}
643
+ on:change={(e) => setGlobalSize('sizeMax', +e.currentTarget.value)} />
644
+ <span class="shadow-slider-unit">px</span>
645
+ </div>
646
+ {/if}
647
+ {#if sg.blurLocked}
648
+ <div class="global-shadow-row">
649
+ <span class="shadow-slider-label" title="How soft the shadow edge is — higher values make a wider, more diffused shadow">Blur</span>
650
+ <input type="range" min="0" max="100" value={sg.blurMin}
651
+ on:pointerdown={() => beginSliderGesture('drag global blur')}
652
+ on:input={(e) => setGlobalBlur('blurMin', +e.currentTarget.value)} />
653
+ <input class="shadow-slider-input" type="number" min="0" max="100"
654
+ value={sg.blurMin}
655
+ on:change={(e) => setGlobalBlur('blurMin', +e.currentTarget.value)} />
656
+ <span class="shadow-slider-unit">px</span>
657
+ <button class="lock-btn" title="Unlock min/max" on:click={() => toggleLock('blurLocked')}>
658
+ <svg viewBox="0 0 16 16" width="14" height="14"><path d="M4 7V5a4 4 0 118 0v2h1a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1V8a1 1 0 011-1h1zm2 0h4V5a2 2 0 10-4 0v2z" fill="currentColor"/></svg>
659
+ </button>
660
+ </div>
661
+ {:else}
662
+ <div class="global-shadow-row">
663
+ <span class="shadow-slider-label" title="How soft the shadow edge is — higher values make a wider, more diffused shadow">Blur Min</span>
664
+ <input type="range" min="0" max="100" value={sg.blurMin}
665
+ on:pointerdown={() => beginSliderGesture('drag global blur min')}
666
+ on:input={(e) => setGlobalBlur('blurMin', +e.currentTarget.value)} />
667
+ <input class="shadow-slider-input" type="number" min="0" max="100"
668
+ value={sg.blurMin}
669
+ on:change={(e) => setGlobalBlur('blurMin', +e.currentTarget.value)} />
670
+ <span class="shadow-slider-unit">px</span>
671
+ <button class="lock-btn unlocked" title="Lock to single value" on:click={() => toggleLock('blurLocked')}>
672
+ <svg viewBox="0 0 16 16" width="14" height="14"><path d="M10 7V5a2 2 0 10-4 0v.5H4V5a4 4 0 118 0v2h1a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1V8a1 1 0 011-1h7z" fill="currentColor"/></svg>
673
+ </button>
674
+ </div>
675
+ <div class="global-shadow-row">
676
+ <span class="shadow-slider-label" title="How soft the shadow edge is — higher values make a wider, more diffused shadow">Blur Max</span>
677
+ <input type="range" min="0" max="100" value={sg.blurMax}
678
+ on:pointerdown={() => beginSliderGesture('drag global blur max')}
679
+ on:input={(e) => setGlobalBlur('blurMax', +e.currentTarget.value)} />
680
+ <input class="shadow-slider-input" type="number" min="0" max="100"
681
+ value={sg.blurMax}
682
+ on:change={(e) => setGlobalBlur('blurMax', +e.currentTarget.value)} />
683
+ <span class="shadow-slider-unit">px</span>
684
+ </div>
685
+ {/if}
686
+ {#if sg.opacityLocked}
687
+ <div class="global-shadow-row">
688
+ <span class="shadow-slider-label" title="How visible the shadow is — 0% is invisible, 100% is fully opaque">Op.</span>
689
+ <input type="range" min="0" max="100" value={Math.round(sg.opacityMin * 100)}
690
+ on:pointerdown={() => beginSliderGesture('drag global opacity')}
691
+ on:input={(e) => setGlobalOpacity('opacityMin', +e.currentTarget.value / 100)} />
692
+ <input class="shadow-slider-input" type="number" min="0" max="100"
693
+ value={Math.round(sg.opacityMin * 100)}
694
+ on:change={(e) => setGlobalOpacity('opacityMin', +e.currentTarget.value / 100)} />
695
+ <span class="shadow-slider-unit">%</span>
696
+ <button class="lock-btn" title="Unlock min/max" on:click={() => toggleLock('opacityLocked')}>
697
+ <svg viewBox="0 0 16 16" width="14" height="14"><path d="M4 7V5a4 4 0 118 0v2h1a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1V8a1 1 0 011-1h1zm2 0h4V5a2 2 0 10-4 0v2z" fill="currentColor"/></svg>
698
+ </button>
699
+ </div>
700
+ {:else}
701
+ <div class="global-shadow-row">
702
+ <span class="shadow-slider-label" title="How visible the shadow is — 0% is invisible, 100% is fully opaque">Op. Min</span>
703
+ <input type="range" min="0" max="100" value={Math.round(sg.opacityMin * 100)}
704
+ on:pointerdown={() => beginSliderGesture('drag global opacity min')}
705
+ on:input={(e) => setGlobalOpacity('opacityMin', +e.currentTarget.value / 100)} />
706
+ <input class="shadow-slider-input" type="number" min="0" max="100"
707
+ value={Math.round(sg.opacityMin * 100)}
708
+ on:change={(e) => setGlobalOpacity('opacityMin', +e.currentTarget.value / 100)} />
709
+ <span class="shadow-slider-unit">%</span>
710
+ <button class="lock-btn unlocked" title="Lock to single value" on:click={() => toggleLock('opacityLocked')}>
711
+ <svg viewBox="0 0 16 16" width="14" height="14"><path d="M10 7V5a2 2 0 10-4 0v.5H4V5a4 4 0 118 0v2h1a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1V8a1 1 0 011-1h7z" fill="currentColor"/></svg>
712
+ </button>
713
+ </div>
714
+ <div class="global-shadow-row">
715
+ <span class="shadow-slider-label" title="How visible the shadow is — 0% is invisible, 100% is fully opaque">Op. Max</span>
716
+ <input type="range" min="0" max="100" value={Math.round(sg.opacityMax * 100)}
717
+ on:pointerdown={() => beginSliderGesture('drag global opacity max')}
718
+ on:input={(e) => setGlobalOpacity('opacityMax', +e.currentTarget.value / 100)} />
719
+ <input class="shadow-slider-input" type="number" min="0" max="100"
720
+ value={Math.round(sg.opacityMax * 100)}
721
+ on:change={(e) => setGlobalOpacity('opacityMax', +e.currentTarget.value / 100)} />
722
+ <span class="shadow-slider-unit">%</span>
723
+ </div>
724
+ {/if}
725
+ <div class="global-color-group">
726
+ <div class="global-color-swatch" style="background: hsl({sg.hue}, {sg.saturation}%, {sg.lightness}%);"></div>
727
+ <div class="global-color-sliders">
728
+ <div class="global-shadow-row">
729
+ <span class="shadow-slider-label" title="Hue — the base color of the shadow (0°=red, 120°=green, 240°=blue)">H</span>
730
+ <div class="slider-track" style="background: {shadowHueGrad}">
731
+ <input type="range" min="0" max="360" value={sg.hue}
732
+ on:pointerdown={() => beginSliderGesture('drag global hue')}
733
+ on:input={(e) => setGlobalColor('hue', +e.currentTarget.value)} />
734
+ </div>
735
+ <input class="shadow-slider-input" type="number" min="0" max="360"
736
+ value={sg.hue}
737
+ on:change={(e) => setGlobalColor('hue', +e.currentTarget.value)} />
738
+ <span class="shadow-slider-unit">&deg;</span>
739
+ </div>
740
+ <div class="global-shadow-row">
741
+ <span class="shadow-slider-label" title="Saturation — 0% is gray, 100% is full color intensity">S</span>
742
+ <div class="slider-track" style="background: {shadowSatGrad}">
743
+ <input type="range" min="0" max="100" value={sg.saturation}
744
+ on:pointerdown={() => beginSliderGesture('drag global saturation')}
745
+ on:input={(e) => setGlobalColor('saturation', +e.currentTarget.value)} />
746
+ </div>
747
+ <input class="shadow-slider-input" type="number" min="0" max="100"
748
+ value={sg.saturation}
749
+ on:change={(e) => setGlobalColor('saturation', +e.currentTarget.value)} />
750
+ <span class="shadow-slider-unit">%</span>
751
+ </div>
752
+ <div class="global-shadow-row">
753
+ <span class="shadow-slider-label" title="Lightness — 0% is black, 100% is white">L</span>
754
+ <div class="slider-track" style="background: {shadowLightGrad}">
755
+ <input type="range" min="0" max="100" value={sg.lightness}
756
+ on:pointerdown={() => beginSliderGesture('drag global lightness')}
757
+ on:input={(e) => setGlobalColor('lightness', +e.currentTarget.value)} />
758
+ </div>
759
+ <input class="shadow-slider-input" type="number" min="0" max="100"
760
+ value={sg.lightness}
761
+ on:change={(e) => setGlobalColor('lightness', +e.currentTarget.value)} />
762
+ <span class="shadow-slider-unit">%</span>
763
+ </div>
764
+ </div>
765
+ </div>
766
+ <button class="bg-picker-btn" on:click={toggleBgPicker}>BG</button>
767
+ {#if bgPickerOpen}
768
+ <div class="bg-picker-menu">
769
+ {#each bgColorGroups as group}
770
+ <button class="bg-group-header" on:click={() => expandedGroup = expandedGroup === group.label ? null : group.label}>
771
+ <span>{group.label}</span>
772
+ <span class="bg-group-arrow">{expandedGroup === group.label ? '▴' : '▾'}</span>
773
+ </button>
774
+ {#if expandedGroup === group.label}
775
+ <div class="bg-group-colors">
776
+ {#each group.colors as color}
777
+ <button class="bg-color-option" on:click={() => pickBg(color.value)}>
778
+ <span class="bg-color-swatch" style="background: {color.value};"></span>
779
+ <span>{color.label}</span>
780
+ </button>
781
+ {/each}
782
+ </div>
783
+ {/if}
784
+ {/each}
785
+ </div>
786
+ {/if}
787
+ {/if}
788
+ </div>
789
+ </div><!-- /.shadows-layout -->
790
+ </section>
791
+
792
+ <style>
793
+ .section {
794
+ display: flex;
795
+ flex-direction: column;
796
+ gap: var(--ui-space-16);
797
+ }
798
+
799
+ .section-title {
800
+ font-size: var(--ui-font-size-lg);
801
+ font-weight: var(--ui-font-weight-semibold);
802
+ color: var(--ui-text-primary);
803
+ margin: 0;
804
+ padding-bottom: var(--ui-space-8);
805
+ border-bottom: 1px solid var(--ui-border-subtle);
806
+ }
807
+
808
+ .token-info {
809
+ display: flex;
810
+ flex-direction: column;
811
+ gap: var(--ui-space-2);
812
+ }
813
+
814
+ .token-variable.copyable {
815
+ all: unset;
816
+ font-size: var(--ui-font-size-md);
817
+ color: var(--ui-text-tertiary);
818
+ font-family: var(--ui-font-mono);
819
+ cursor: pointer;
820
+ transition: color var(--ui-transition-fast);
821
+ }
822
+
823
+ .token-variable.copyable:hover {
824
+ color: var(--ui-text-accent);
825
+ }
826
+
827
+ .token-variable.copyable.copied {
828
+ color: var(--ui-text-success);
829
+ }
830
+
831
+ .token-value {
832
+ font-size: var(--ui-font-size-md);
833
+ color: var(--ui-text-muted);
834
+ }
835
+
836
+ /* Shadows */
837
+ .shadows-section {
838
+ background: var(--ui-surface-highest);
839
+ padding: var(--ui-space-16);
840
+ border-radius: var(--ui-radius-lg);
841
+ position: relative;
842
+ }
843
+
844
+ .shadows-layout {
845
+ display: flex;
846
+ gap: var(--ui-space-16);
847
+ align-items: flex-start;
848
+ }
849
+
850
+ .shadows-main {
851
+ flex: 1;
852
+ min-width: 0;
853
+ }
854
+
855
+ .shadows-grid {
856
+ display: grid;
857
+ grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
858
+ gap: var(--ui-space-16);
859
+ }
860
+
861
+ .shadow-item {
862
+ display: flex;
863
+ flex-direction: column;
864
+ align-items: center;
865
+ gap: var(--ui-space-8);
866
+ }
867
+
868
+ .shadow-item.active {
869
+ outline: 2px solid var(--ui-text-accent);
870
+ outline-offset: var(--ui-space-4);
871
+ border-radius: var(--ui-radius-md);
872
+ }
873
+
874
+ .editor-header {
875
+ display: flex;
876
+ align-items: center;
877
+ justify-content: space-between;
878
+ gap: var(--ui-space-8);
879
+ }
880
+
881
+ .reset-btn {
882
+ all: unset;
883
+ font-size: var(--ui-font-size-xs);
884
+ color: var(--ui-text-muted);
885
+ cursor: pointer;
886
+ padding: var(--ui-space-4) var(--ui-space-8);
887
+ border: 1px solid var(--ui-border-subtle);
888
+ border-radius: var(--ui-radius-sm);
889
+ text-align: center;
890
+ transition: color var(--ui-transition-fast), border-color var(--ui-transition-fast);
891
+ }
892
+
893
+ .reset-btn:hover {
894
+ color: var(--ui-text-primary);
895
+ border-color: var(--ui-border-medium);
896
+ }
897
+
898
+ .shadow-box {
899
+ width: 4rem;
900
+ height: 4rem;
901
+ background: var(--ui-surface-high);
902
+ border-radius: var(--ui-radius-md);
903
+ }
904
+
905
+ .shadow-item .token-info {
906
+ align-items: center;
907
+ text-align: center;
908
+ }
909
+
910
+ .shadow-edit-btn {
911
+ all: unset;
912
+ font-size: var(--ui-font-size-xs);
913
+ color: var(--ui-text-muted);
914
+ cursor: pointer;
915
+ padding: var(--ui-space-2) var(--ui-space-8);
916
+ border-radius: var(--ui-radius-sm);
917
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
918
+ }
919
+
920
+ .shadow-edit-btn:hover {
921
+ color: var(--ui-text-accent);
922
+ background: var(--ui-surface-low);
923
+ }
924
+
925
+ .shadow-slider-label {
926
+ font-size: var(--ui-font-size-xs);
927
+ font-weight: var(--ui-font-weight-semibold);
928
+ color: var(--ui-text-tertiary);
929
+ width: 4rem;
930
+ text-align: right;
931
+ flex-shrink: 0;
932
+ }
933
+
934
+ .shadow-slider-input {
935
+ font-size: var(--ui-font-size-xs);
936
+ color: var(--ui-text-primary);
937
+ font-family: var(--ui-font-mono);
938
+ width: 2.5rem;
939
+ text-align: right;
940
+ flex-shrink: 0;
941
+ background: var(--ui-surface-lowest);
942
+ border: 1px solid var(--ui-border-subtle);
943
+ border-radius: var(--ui-radius-sm);
944
+ padding: var(--ui-space-2) var(--ui-space-4);
945
+ -moz-appearance: textfield;
946
+ appearance: textfield;
947
+ }
948
+
949
+ .shadow-slider-input::-webkit-inner-spin-button,
950
+ .shadow-slider-input::-webkit-outer-spin-button {
951
+ -webkit-appearance: none;
952
+ margin: 0;
953
+ }
954
+
955
+ .shadow-slider-input:focus {
956
+ outline: none;
957
+ border-color: var(--ui-border-medium);
958
+ }
959
+
960
+ .shadow-slider-unit {
961
+ font-size: var(--ui-font-size-xs);
962
+ color: var(--ui-text-muted);
963
+ font-family: var(--ui-font-mono);
964
+ width: 1rem;
965
+ flex-shrink: 0;
966
+ }
967
+
968
+ /* Angle dial */
969
+ .angle-dial {
970
+ cursor: pointer;
971
+ touch-action: none;
972
+ flex-shrink: 0;
973
+ }
974
+
975
+ .dial-ring {
976
+ fill: none;
977
+ stroke: var(--ui-border-subtle);
978
+ stroke-width: 1.5;
979
+ }
980
+
981
+ .dial-line {
982
+ stroke: var(--ui-text-secondary);
983
+ stroke-width: 1.5;
984
+ stroke-linecap: round;
985
+ }
986
+
987
+ .dial-handle {
988
+ fill: var(--ui-text-accent);
989
+ stroke: none;
990
+ }
991
+
992
+ .angle-dial:hover .dial-ring {
993
+ stroke: var(--ui-border-medium);
994
+ }
995
+
996
+ .angle-dial:hover .dial-handle {
997
+ fill: var(--ui-text-primary);
998
+ }
999
+
1000
+ .shadow-css-output {
1001
+ display: flex;
1002
+ flex-direction: column;
1003
+ gap: var(--ui-space-4);
1004
+ margin-top: var(--ui-space-4);
1005
+ padding-top: var(--ui-space-8);
1006
+ border-top: 1px solid var(--ui-border-faint);
1007
+ }
1008
+
1009
+ .shadow-css-output code {
1010
+ font-size: var(--ui-font-size-xs);
1011
+ color: var(--ui-text-accent);
1012
+ font-family: var(--ui-font-mono);
1013
+ word-break: break-all;
1014
+ }
1015
+
1016
+ .shadow-copy-btn {
1017
+ all: unset;
1018
+ font-size: var(--ui-font-size-xs);
1019
+ color: var(--ui-text-muted);
1020
+ cursor: pointer;
1021
+ padding: var(--ui-space-2) var(--ui-space-6);
1022
+ border: 1px solid var(--ui-border-subtle);
1023
+ border-radius: var(--ui-radius-sm);
1024
+ text-align: center;
1025
+ transition: color var(--ui-transition-fast), border-color var(--ui-transition-fast);
1026
+ }
1027
+
1028
+ .shadow-copy-btn:hover {
1029
+ color: var(--ui-text-primary);
1030
+ border-color: var(--ui-border-medium);
1031
+ }
1032
+
1033
+ /* Global shadow editor */
1034
+ .global-shadow-editor {
1035
+ position: sticky;
1036
+ top: var(--ui-space-12);
1037
+ flex-shrink: 0;
1038
+ display: flex;
1039
+ flex-direction: column;
1040
+ gap: var(--ui-space-6);
1041
+ padding: var(--ui-space-12);
1042
+ background: var(--ui-surface-lowest);
1043
+ border: 1px solid var(--ui-border-subtle);
1044
+ border-radius: var(--ui-radius-md);
1045
+ width: 18rem;
1046
+ }
1047
+
1048
+ .global-shadow-title {
1049
+ font-size: var(--ui-font-size-xs);
1050
+ font-weight: var(--ui-font-weight-semibold);
1051
+ color: var(--ui-text-secondary);
1052
+ margin: 0;
1053
+ }
1054
+
1055
+ .global-shadow-row {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ gap: var(--ui-space-8);
1059
+ }
1060
+
1061
+ .lock-btn {
1062
+ all: unset;
1063
+ cursor: pointer;
1064
+ color: var(--ui-text-muted);
1065
+ flex-shrink: 0;
1066
+ display: flex;
1067
+ align-items: center;
1068
+ padding: var(--ui-space-2);
1069
+ border-radius: var(--ui-radius-sm);
1070
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
1071
+ }
1072
+
1073
+ .lock-btn:hover {
1074
+ color: var(--ui-text-primary);
1075
+ background: var(--ui-surface-low);
1076
+ }
1077
+
1078
+ .lock-btn.unlocked {
1079
+ color: var(--ui-text-accent);
1080
+ }
1081
+
1082
+ .global-shadow-row .shadow-slider-label {
1083
+ width: 3rem;
1084
+ }
1085
+
1086
+ .global-shadow-row input[type="range"] {
1087
+ flex: 1;
1088
+ min-width: 4rem;
1089
+ accent-color: var(--ui-text-accent);
1090
+ height: 4px;
1091
+ cursor: pointer;
1092
+ }
1093
+
1094
+ .slider-track {
1095
+ flex: 1;
1096
+ min-width: 4rem;
1097
+ position: relative;
1098
+ height: 12px;
1099
+ border-radius: var(--ui-radius-sm);
1100
+ border: 1px solid var(--ui-border-subtle);
1101
+ }
1102
+
1103
+ .slider-track input[type="range"] {
1104
+ position: absolute;
1105
+ top: 0;
1106
+ left: 0;
1107
+ width: 100%;
1108
+ height: 100%;
1109
+ cursor: pointer;
1110
+ margin: 0;
1111
+ -webkit-appearance: none;
1112
+ appearance: none;
1113
+ background: transparent;
1114
+ }
1115
+
1116
+ .slider-track input[type="range"]::-webkit-slider-thumb {
1117
+ -webkit-appearance: none;
1118
+ width: 10px;
1119
+ height: 14px;
1120
+ border-radius: 2px;
1121
+ background: white;
1122
+ border: 1px solid var(--ui-border-subtle);
1123
+ box-shadow: 0 1px 3px rgba(0,0,0,0.4);
1124
+ cursor: pointer;
1125
+ }
1126
+
1127
+ .slider-track input[type="range"]::-moz-range-thumb {
1128
+ width: 10px;
1129
+ height: 14px;
1130
+ border-radius: 2px;
1131
+ background: white;
1132
+ border: 1px solid var(--ui-border-subtle);
1133
+ box-shadow: 0 1px 3px rgba(0,0,0,0.4);
1134
+ cursor: pointer;
1135
+ }
1136
+
1137
+ .slider-track input[type="range"]::-moz-range-track {
1138
+ background: transparent;
1139
+ border: none;
1140
+ }
1141
+
1142
+ .global-color-group {
1143
+ display: flex;
1144
+ gap: var(--ui-space-8);
1145
+ align-items: stretch;
1146
+ }
1147
+
1148
+ .global-color-swatch {
1149
+ width: 2rem;
1150
+ flex-shrink: 0;
1151
+ border-radius: var(--ui-radius-sm);
1152
+ border: 1px solid var(--ui-border-subtle);
1153
+ }
1154
+
1155
+ .global-color-sliders {
1156
+ flex: 1;
1157
+ display: flex;
1158
+ flex-direction: column;
1159
+ gap: var(--ui-space-6);
1160
+ }
1161
+
1162
+ .bg-picker-btn {
1163
+ all: unset;
1164
+ font-size: var(--ui-font-size-xs);
1165
+ font-family: var(--ui-font-mono);
1166
+ color: var(--ui-text-muted);
1167
+ cursor: pointer;
1168
+ padding: var(--ui-space-4) var(--ui-space-8);
1169
+ border: 1px solid var(--ui-border-subtle);
1170
+ border-radius: var(--ui-radius-md);
1171
+ background: var(--ui-surface-low);
1172
+ transition: color var(--ui-transition-fast), border-color var(--ui-transition-fast);
1173
+ }
1174
+
1175
+ .bg-picker-btn:hover {
1176
+ color: var(--ui-text-primary);
1177
+ border-color: var(--ui-border-medium);
1178
+ }
1179
+
1180
+ .bg-picker-menu {
1181
+ position: absolute;
1182
+ bottom: calc(100% + var(--ui-space-4));
1183
+ left: 0;
1184
+ width: 14rem;
1185
+ max-width: calc(100vw - 2rem);
1186
+ max-height: 24rem;
1187
+ overflow-y: auto;
1188
+ background: var(--ui-surface-low);
1189
+ border: 1px solid var(--ui-border-subtle);
1190
+ border-radius: var(--ui-radius-md);
1191
+ padding: var(--ui-space-4);
1192
+ display: flex;
1193
+ flex-direction: column;
1194
+ z-index: 10;
1195
+ }
1196
+
1197
+ .bg-group-header {
1198
+ all: unset;
1199
+ display: flex;
1200
+ justify-content: space-between;
1201
+ align-items: center;
1202
+ padding: var(--ui-space-6) var(--ui-space-8);
1203
+ font-size: var(--ui-font-size-xs);
1204
+ color: var(--ui-text-secondary);
1205
+ cursor: pointer;
1206
+ border-radius: var(--ui-radius-sm);
1207
+ transition: background var(--ui-transition-fast);
1208
+ }
1209
+
1210
+ .bg-group-header:hover {
1211
+ background: var(--ui-surface-high);
1212
+ }
1213
+
1214
+ .bg-group-arrow {
1215
+ font-size: 0.65rem;
1216
+ color: var(--ui-text-muted);
1217
+ }
1218
+
1219
+ .bg-group-colors {
1220
+ display: flex;
1221
+ flex-direction: column;
1222
+ padding-left: var(--ui-space-8);
1223
+ }
1224
+
1225
+ .bg-color-option {
1226
+ all: unset;
1227
+ display: flex;
1228
+ align-items: center;
1229
+ gap: var(--ui-space-8);
1230
+ padding: var(--ui-space-4) var(--ui-space-8);
1231
+ font-size: var(--ui-font-size-xs);
1232
+ color: var(--ui-text-tertiary);
1233
+ cursor: pointer;
1234
+ border-radius: var(--ui-radius-sm);
1235
+ transition: background var(--ui-transition-fast);
1236
+ }
1237
+
1238
+ .bg-color-option:hover {
1239
+ background: var(--ui-surface-high);
1240
+ color: var(--ui-text-primary);
1241
+ }
1242
+
1243
+ .bg-color-swatch {
1244
+ width: 1rem;
1245
+ height: 1rem;
1246
+ border-radius: var(--ui-radius-sm);
1247
+ border: 1px solid var(--ui-border-subtle);
1248
+ flex-shrink: 0;
1249
+ }
1250
+ </style>