@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,1084 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import { slide } from 'svelte/transition';
4
+ import { cubicOut, cubicIn } from 'svelte/easing';
5
+ import { resolveAliasChain } from '../lib/tokenRegistry';
6
+ import { editorState } from '../lib/editorStore';
7
+ import { formatGradientStops } from '../lib/slices/gradients';
8
+ import type { GradientToken } from '../lib/editorTypes';
9
+ import UITokenSelector from './UITokenSelector.svelte';
10
+
11
+ /** Honor prefers-reduced-motion: `t()` zeroes durations when the OS asks for
12
+ * less motion, so the detail-row enter/exit slides skip outright. Mirrors
13
+ * the pattern used in UIPaddingSelector for split↔merge transitions. */
14
+ const reduceMotion = typeof window !== 'undefined'
15
+ && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
16
+ const t = (ms: number) => (reduceMotion ? 0 : ms);
17
+
18
+ /** Tokens that match the surface/fill suffix but live in <color>-only CSS contexts
19
+ (color-mix, box-shadow color slot) where a gradient would invalidate the declaration. */
20
+ const GRADIENT_DENYLIST = new Set<string>([
21
+ '--radiobutton-default-surface',
22
+ '--radiobutton-hover-surface',
23
+ '--radiobutton-active-surface',
24
+ ]);
25
+
26
+ /** Slot kinds where a gradient is a renderable assignment. */
27
+ function acceptsGradient(name: string): boolean {
28
+ if (GRADIENT_DENYLIST.has(name)) return false;
29
+ return name.endsWith('-surface') || name.endsWith('-fill');
30
+ }
31
+
32
+ const dispatch = createEventDispatcher();
33
+
34
+ export let variable: string;
35
+ export let component: string | undefined = undefined;
36
+ export let canBeLinked: boolean = false;
37
+ export let disabled: boolean = false;
38
+ export let selectionsLocked: boolean = false;
39
+
40
+ type Category = 'palette' | 'surface' | 'border' | 'text';
41
+
42
+ const families = [
43
+ { name: 'neutral', label: 'Neutral' },
44
+ { name: 'alternate', label: 'Alternate' },
45
+ { name: 'canvas', label: 'Canvas' },
46
+ { name: 'brand', label: 'Brand' },
47
+ { name: 'accent', label: 'Accent' },
48
+ { name: 'special', label: 'Special' },
49
+ { name: 'success', label: 'Success' },
50
+ { name: 'warning', label: 'Warning' },
51
+ { name: 'info', label: 'Info' },
52
+ { name: 'danger', label: 'Danger' },
53
+ ];
54
+
55
+ const familyNames = families.map(f => f.name);
56
+
57
+ const paletteSteps = ['100', '200', '300', '400', '500', '600', '700', '800', '850', '900', '950'];
58
+
59
+ const surfaceSteps = [
60
+ { key: 'lowest', label: 'Lowest' },
61
+ { key: 'lower', label: 'Lower' },
62
+ { key: 'low', label: 'Low' },
63
+ { key: '', label: 'Default' },
64
+ { key: 'high', label: 'High' },
65
+ { key: 'higher', label: 'Higher' },
66
+ { key: 'highest', label: 'Highest' },
67
+ ];
68
+
69
+ const borderSteps = [
70
+ { key: 'faint', label: 'Faint' },
71
+ { key: 'subtle', label: 'Subtle' },
72
+ { key: '', label: 'Default' },
73
+ { key: 'medium', label: 'Medium' },
74
+ { key: 'strong', label: 'Strong' },
75
+ ];
76
+
77
+ const textSteps = [
78
+ { key: 'primary', label: 'Primary' },
79
+ { key: 'secondary', label: 'Secondary' },
80
+ { key: 'tertiary', label: 'Tertiary' },
81
+ { key: 'muted', label: 'Muted' },
82
+ { key: 'disabled', label: 'Disabled' },
83
+ ];
84
+
85
+ const surfaceStepKeys = surfaceSteps.map(s => s.key);
86
+ const borderStepKeys = borderSteps.map(s => s.key);
87
+ const textStepKeys = textSteps.map(s => s.key);
88
+
89
+ const familiesWithText = ['neutral', 'canvas', 'brand', 'accent', 'special', 'success', 'warning', 'info', 'danger'];
90
+
91
+ const allCategories: { id: Category; label: string }[] = [
92
+ { id: 'palette', label: 'Palette' },
93
+ { id: 'surface', label: 'Surface' },
94
+ { id: 'border', label: 'Border' },
95
+ { id: 'text', label: 'Text' },
96
+ ];
97
+
98
+ let selector: UITokenSelector;
99
+ let selectedFamily: string | null = null;
100
+ let selectedTab: Category = 'palette';
101
+
102
+ /** Compass-rose layout for the orientation grid. Angles follow the CSS
103
+ * linear-gradient convention (0° points up, 90° points right). */
104
+ const directionGrid: { angle: number; glyph: string; col: number; row: number; label: string }[] = [
105
+ { angle: 315, glyph: '↖', col: 1, row: 1, label: 'top-left' },
106
+ { angle: 0, glyph: '↑', col: 2, row: 1, label: 'top' },
107
+ { angle: 45, glyph: '↗', col: 3, row: 1, label: 'top-right' },
108
+ { angle: 270, glyph: '←', col: 1, row: 2, label: 'left' },
109
+ { angle: 90, glyph: '→', col: 3, row: 2, label: 'right' },
110
+ { angle: 225, glyph: '↙', col: 1, row: 3, label: 'bottom-left' },
111
+ { angle: 180, glyph: '↓', col: 2, row: 3, label: 'bottom' },
112
+ { angle: 135, glyph: '↘', col: 3, row: 3, label: 'bottom-right' },
113
+ ];
114
+
115
+ /** Numeric input value. Kept in sync with chosenAngle/token-default by the
116
+ * reactive block below; user edits flow back through `applyOrientation`. */
117
+ let angleInput: number = 0;
118
+
119
+ let chosenCategory: Category | null = null;
120
+ let chosenFamily: string | null = null;
121
+ let chosenStep: string | null = null;
122
+ let chosenNone: boolean = false;
123
+ let chosenGradient: string | null = null;
124
+ /** Per-slot angle override on the chosen linear gradient. Null means
125
+ * "no override" — the slot writes `var(--gradient-N)` and inherits the
126
+ * token's natural angle. Non-null means the slot writes a materialized
127
+ * `linear-gradient(<angle>, <token's stops>)` so the angle is locally
128
+ * pinned while stop colors keep flowing from the token's `var()` refs. */
129
+ let chosenAngle: number | null = null;
130
+ let opacity: number = 100;
131
+ let selfDefaultHex: string = '';
132
+
133
+ $: gradientsAllowed = acceptsGradient(variable);
134
+ $: gradientTokens = $editorState.gradients.tokens;
135
+
136
+ function captureSelfDefault() {
137
+ const root = document.documentElement;
138
+ const inline = root.style.getPropertyValue(variable);
139
+ if (inline) root.style.removeProperty(variable);
140
+ selfDefaultHex = rgbToHex(getComputedStyle(root).getPropertyValue(variable).trim());
141
+ if (inline) root.style.setProperty(variable, inline);
142
+ }
143
+
144
+ function previewBg(category: Category, family: string, step: string): string {
145
+ const varName = getVarName(category, family, step);
146
+ if (varName === variable) return selfDefaultHex || `var(${varName})`;
147
+ return `var(${varName})`;
148
+ }
149
+
150
+ function rgbToHex(value: string): string {
151
+ const m = value.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
152
+ if (!m) return value;
153
+ const toHex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');
154
+ return `#${toHex(parseInt(m[1]))}${toHex(parseInt(m[2]))}${toHex(parseInt(m[3]))}`;
155
+ }
156
+
157
+ function getVarName(category: Category, family: string, stepKey: string): string {
158
+ switch (category) {
159
+ case 'palette':
160
+ return `--color-${family}-${stepKey}`;
161
+ case 'surface':
162
+ return stepKey ? `--surface-${family}-${stepKey}` : `--surface-${family}`;
163
+ case 'border':
164
+ return stepKey ? `--border-${family}-${stepKey}` : `--border-${family}`;
165
+ case 'text':
166
+ if (family === 'neutral') return `--text-${stepKey}`;
167
+ if (stepKey === 'primary') return `--text-${family}`;
168
+ return `--text-${family}-${stepKey}`;
169
+ }
170
+ }
171
+
172
+ function parseTextVarName(varName: string): { family: string; step: string } | null {
173
+ if (varName === '--text-primary') return { family: 'neutral', step: 'primary' };
174
+ if (varName === '--text-secondary') return { family: 'neutral', step: 'secondary' };
175
+ if (varName === '--text-tertiary') return { family: 'neutral', step: 'tertiary' };
176
+ if (varName === '--text-muted') return { family: 'neutral', step: 'muted' };
177
+ if (varName === '--text-disabled') return { family: 'neutral', step: 'disabled' };
178
+ const m = varName.match(/^--text-([a-z]+)(?:-([a-z]+))?$/);
179
+ if (m) {
180
+ const fam = m[1];
181
+ const hier = m[2] || 'primary';
182
+ if (familiesWithText.includes(fam) && fam !== 'neutral' && textStepKeys.includes(hier)) {
183
+ return { family: fam, step: hier };
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+
189
+ function parseRef(value: string): { category: Category; family: string; step: string } | null {
190
+ const varMatch = value.match(/var\((--[a-z0-9-]+)\)/);
191
+ if (!varMatch) return null;
192
+ const varName = varMatch[1];
193
+
194
+ const paletteM = varName.match(/^--color-([a-z]+)-(\d{3})$/);
195
+ if (paletteM && familyNames.includes(paletteM[1]) && paletteSteps.includes(paletteM[2])) {
196
+ return { category: 'palette', family: paletteM[1], step: paletteM[2] };
197
+ }
198
+
199
+ const surfaceM = varName.match(/^--surface-([a-z]+)(?:-([a-z]+))?$/);
200
+ if (surfaceM && familyNames.includes(surfaceM[1])) {
201
+ const step = surfaceM[2] || '';
202
+ if (surfaceStepKeys.includes(step)) {
203
+ return { category: 'surface', family: surfaceM[1], step };
204
+ }
205
+ }
206
+
207
+ const borderM = varName.match(/^--border-([a-z]+)(?:-([a-z]+))?$/);
208
+ if (borderM && familyNames.includes(borderM[1])) {
209
+ const step = borderM[2] || '';
210
+ if (borderStepKeys.includes(step)) {
211
+ return { category: 'border', family: borderM[1], step };
212
+ }
213
+ }
214
+
215
+ const textResult = parseTextVarName(varName);
216
+ if (textResult) {
217
+ return { category: 'text', ...textResult };
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ function parseOpacity(raw: string): { inner: string; opacity: number } | null {
224
+ const m = raw.match(/^color-mix\(in srgb,\s*(var\(--[a-z0-9-]+\))\s+(\d+)%,\s*transparent\)$/);
225
+ if (!m) return null;
226
+ return { inner: m[1], opacity: parseInt(m[2]) };
227
+ }
228
+
229
+ function buildValue(varName: string): string | null {
230
+ if (varName === variable && opacity >= 100) return null;
231
+ if (opacity >= 100) return varName;
232
+ return `color-mix(in srgb, var(${varName}) ${opacity}%, transparent)`;
233
+ }
234
+
235
+ function applyOpacity() {
236
+ opacity = Math.max(0, Math.min(100, Math.round(opacity)));
237
+ if (chosenCategory === null || chosenFamily === null || chosenStep === null) return;
238
+ const varName = getVarName(chosenCategory, chosenFamily, chosenStep);
239
+ selector.writeOverride(buildValue(varName));
240
+ dispatch('change');
241
+ }
242
+
243
+ /** Apply (or clear) a per-slot angle override on the chosen linear gradient.
244
+ * When the requested angle matches the gradient token's own natural angle
245
+ * we drop back to the cleaner `var(--gradient-N)` form — that way dialing
246
+ * back to default and pressing the reset button arrive at the same value. */
247
+ function applyOrientation(nextAngle: number) {
248
+ if (chosenGradient === null) return;
249
+ const token = getGradientToken(chosenGradient);
250
+ if (!token || token.type !== 'linear') return;
251
+ const normalized = ((Math.round(nextAngle) % 360) + 360) % 360;
252
+ chosenAngle = normalized;
253
+ if (normalized === token.angle) {
254
+ selector.writeOverride(chosenGradient);
255
+ } else {
256
+ selector.writeOverride(materializeGradient(token, normalized));
257
+ }
258
+ dispatch('change');
259
+ }
260
+
261
+ function resetOrientation() {
262
+ if (chosenGradient === null) return;
263
+ chosenAngle = null;
264
+ selector.writeOverride(chosenGradient);
265
+ dispatch('change');
266
+ }
267
+
268
+ function isGradientToken(name: string): boolean {
269
+ return gradientTokens.some((g) => g.variable === name);
270
+ }
271
+
272
+ function getGradientToken(name: string): GradientToken | undefined {
273
+ return gradientTokens.find((g) => g.variable === name);
274
+ }
275
+
276
+ /** Normalize whitespace so a slot's stored linear-gradient string compares
277
+ * equal to the canonical `formatGradientStops` output regardless of how it
278
+ * was last serialized through the DOM / persistence layer. */
279
+ function normStops(s: string): string {
280
+ return s.replace(/\s+/g, ' ').trim();
281
+ }
282
+
283
+ /** Match a materialized linear-gradient back to its source gradient token by
284
+ * comparing the inlined stop list. We only need to identify linear tokens
285
+ * here because angle override doesn't apply to radial. */
286
+ function identifyMaterializedGradient(raw: string): { variable: string; angle: number } | null {
287
+ const m = raw.match(/^linear-gradient\(\s*([\d.]+)deg\s*,\s*(.+)\)\s*$/);
288
+ if (!m) return null;
289
+ const angle = parseFloat(m[1]);
290
+ const stops = normStops(m[2]);
291
+ for (const t of gradientTokens) {
292
+ if (t.type !== 'linear') continue;
293
+ if (normStops(formatGradientStops(t)) === stops) {
294
+ return { variable: t.variable, angle };
295
+ }
296
+ }
297
+ return null;
298
+ }
299
+
300
+ function materializeGradient(token: GradientToken, angle: number): string {
301
+ return `linear-gradient(${angle}deg, ${formatGradientStops(token)})`;
302
+ }
303
+
304
+ function initFromCurrent() {
305
+ const raw = document.documentElement.style.getPropertyValue(variable).trim();
306
+
307
+ if (raw === 'transparent') {
308
+ chosenNone = true;
309
+ chosenCategory = null;
310
+ chosenFamily = null;
311
+ chosenStep = null;
312
+ chosenGradient = null;
313
+ chosenAngle = null;
314
+ opacity = 100;
315
+ return;
316
+ }
317
+
318
+ chosenNone = false;
319
+
320
+ if (gradientsAllowed) {
321
+ const gradMatch = raw.match(/^var\((--[a-z0-9-]+)\)$/);
322
+ if (gradMatch && isGradientToken(gradMatch[1])) {
323
+ chosenGradient = gradMatch[1];
324
+ chosenCategory = null;
325
+ chosenFamily = null;
326
+ chosenStep = null;
327
+ chosenAngle = null;
328
+ opacity = 100;
329
+ return;
330
+ }
331
+ // Materialized form: `linear-gradient(<angle>, <token's stops>)`
332
+ // — an angle override pinned locally while stops still carry var() refs
333
+ // back to palette tokens.
334
+ const materialized = identifyMaterializedGradient(raw);
335
+ if (materialized) {
336
+ chosenGradient = materialized.variable;
337
+ chosenCategory = null;
338
+ chosenFamily = null;
339
+ chosenStep = null;
340
+ chosenAngle = materialized.angle;
341
+ opacity = 100;
342
+ return;
343
+ }
344
+ }
345
+ chosenGradient = null;
346
+ chosenAngle = null;
347
+
348
+ const opacityParsed = parseOpacity(raw);
349
+ if (opacityParsed) {
350
+ const parsed = parseRef(opacityParsed.inner);
351
+ if (parsed) {
352
+ chosenCategory = parsed.category;
353
+ chosenFamily = parsed.family;
354
+ chosenStep = parsed.step;
355
+ opacity = opacityParsed.opacity;
356
+ return;
357
+ }
358
+ }
359
+
360
+ const parsed = raw ? parseRef(raw) : null;
361
+ if (parsed) {
362
+ chosenCategory = parsed.category;
363
+ chosenFamily = parsed.family;
364
+ chosenStep = parsed.step;
365
+ opacity = 100;
366
+ return;
367
+ }
368
+ for (const alias of resolveAliasChain(variable)) {
369
+ const aliasParsed = parseRef(`var(${alias})`);
370
+ if (aliasParsed) {
371
+ chosenCategory = aliasParsed.category;
372
+ chosenFamily = aliasParsed.family;
373
+ chosenStep = aliasParsed.step;
374
+ opacity = 100;
375
+ return;
376
+ }
377
+ }
378
+ chosenCategory = null;
379
+ chosenFamily = null;
380
+ chosenStep = null;
381
+ opacity = 100;
382
+ }
383
+
384
+ function handleReset() {
385
+ opacity = 100;
386
+ chosenNone = false;
387
+ initFromCurrent();
388
+ selectedFamily = null;
389
+ dispatch('change');
390
+ }
391
+
392
+ function handleClose() {
393
+ selectedFamily = null;
394
+ }
395
+
396
+ function selectFamily(name: string) {
397
+ selectedFamily = name;
398
+ if (name === chosenFamily && chosenCategory) {
399
+ selectedTab = chosenCategory;
400
+ }
401
+ }
402
+
403
+ function backToFamilies() {
404
+ selectedFamily = null;
405
+ }
406
+
407
+ function selectNone(close: () => void) {
408
+ chosenNone = true;
409
+ chosenCategory = null;
410
+ chosenFamily = null;
411
+ chosenStep = null;
412
+ chosenGradient = null;
413
+ chosenAngle = null;
414
+ opacity = 100;
415
+ selector.writeOverride('transparent');
416
+ selectedFamily = null;
417
+ close();
418
+ dispatch('change');
419
+ }
420
+
421
+ function selectSwatch(category: Category, step: string, close: () => void) {
422
+ const varName = getVarName(category, selectedFamily!, step);
423
+ chosenNone = false;
424
+ chosenGradient = null;
425
+ chosenAngle = null;
426
+ chosenCategory = category;
427
+ chosenFamily = selectedFamily;
428
+ chosenStep = step;
429
+ selector.writeOverride(buildValue(varName));
430
+ selectedFamily = null;
431
+ close();
432
+ dispatch('change');
433
+ }
434
+
435
+ // Picking any gradient is a fresh start: any prior angle override is
436
+ // discarded and the slot adopts the new token's natural orientation by
437
+ // writing `var(--gradient-N)`. The orientation control then displays the
438
+ // token's default angle but with no local override active.
439
+ function selectGradient(gradientVar: string, close: () => void) {
440
+ chosenNone = false;
441
+ chosenCategory = null;
442
+ chosenFamily = null;
443
+ chosenStep = null;
444
+ chosenGradient = gradientVar;
445
+ chosenAngle = null;
446
+ opacity = 100;
447
+ selector.writeOverride(gradientVar);
448
+ selectedFamily = null;
449
+ close();
450
+ dispatch('change');
451
+ }
452
+
453
+ // Re-derive trigger state when the bound `variable` changes (e.g. when a
454
+ // VariantGroup tabs view reuses the same selector instance across states).
455
+ // The wrapper UITokenSelector forwards `var-change` only for the currently
456
+ // bound variable, so prop swaps wouldn't otherwise refresh `chosenCategory`
457
+ // / `chosenFamily` / `chosenStep` and the meta label drifts from the swatch.
458
+ let lastSeenVariable: string | null = null;
459
+ $: if (variable !== lastSeenVariable) {
460
+ lastSeenVariable = variable;
461
+ initFromCurrent();
462
+ captureSelfDefault();
463
+ }
464
+
465
+ $: chosenGradientToken = chosenGradient ? getGradientToken(chosenGradient) : undefined;
466
+ $: isLinearGradientChosen = !!chosenGradientToken && chosenGradientToken.type === 'linear';
467
+ $: effectiveAngle = chosenGradientToken
468
+ ? (chosenAngle ?? chosenGradientToken.angle)
469
+ : 0;
470
+ $: angleInput = effectiveAngle;
471
+
472
+ $: triggerMeta = chosenNone
473
+ ? 'none'
474
+ : chosenGradient
475
+ ? chosenGradient.replace(/^--/, '') + (chosenAngle !== null ? ` (${effectiveAngle}°)` : '')
476
+ : (chosenCategory && chosenFamily && chosenStep !== null
477
+ ? getVarName(chosenCategory, chosenFamily, chosenStep).replace(/^--/, '') + (opacity < 100 ? ` (${opacity}%)` : '')
478
+ : '');
479
+
480
+ $: availableTabs = selectedFamily
481
+ ? allCategories.filter(c => c.id !== 'text' || familiesWithText.includes(selectedFamily!))
482
+ : allCategories;
483
+
484
+ $: if (selectedFamily && !availableTabs.find(t => t.id === selectedTab)) {
485
+ selectedTab = 'palette';
486
+ }
487
+ </script>
488
+
489
+ <UITokenSelector
490
+ bind:this={selector}
491
+ {variable}
492
+ {component}
493
+ {canBeLinked}
494
+ {disabled}
495
+ {selectionsLocked}
496
+ dropdownMinWidth="14rem"
497
+ dropdownMaxWidth="calc(100vw - 2rem)"
498
+ hideDefaultHeader={!!selectedFamily}
499
+ on:reset={handleReset}
500
+ on:close={handleClose}
501
+ on:var-change={initFromCurrent}
502
+ >
503
+ <div slot="trigger-preview" class="swatch-wrap">
504
+ <div class="swatch" style="background: var({variable});"></div>
505
+ </div>
506
+ <div slot="subheader" class="opacity-control" class:hidden={chosenGradient !== null}>
507
+ <span class="opacity-label">opacity</span>
508
+ <input type="range" min="0" max="100" bind:value={opacity} class="opacity-slider" on:input={applyOpacity} />
509
+ <input type="number" min="0" max="100" bind:value={opacity} class="opacity-input" on:change={applyOpacity} />
510
+ <span class="opacity-unit">%</span>
511
+ </div>
512
+ <svelte:fragment slot="trigger-meta">{triggerMeta}</svelte:fragment>
513
+
514
+ <svelte:fragment let:close>
515
+ {#if selectedFamily === null}
516
+ <div class="family-list">
517
+ <button class="family-item" class:active={chosenNone} on:click={() => selectNone(close)}>
518
+ <div class="family-swatches">
519
+ <div class="none-swatch"></div>
520
+ </div>
521
+ <span class="family-label">None</span>
522
+ </button>
523
+ {#each families as fam}
524
+ <button class="family-item" class:active={!chosenNone && chosenFamily === fam.name} on:click={() => selectFamily(fam.name)}>
525
+ <div class="family-swatches">
526
+ <div class="mini-swatch" style="background: var(--color-{fam.name}-300);"></div>
527
+ <div class="mini-swatch" style="background: var(--color-{fam.name}-500);"></div>
528
+ <div class="mini-swatch" style="background: var(--color-{fam.name}-700);"></div>
529
+ </div>
530
+ <span class="family-label">{fam.label}</span>
531
+ <i class="fas fa-chevron-right family-arrow"></i>
532
+ </button>
533
+ {/each}
534
+ {#if gradientsAllowed && gradientTokens.length > 0}
535
+ <div class="family-divider">Gradients</div>
536
+ {#each gradientTokens as g}
537
+ <button class="family-item" class:active={chosenGradient === g.variable} on:click={() => selectGradient(g.variable, close)}>
538
+ <div class="family-swatches">
539
+ <div class="gradient-swatch" style="background: var({g.variable});"></div>
540
+ </div>
541
+ <span class="family-label">Gradient {g.variable.replace(/^--gradient-/, '')}</span>
542
+ </button>
543
+ {/each}
544
+ {/if}
545
+ </div>
546
+ {:else}
547
+ <button class="dropdown-back" on:click={backToFamilies}>
548
+ <i class="fas fa-chevron-left"></i>
549
+ <span>{families.find(f => f.name === selectedFamily)?.label}</span>
550
+ </button>
551
+
552
+ <div class="tab-bar">
553
+ {#each availableTabs as tab}
554
+ <button
555
+ class="tab-btn"
556
+ class:selected={selectedTab === tab.id}
557
+ class:assigned={chosenCategory === tab.id && chosenFamily === selectedFamily}
558
+ on:click={() => selectedTab = tab.id}
559
+ >{tab.label}</button>
560
+ {/each}
561
+ </div>
562
+
563
+ {#if selectedTab === 'palette'}
564
+ <div class="step-grid">
565
+ {#each paletteSteps as step}
566
+ <button
567
+ class="step-item"
568
+ class:active={chosenCategory === 'palette' && chosenFamily === selectedFamily && chosenStep === step}
569
+ on:click={() => selectSwatch('palette', step, close)}
570
+ >
571
+ <div class="step-swatch" style="background: var(--color-{selectedFamily}-{step});"></div>
572
+ <span class="step-label">{step}</span>
573
+ </button>
574
+ {/each}
575
+ </div>
576
+ {:else if selectedTab === 'surface'}
577
+ <div class="step-grid">
578
+ {#each surfaceSteps as step}
579
+ <button
580
+ class="step-item"
581
+ class:active={chosenCategory === 'surface' && chosenFamily === selectedFamily && chosenStep === step.key}
582
+ on:click={() => selectSwatch('surface', step.key, close)}
583
+ >
584
+ <div class="step-swatch" style="background: {previewBg('surface', selectedFamily, step.key)};"></div>
585
+ <span class="step-label">{step.label}</span>
586
+ </button>
587
+ {/each}
588
+ </div>
589
+ {:else if selectedTab === 'border'}
590
+ <div class="step-grid">
591
+ {#each borderSteps as step}
592
+ <button
593
+ class="step-item"
594
+ class:active={chosenCategory === 'border' && chosenFamily === selectedFamily && chosenStep === step.key}
595
+ on:click={() => selectSwatch('border', step.key, close)}
596
+ >
597
+ <div class="step-swatch" style="background: {previewBg('border', selectedFamily, step.key)};"></div>
598
+ <span class="step-label">{step.label}</span>
599
+ </button>
600
+ {/each}
601
+ </div>
602
+ {:else if selectedTab === 'text'}
603
+ <div class="step-grid">
604
+ {#each textSteps as step}
605
+ <button
606
+ class="step-item"
607
+ class:active={chosenCategory === 'text' && chosenFamily === selectedFamily && chosenStep === step.key}
608
+ on:click={() => selectSwatch('text', step.key, close)}
609
+ >
610
+ <div class="step-swatch" style="background: {previewBg('text', selectedFamily, step.key)};"></div>
611
+ <span class="step-label">{step.label}</span>
612
+ </button>
613
+ {/each}
614
+ </div>
615
+ {/if}
616
+ {/if}
617
+ </svelte:fragment>
618
+ </UITokenSelector>
619
+
620
+ <!--
621
+ Inline orientation row. When a linear gradient is the chosen value the
622
+ palette row "grows" — UITokenSelector occupies row 1 (cols 2-3 of the
623
+ parent token-row's subgrid via its default `grid-column: span 2`); this
624
+ block lands on row 2, also cols 2-3, sitting directly under the swatch
625
+ trigger. Same pattern padding-sides uses to expand into row 2 of the
626
+ parent token-row, just lighter (no link/merge header chrome).
627
+ -->
628
+ {#if isLinearGradientChosen}
629
+ <div
630
+ class="palette-detail-row"
631
+ in:slide|local={{ duration: t(280), delay: t(60), easing: cubicOut }}
632
+ out:slide|local={{ duration: t(220), easing: cubicIn }}
633
+ >
634
+ <span class="detail-label">orientation</span>
635
+ <div class="orientation-body">
636
+ <div class="dir-grid">
637
+ {#each directionGrid as d}
638
+ <button
639
+ type="button"
640
+ class="dir-btn"
641
+ class:active={d.angle === ((effectiveAngle % 360) + 360) % 360}
642
+ style="grid-column: {d.col}; grid-row: {d.row};"
643
+ on:click={() => applyOrientation(d.angle)}
644
+ title="{d.label} ({d.angle}°)"
645
+ >{d.glyph}</button>
646
+ {/each}
647
+ </div>
648
+ <div class="angle-input-wrap">
649
+ <input
650
+ type="number"
651
+ min="0"
652
+ max="359"
653
+ class="angle-input"
654
+ bind:value={angleInput}
655
+ on:change={() => applyOrientation(angleInput)}
656
+ />
657
+ <span class="angle-unit">°</span>
658
+ </div>
659
+ <button
660
+ type="button"
661
+ class="orientation-reset"
662
+ class:active={chosenAngle !== null}
663
+ on:click={resetOrientation}
664
+ disabled={chosenAngle === null}
665
+ title="Reset to gradient default"
666
+ >
667
+ <i class="fas fa-rotate-left"></i>
668
+ </button>
669
+ </div>
670
+ </div>
671
+ {/if}
672
+
673
+ <style>
674
+ .swatch-wrap {
675
+ align-self: stretch;
676
+ flex: 1;
677
+ border-radius: var(--ui-radius-sm);
678
+ border: 1px solid var(--ui-border-faint);
679
+ background-image: linear-gradient(45deg, var(--ui-border-subtle) 25%, transparent 25%),
680
+ linear-gradient(-45deg, var(--ui-border-subtle) 25%, transparent 25%),
681
+ linear-gradient(45deg, transparent 75%, var(--ui-border-subtle) 75%),
682
+ linear-gradient(-45deg, transparent 75%, var(--ui-border-subtle) 75%);
683
+ background-size: 8px 8px;
684
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0;
685
+ overflow: hidden;
686
+ }
687
+
688
+ .swatch {
689
+ width: 100%;
690
+ height: 100%;
691
+ }
692
+
693
+ .opacity-control {
694
+ display: flex;
695
+ align-items: center;
696
+ gap: var(--ui-space-6);
697
+ padding: var(--ui-space-6) var(--ui-space-8);
698
+ border-bottom: 1px solid var(--ui-border-faint);
699
+ }
700
+
701
+ .opacity-control.hidden {
702
+ display: none;
703
+ }
704
+
705
+ .opacity-label {
706
+ font-size: var(--ui-font-size-xs);
707
+ color: var(--ui-text-secondary);
708
+ flex-shrink: 0;
709
+ }
710
+
711
+ .opacity-slider {
712
+ flex: 1;
713
+ height: 4px;
714
+ -webkit-appearance: none;
715
+ appearance: none;
716
+ background: var(--ui-border-default);
717
+ border-radius: 2px;
718
+ outline: none;
719
+ cursor: pointer;
720
+ }
721
+
722
+ .opacity-slider::-webkit-slider-thumb {
723
+ -webkit-appearance: none;
724
+ width: 12px;
725
+ height: 12px;
726
+ border-radius: 50%;
727
+ background: var(--ui-text-primary);
728
+ cursor: pointer;
729
+ }
730
+
731
+ .opacity-input {
732
+ width: 3rem;
733
+ padding: var(--ui-space-2) var(--ui-space-4);
734
+ background: var(--ui-surface-lowest);
735
+ border: 1px solid var(--ui-border-subtle);
736
+ border-radius: var(--ui-radius-sm);
737
+ color: var(--ui-text-primary);
738
+ font-size: var(--ui-font-size-xs);
739
+ font-family: var(--ui-font-mono);
740
+ text-align: right;
741
+ -moz-appearance: textfield;
742
+ }
743
+
744
+ .opacity-input::-webkit-inner-spin-button,
745
+ .opacity-input::-webkit-outer-spin-button {
746
+ -webkit-appearance: none;
747
+ margin: 0;
748
+ }
749
+
750
+ .opacity-unit {
751
+ font-size: var(--ui-font-size-xs);
752
+ color: var(--ui-text-muted);
753
+ }
754
+
755
+ .dropdown-back {
756
+ display: flex;
757
+ align-items: center;
758
+ gap: var(--ui-space-6);
759
+ width: 100%;
760
+ padding: var(--ui-space-8) var(--ui-space-10);
761
+ background: none;
762
+ border: none;
763
+ border-bottom: 1px solid var(--ui-border-faint);
764
+ color: var(--ui-text-secondary);
765
+ font-size: var(--ui-font-size-sm);
766
+ font-weight: var(--ui-font-weight-medium);
767
+ cursor: pointer;
768
+ transition: background var(--ui-transition-fast);
769
+ }
770
+
771
+ .dropdown-back:hover {
772
+ background: var(--ui-hover);
773
+ }
774
+
775
+ .dropdown-back i {
776
+ font-size: 0.5rem;
777
+ }
778
+
779
+ .tab-bar {
780
+ display: flex;
781
+ border-bottom: 1px solid var(--ui-border-faint);
782
+ }
783
+
784
+ .tab-btn {
785
+ flex: 1;
786
+ padding: var(--ui-space-4) var(--ui-space-4);
787
+ background: none;
788
+ border: 1px solid transparent;
789
+ border-radius: 0;
790
+ color: var(--ui-text-muted);
791
+ font-size: var(--ui-font-size-xs);
792
+ font-weight: var(--ui-font-weight-medium);
793
+ cursor: pointer;
794
+ transition: all var(--ui-transition-fast);
795
+ text-align: center;
796
+ }
797
+
798
+ .tab-btn:hover {
799
+ color: var(--ui-text-secondary);
800
+ background: var(--ui-hover);
801
+ }
802
+
803
+ .tab-btn.assigned {
804
+ border-color: var(--ui-border-default);
805
+ }
806
+
807
+ .tab-btn.selected {
808
+ color: var(--ui-text-primary);
809
+ box-shadow: inset 0 -2px 0 var(--ui-text-accent);
810
+ background: var(--ui-hover);
811
+ }
812
+
813
+ .family-list {
814
+ display: flex;
815
+ flex-direction: column;
816
+ max-height: 20rem;
817
+ overflow-y: auto;
818
+ }
819
+
820
+ .family-item {
821
+ display: flex;
822
+ align-items: center;
823
+ gap: var(--ui-space-8);
824
+ width: 100%;
825
+ padding: var(--ui-space-6) var(--ui-space-10);
826
+ background: none;
827
+ border: none;
828
+ cursor: pointer;
829
+ transition: background var(--ui-transition-fast);
830
+ }
831
+
832
+ .family-item:hover {
833
+ background: var(--ui-hover);
834
+ }
835
+
836
+ .family-item.active {
837
+ background: var(--ui-hover-high);
838
+ box-shadow: inset 3px 0 0 var(--ui-text-accent);
839
+ }
840
+
841
+ .family-item.active .family-label {
842
+ color: var(--ui-text-accent);
843
+ }
844
+
845
+ .family-swatches {
846
+ display: flex;
847
+ gap: 2px;
848
+ }
849
+
850
+ .mini-swatch {
851
+ width: 0.75rem;
852
+ height: 0.75rem;
853
+ border-radius: 2px;
854
+ }
855
+
856
+ .none-swatch {
857
+ width: 2.5rem;
858
+ height: 0.75rem;
859
+ border-radius: 2px;
860
+ border: 1px solid var(--ui-border-subtle);
861
+ position: relative;
862
+ overflow: hidden;
863
+ }
864
+
865
+ .gradient-swatch {
866
+ width: 2.5rem;
867
+ height: 0.75rem;
868
+ border-radius: 2px;
869
+ border: 1px solid var(--ui-border-subtle);
870
+ }
871
+
872
+ .family-divider {
873
+ margin-top: var(--ui-space-6);
874
+ padding: var(--ui-space-4) var(--ui-space-8);
875
+ font-size: var(--ui-font-size-xs);
876
+ font-family: var(--ui-font-mono);
877
+ color: var(--ui-text-tertiary);
878
+ text-transform: uppercase;
879
+ letter-spacing: 0.04em;
880
+ border-top: 1px solid var(--ui-border-faint);
881
+ }
882
+
883
+ .none-swatch::after {
884
+ content: '';
885
+ position: absolute;
886
+ top: -1px;
887
+ left: -1px;
888
+ right: -1px;
889
+ bottom: -1px;
890
+ background: repeating-linear-gradient(
891
+ -45deg,
892
+ transparent,
893
+ transparent 3px,
894
+ var(--ui-border-subtle) 3px,
895
+ var(--ui-border-subtle) 4px
896
+ );
897
+ }
898
+
899
+ .family-label {
900
+ flex: 1;
901
+ font-size: var(--ui-font-size-sm);
902
+ color: var(--ui-text-primary);
903
+ text-align: left;
904
+ }
905
+
906
+ .family-arrow {
907
+ font-size: 0.5rem;
908
+ color: var(--ui-text-muted);
909
+ }
910
+
911
+ .step-grid {
912
+ display: grid;
913
+ grid-template-columns: repeat(4, 1fr);
914
+ gap: var(--ui-space-4);
915
+ padding: var(--ui-space-8);
916
+ }
917
+
918
+ .step-item {
919
+ display: flex;
920
+ flex-direction: column;
921
+ align-items: center;
922
+ gap: var(--ui-space-2);
923
+ padding: var(--ui-space-4);
924
+ background: none;
925
+ border: 1px solid transparent;
926
+ border-radius: var(--ui-radius-sm);
927
+ cursor: pointer;
928
+ transition: all var(--ui-transition-fast);
929
+ }
930
+
931
+ .step-item:hover {
932
+ background: var(--ui-hover);
933
+ border-color: var(--ui-border-default);
934
+ }
935
+
936
+ .step-item.active {
937
+ border-color: var(--ui-text-accent);
938
+ border-width: 2px;
939
+ background: var(--ui-hover-high);
940
+ padding: 3px;
941
+ }
942
+
943
+ .step-item.active .step-label {
944
+ color: var(--ui-text-accent);
945
+ font-weight: var(--ui-font-weight-semibold);
946
+ }
947
+
948
+ .step-swatch {
949
+ width: 2rem;
950
+ height: 1.5rem;
951
+ border-radius: var(--ui-radius-sm);
952
+ border: 1px solid var(--ui-border-faint);
953
+ }
954
+
955
+ .step-label {
956
+ font-size: var(--ui-font-size-xs);
957
+ color: var(--ui-text-secondary);
958
+ font-family: var(--ui-font-mono);
959
+ }
960
+
961
+ /* Inline detail row, sibling of the UITokenSelector inside the parent
962
+ token-row's 3-col subgrid. Lands in cols 2-3 of row 2, directly under
963
+ the swatch trigger — same column anchoring the value/contexts strip
964
+ uses, so the orientation block reads as a continuation of the row. */
965
+ .palette-detail-row {
966
+ grid-column: 2 / -1;
967
+ display: flex;
968
+ align-items: center;
969
+ gap: var(--ui-space-8);
970
+ padding-top: var(--ui-space-4);
971
+ min-width: 0;
972
+ }
973
+
974
+ .detail-label {
975
+ font-size: var(--ui-font-size-xs);
976
+ color: var(--ui-text-secondary);
977
+ flex-shrink: 0;
978
+ }
979
+
980
+ .orientation-reset {
981
+ display: inline-flex;
982
+ align-items: center;
983
+ justify-content: center;
984
+ width: 1.25rem;
985
+ height: 1.25rem;
986
+ padding: 0;
987
+ background: none;
988
+ border: 1px solid transparent;
989
+ border-radius: var(--ui-radius-sm);
990
+ color: var(--ui-text-muted);
991
+ font-size: 0.625rem;
992
+ cursor: pointer;
993
+ transition: all var(--ui-transition-fast);
994
+ flex-shrink: 0;
995
+ }
996
+
997
+ .orientation-reset.active {
998
+ color: var(--ui-link-broken, var(--ui-text-secondary));
999
+ border-color: var(--ui-border-subtle);
1000
+ }
1001
+
1002
+ .orientation-reset:hover:not(:disabled) {
1003
+ background: var(--ui-hover);
1004
+ color: var(--ui-text-primary);
1005
+ }
1006
+
1007
+ .orientation-reset:disabled {
1008
+ cursor: default;
1009
+ opacity: 0.4;
1010
+ }
1011
+
1012
+ .orientation-body {
1013
+ display: flex;
1014
+ align-items: center;
1015
+ gap: var(--ui-space-8);
1016
+ flex: 1;
1017
+ min-width: 0;
1018
+ }
1019
+
1020
+ .dir-grid {
1021
+ display: grid;
1022
+ grid-template-columns: repeat(3, 1.25rem);
1023
+ grid-template-rows: repeat(3, 1.25rem);
1024
+ gap: 2px;
1025
+ flex-shrink: 0;
1026
+ }
1027
+
1028
+ .dir-btn {
1029
+ display: inline-flex;
1030
+ align-items: center;
1031
+ justify-content: center;
1032
+ padding: 0;
1033
+ background: var(--ui-surface-lowest);
1034
+ border: 1px solid var(--ui-border-subtle);
1035
+ border-radius: var(--ui-radius-sm);
1036
+ color: var(--ui-text-secondary);
1037
+ font-size: 0.875rem;
1038
+ line-height: 1;
1039
+ cursor: pointer;
1040
+ transition: all var(--ui-transition-fast);
1041
+ }
1042
+
1043
+ .dir-btn:hover {
1044
+ background: var(--ui-hover);
1045
+ border-color: var(--ui-border-default);
1046
+ color: var(--ui-text-primary);
1047
+ }
1048
+
1049
+ .dir-btn.active {
1050
+ background: var(--ui-hover-high);
1051
+ border-color: var(--ui-text-accent);
1052
+ color: var(--ui-text-accent);
1053
+ }
1054
+
1055
+ .angle-input-wrap {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ gap: var(--ui-space-2);
1059
+ }
1060
+
1061
+ .angle-input {
1062
+ width: 3rem;
1063
+ padding: var(--ui-space-2) var(--ui-space-4);
1064
+ background: var(--ui-surface-lowest);
1065
+ border: 1px solid var(--ui-border-subtle);
1066
+ border-radius: var(--ui-radius-sm);
1067
+ color: var(--ui-text-primary);
1068
+ font-size: var(--ui-font-size-xs);
1069
+ font-family: var(--ui-font-mono);
1070
+ text-align: right;
1071
+ -moz-appearance: textfield;
1072
+ }
1073
+
1074
+ .angle-input::-webkit-inner-spin-button,
1075
+ .angle-input::-webkit-outer-spin-button {
1076
+ -webkit-appearance: none;
1077
+ margin: 0;
1078
+ }
1079
+
1080
+ .angle-unit {
1081
+ font-size: var(--ui-font-size-xs);
1082
+ color: var(--ui-text-muted);
1083
+ }
1084
+ </style>