@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,362 @@
1
+ /**
2
+ * Components slice — per-component `{ activeFile, aliases, config, unlinked? }`.
3
+ *
4
+ * `aliases` are the typed component-token → semantic-token map (each entry is
5
+ * a `CssVarRef` discriminated union: `{ kind: 'token', name }` or
6
+ * `{ kind: 'literal', value }`). The renderer emits each alias entry as
7
+ * `var(<name>)` for tokens or as the raw literal for literals.
8
+ *
9
+ * `config` carries literal-valued knobs that don't follow the alias →
10
+ * `var(...)` shape (e.g. `--dialog-confirm-variant: 'primary'`). The set of
11
+ * keys routed to config lives in `componentConfigKeys`.
12
+ *
13
+ * Themes and components are orthogonal: `loadFromFile` preserves
14
+ * `state.components`.
15
+ *
16
+ * Sharing semantics: tokens with the same groupKey (registered explicitly
17
+ * via `registerComponentSchema`) are siblings. Each individual sibling can opt out of the group: `unlinked`
18
+ * lists the specific variable names that have detached, leaving the rest
19
+ * of the group intact. `setComponentAliasLinked` writes the alias to every
20
+ * sibling that is *currently linked* (i.e. not in `unlinked`) plus the
21
+ * target itself, re-joining it to the group; `unlinkComponentProperty`
22
+ * detaches just that one variable. The `unlinked` list persists across
23
+ * theme loads.
24
+ *
25
+ * Dirty tracking: `savedComponents` is the on-disk snapshot baseline (one
26
+ * stringified `{aliases, config}` bag per component). `componentDirty`
27
+ * re-evaluates on any state change or baseline bump. The baseline is set
28
+ * explicitly by the load path (`setSavedComponentBaseline`) — `loadComponentActive`
29
+ * and `seedComponentsFromApi` in editorStore call into this from their migration
30
+ * pipeline.
31
+ */
32
+ import { writable, derived, get, type Readable } from 'svelte/store';
33
+ import type { CssVarRef, EditorState } from '../editorTypes';
34
+ import { store, mutate } from '../editorCore';
35
+
36
+ const EMPTY_COMPONENT_BASELINE = JSON.stringify({ aliases: {}, config: {} });
37
+
38
+ export function componentBaseline(slice: { aliases: Record<string, CssVarRef>; config: Record<string, unknown> }): string {
39
+ return JSON.stringify({ aliases: slice.aliases, config: slice.config });
40
+ }
41
+
42
+ export function componentsToVars(components: EditorState['components']): Record<string, string> {
43
+ const out: Record<string, string> = {};
44
+ for (const slice of Object.values(components)) {
45
+ for (const [varName, ref] of Object.entries(slice.aliases)) {
46
+ out[varName] = ref.kind === 'token' ? `var(${ref.name})` : ref.value;
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ export function getComponentOwnedVarNames(state: EditorState): string[] {
53
+ const names: string[] = [];
54
+ for (const slice of Object.values(state.components)) {
55
+ for (const name of Object.keys(slice.aliases)) names.push(name);
56
+ }
57
+ return names;
58
+ }
59
+
60
+ /**
61
+ * Loader: themes and components are orthogonal — component aliases live
62
+ * in their own files, not the theme JSON. Preserve the current slice
63
+ * across theme loads and strip any component-owned vars that may have
64
+ * leaked into the theme's cssVariables bag. Mutates `next` and `rawVars`
65
+ * in place.
66
+ */
67
+ export function loadComponentsFromVars(
68
+ next: EditorState,
69
+ rawVars: Record<string, string>,
70
+ ): void {
71
+ next.components = structuredClone(get(store).components);
72
+ for (const name of getComponentOwnedVarNames(next)) delete rawVars[name];
73
+ }
74
+
75
+ // Module-private baseline for per-component dirty detection. Parallels
76
+ // `savedAtIndex` + `historyTick` for the global flag; `componentSavedTick`
77
+ // drives re-derivation when the baseline changes.
78
+ const savedComponents: Record<string, string> = {};
79
+ const componentSavedTick = writable(0);
80
+ function bumpComponentSavedTick(): void { componentSavedTick.update((n) => n + 1); }
81
+
82
+ export const componentDirty: Readable<Record<string, boolean>> = derived(
83
+ [store, componentSavedTick],
84
+ ([$state]) => {
85
+ const out: Record<string, boolean> = {};
86
+ for (const [comp, slice] of Object.entries($state.components)) {
87
+ out[comp] = componentBaseline(slice) !== (savedComponents[comp] ?? EMPTY_COMPONENT_BASELINE);
88
+ }
89
+ return out;
90
+ },
91
+ );
92
+
93
+ export function setComponentAlias(component: string, varName: string, ref: CssVarRef): void {
94
+ mutate(`set alias ${component}/${varName}`, (s) => {
95
+ const existing = s.components[component];
96
+ if (existing) {
97
+ existing.aliases[varName] = ref;
98
+ } else {
99
+ s.components[component] = { activeFile: 'default', aliases: { [varName]: ref }, config: {} };
100
+ }
101
+ });
102
+ }
103
+
104
+ export function clearComponentAlias(component: string, varName: string): void {
105
+ mutate(`clear alias ${component}/${varName}`, (s) => {
106
+ const slice = s.components[component];
107
+ if (!slice) return;
108
+ delete slice.aliases[varName];
109
+ });
110
+ }
111
+
112
+ export function setComponentConfig(component: string, key: string, value: unknown): void {
113
+ mutate(`set config ${component}/${key}`, (s) => {
114
+ const existing = s.components[component];
115
+ if (existing) {
116
+ existing.config[key] = value;
117
+ } else {
118
+ s.components[component] = { activeFile: 'default', aliases: {}, config: { [key]: value } };
119
+ }
120
+ });
121
+ }
122
+
123
+ export function clearComponentConfig(component: string, key: string): void {
124
+ mutate(`clear config ${component}/${key}`, (s) => {
125
+ const slice = s.components[component];
126
+ if (!slice) return;
127
+ delete slice.config[key];
128
+ });
129
+ }
130
+
131
+ function componentVarPrefix(component: string): string {
132
+ return `--${component}-`;
133
+ }
134
+
135
+ /**
136
+ * Per-component groupKey schema registered by editor modules. Maps each
137
+ * declared variable to its groupKey (the explicit sibling-set identifier).
138
+ * Tokens with the same groupKey are siblings; tokens not in the schema fall
139
+ * back to last-dash property inference so unmigrated editors keep working.
140
+ */
141
+ const componentSchemas: Record<string, Map<string, string>> = {};
142
+ /** Inverse of `componentSchemas`: groupKey → declared variables sharing it.
143
+ * This is the linkage topology declared by the editor, independent of which
144
+ * aliases the user happens to have saved. */
145
+ const componentSchemaSiblings: Record<string, Map<string, string[]>> = {};
146
+
147
+ const TYPOGRAPHY_PROP_SUFFIXES = ['font-family', 'font-size', 'font-weight', 'line-height'] as const;
148
+
149
+ /** Pull the slot identifier off a typography variable name, e.g.
150
+ * `--card-primary-title-font-family` → `'title'`. Returns null for
151
+ * non-typography vars, where the slot concept doesn't apply. */
152
+ function typographySlotOf(varName: string): string | null {
153
+ for (const suffix of TYPOGRAPHY_PROP_SUFFIXES) {
154
+ if (!varName.endsWith('-' + suffix)) continue;
155
+ const head = varName.slice(0, -(suffix.length + 1));
156
+ const lastDash = head.lastIndexOf('-');
157
+ if (lastDash < 0) return null;
158
+ return head.slice(lastDash + 1);
159
+ }
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Register a component's token → groupKey mapping. Editors call this at
165
+ * module load (top of `<script>`) so sibling lookups can prefer explicit
166
+ * groupKeys over name-derived inference. Re-registration overwrites prior
167
+ * entries for the same component.
168
+ *
169
+ * Warns when a single groupKey covers typography variables whose name-derived
170
+ * slots differ — e.g. `groupKey: 'font-family'` covering both
171
+ * `--card-primary-title-font-family` and `--card-primary-body-font-family`.
172
+ * Slot prefixes (`title-font-family` vs `body-font-family`) are required so
173
+ * each slot is independently linkable. See `src/styles/CONVENTIONS.md`.
174
+ */
175
+ export function registerComponentSchema(
176
+ component: string,
177
+ tokens: ReadonlyArray<{ variable: string; groupKey?: string }>,
178
+ ): void {
179
+ const map = new Map<string, string>();
180
+ const siblings = new Map<string, string[]>();
181
+ for (const t of tokens) {
182
+ if (!t.groupKey) continue;
183
+ map.set(t.variable, t.groupKey);
184
+ const list = siblings.get(t.groupKey) ?? [];
185
+ list.push(t.variable);
186
+ siblings.set(t.groupKey, list);
187
+ }
188
+ componentSchemas[component] = map;
189
+ componentSchemaSiblings[component] = siblings;
190
+
191
+ for (const [groupKey, vars] of siblings) {
192
+ const slots = new Set<string>();
193
+ for (const v of vars) {
194
+ const slot = typographySlotOf(v);
195
+ if (slot) slots.add(slot);
196
+ }
197
+ if (slots.size > 1) {
198
+ const slotList = [...slots];
199
+ const examples = slotList.map((s) => `"${s}-${groupKey}"`).join(', ');
200
+ console.warn(
201
+ `[registerComponentSchema] component "${component}" groupKey "${groupKey}" links typography variables with distinct slots: ${slotList.join(', ')}. ` +
202
+ `Use slot-prefixed groupKeys (e.g. ${examples}) so each slot is independently linkable.`,
203
+ );
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Resolve a variable's groupKey from the component's registered schema.
210
+ * Returns null when the variable has no declared groupKey (i.e. it isn't a
211
+ * member of any sibling group).
212
+ */
213
+ function getGroupKey(component: string, varName: string): string | null {
214
+ return componentSchemas[component]?.get(varName) ?? null;
215
+ }
216
+
217
+ /**
218
+ * Variables that share `varName`'s groupKey, i.e. are declared as one
219
+ * linkage group. The editor's registered schema is the source of truth — the
220
+ * declared topology is reported regardless of which aliases happen to be
221
+ * persisted in the slice, so link UI reflects the editor's intent rather than
222
+ * leaking through the on-disk state. Falls back to a slice scan only when the
223
+ * variable isn't declared (legacy/inferred groupKey).
224
+ */
225
+ export function getComponentPropertySiblings(component: string, varName: string): string[] {
226
+ const groupKey = getGroupKey(component, varName);
227
+ if (!groupKey) return [];
228
+ const declared = componentSchemaSiblings[component]?.get(groupKey);
229
+ if (declared && declared.length > 0) return declared.slice();
230
+ const slice = get(store).components[component];
231
+ if (!slice) return [];
232
+ const siblings: string[] = [];
233
+ for (const v of Object.keys(slice.aliases)) {
234
+ if (getGroupKey(component, v) === groupKey) siblings.push(v);
235
+ }
236
+ return siblings;
237
+ }
238
+
239
+ function cssVarRefEqual(a: CssVarRef | undefined, b: CssVarRef | undefined): boolean {
240
+ if (!a || !b) return a === b;
241
+ if (a.kind !== b.kind) return false;
242
+ return a.kind === 'token'
243
+ ? a.name === (b as { kind: 'token'; name: string }).name
244
+ : a.value === (b as { kind: 'literal'; value: string }).value;
245
+ }
246
+
247
+ /** True iff `varName` is not individually opted out, has ≥2 declared siblings,
248
+ * and the linked siblings agree — either all sharing the same explicit alias,
249
+ * or all having no override (linked at the upstream default). */
250
+ export function isComponentPropertyLinked(component: string, varName: string): boolean {
251
+ const slice = get(store).components[component];
252
+ if (slice?.unlinked?.includes(varName)) return false;
253
+ const siblings = getComponentPropertySiblings(component, varName);
254
+ if (siblings.length < 2) return false;
255
+ const unlinkedList = slice?.unlinked ?? [];
256
+ const linkedSiblings = siblings.filter((v) => !unlinkedList.includes(v));
257
+ if (linkedSiblings.length < 2) return false;
258
+ const aliases = slice?.aliases ?? {};
259
+ const first = aliases[linkedSiblings[0]];
260
+ return linkedSiblings.every((v) => cssVarRefEqual(aliases[v], first));
261
+ }
262
+
263
+ /** Write `ref` to `varName` and every sibling currently linked (not in `unlinked`),
264
+ * and remove `varName` from the unlinked list so it rejoins the group. */
265
+ export function setComponentAliasLinked(component: string, varName: string, ref: CssVarRef): void {
266
+ const groupKey = getGroupKey(component, varName);
267
+ const siblings = getComponentPropertySiblings(component, varName);
268
+ if (!groupKey || siblings.length === 0) {
269
+ setComponentAlias(component, varName, ref);
270
+ return;
271
+ }
272
+ mutate(`link ${component}/${groupKey}`, (s) => {
273
+ const slice = s.components[component] ?? (s.components[component] = { activeFile: 'default', aliases: {}, config: {} });
274
+ const unlinked = (slice.unlinked ?? []).filter((p) => p !== varName);
275
+ slice.aliases[varName] = ref;
276
+ for (const v of siblings) {
277
+ if (v === varName) continue;
278
+ if (!unlinked.includes(v)) slice.aliases[v] = ref;
279
+ }
280
+ if (unlinked.length === 0) delete slice.unlinked;
281
+ else slice.unlinked = unlinked;
282
+ });
283
+ }
284
+
285
+ /** Clear `varName` and every sibling currently linked (not in `unlinked`). */
286
+ export function clearComponentAliasLinked(component: string, varName: string): void {
287
+ const groupKey = getGroupKey(component, varName);
288
+ const siblings = getComponentPropertySiblings(component, varName);
289
+ if (!groupKey || siblings.length === 0) {
290
+ clearComponentAlias(component, varName);
291
+ return;
292
+ }
293
+ mutate(`clear link ${component}/${groupKey}`, (s) => {
294
+ const slice = s.components[component];
295
+ if (!slice) return;
296
+ const unlinked = slice.unlinked ?? [];
297
+ for (const v of siblings) {
298
+ if (v === varName || !unlinked.includes(v)) delete slice.aliases[v];
299
+ }
300
+ });
301
+ }
302
+
303
+ /** Detach a single property from its sibling group. Other siblings stay linked
304
+ * to each other; only `varName` becomes independently editable. */
305
+ export function unlinkComponentProperty(component: string, varName: string): void {
306
+ const groupKey = getGroupKey(component, varName);
307
+ if (!groupKey) return;
308
+ const siblings = getComponentPropertySiblings(component, varName);
309
+ if (siblings.length < 2) return;
310
+ mutate(`unlink ${component}/${varName}`, (s) => {
311
+ const slice = s.components[component];
312
+ if (!slice) return;
313
+ const unlinked = slice.unlinked ?? [];
314
+ if (!unlinked.includes(varName)) {
315
+ slice.unlinked = [...unlinked, varName];
316
+ }
317
+ });
318
+ }
319
+
320
+ /** Rejoin `varName` to its sibling group as pure metadata: drop it from the
321
+ * `unlinked` list without writing any alias. Use when the group has no
322
+ * overrides yet (linked at the upstream default) and the user just wants to
323
+ * re-engage membership — `setComponentAliasLinked` requires a ref to write,
324
+ * which there is none of in that state. */
325
+ export function relinkComponentProperty(component: string, varName: string): void {
326
+ const slice = get(store).components[component];
327
+ if (!slice?.unlinked?.includes(varName)) return;
328
+ mutate(`relink ${component}/${varName}`, (s) => {
329
+ const next = s.components[component];
330
+ if (!next?.unlinked) return;
331
+ const remaining = next.unlinked.filter((v) => v !== varName);
332
+ if (remaining.length === 0) delete next.unlinked;
333
+ else next.unlinked = remaining;
334
+ });
335
+ }
336
+
337
+ export function markComponentSaved(component: string): void {
338
+ const slice = get(store).components[component];
339
+ if (!slice) return;
340
+ savedComponents[component] = componentBaseline(slice);
341
+ bumpComponentSavedTick();
342
+ }
343
+
344
+ /**
345
+ * Set the on-disk baseline for a component without touching the store.
346
+ * Called by `loadComponentActive` / `seedComponentsFromApi` after their
347
+ * migration step so the post-load state reads clean.
348
+ */
349
+ export function setSavedComponentBaseline(component: string, baseline: string): void {
350
+ savedComponents[component] = baseline;
351
+ }
352
+
353
+ /** Notify subscribers that the dirty baseline changed. */
354
+ export function notifyComponentSavedChanged(): void {
355
+ bumpComponentSavedTick();
356
+ }
357
+
358
+ /** Test-only: clear the baseline and the dirty signal. */
359
+ export function __resetComponentsForTests(): void {
360
+ for (const k of Object.keys(savedComponents)) delete savedComponents[k];
361
+ bumpComponentSavedTick();
362
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Aggregated CSS-var names owned by store domains. Consumers that scrape
3
+ * the DOM (e.g. the save flow still pulling palette ramps emitted by
4
+ * PaletteEditor) use this to drop domain-owned keys from their scraped bag
5
+ * so the store stays the single source of truth.
6
+ */
7
+ import { COLUMN_VAR_NAMES } from './columns';
8
+ import { OVERLAY_VAR_NAMES } from './overlays';
9
+ import { SHADOW_VAR_NAMES } from './shadows';
10
+
11
+ export const DOMAIN_VAR_NAMES: readonly string[] = [
12
+ ...COLUMN_VAR_NAMES,
13
+ ...OVERLAY_VAR_NAMES,
14
+ ...SHADOW_VAR_NAMES,
15
+ ];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fonts slice — sources + stacks. No derived CSS vars owned by this store
3
+ * (the `--font-*` vars are written by `applyFontStacks` in fontLoader, and
4
+ * @font-face rules by `applyFontSources`). We own the *data* (sources +
5
+ * stacks); callers still invoke the DOM-side-effect helpers themselves
6
+ * after mutating.
7
+ */
8
+ import type { FontSource, FontStack } from '../themeTypes';
9
+ import { store, mutate, persist } from '../editorCore';
10
+
11
+ export function setFontSources(sources: FontSource[]): void {
12
+ mutate('update font sources', (s) => { s.fonts.sources = sources; });
13
+ }
14
+
15
+ export function setFontStacks(stacks: FontStack[]): void {
16
+ mutate('update font stacks', (s) => { s.fonts.stacks = stacks; });
17
+ }
18
+
19
+ /**
20
+ * Populate fonts from the server's active theme at boot. Does not push
21
+ * a history entry — the boot load is a starting point, not an edit.
22
+ */
23
+ export function seedFontsFromTheme(sources: FontSource[], stacks: FontStack[]): void {
24
+ store.update((s) => {
25
+ s.fonts.sources = structuredClone(sources);
26
+ s.fonts.stacks = structuredClone(stacks);
27
+ return s;
28
+ });
29
+ persist();
30
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Gradients slice — fixed four-slot scale (--gradient-1 … --gradient-4),
3
+ * each rendering to a single CSS var. Stops carry token-name references
4
+ * (`--color-brand-500`); the renderer wraps them in `var(...)` so palette
5
+ * edits flow through.
6
+ */
7
+ import type { EditorState, GradientToken, GradientTokenStop, GradientType } from '../editorTypes';
8
+ import { mutate } from '../editorCore';
9
+
10
+ export function makeDefaultGradients(): GradientToken[] {
11
+ return [
12
+ {
13
+ variable: '--gradient-1',
14
+ type: 'linear',
15
+ angle: 90,
16
+ stops: [
17
+ { position: 0, color: '--color-brand-500' },
18
+ { position: 100, color: '--color-accent-500' },
19
+ ],
20
+ },
21
+ {
22
+ variable: '--gradient-2',
23
+ type: 'linear',
24
+ angle: 135,
25
+ stops: [
26
+ { position: 0, color: '--color-brand-500' },
27
+ { position: 100, color: '--color-special-500' },
28
+ ],
29
+ },
30
+ {
31
+ variable: '--gradient-3',
32
+ type: 'linear',
33
+ angle: 90,
34
+ stops: [
35
+ { position: 0, color: '--color-success-500' },
36
+ { position: 100, color: '--color-info-500' },
37
+ ],
38
+ },
39
+ {
40
+ variable: '--gradient-4',
41
+ type: 'linear',
42
+ angle: 45,
43
+ stops: [
44
+ { position: 0, color: '--color-danger-500' },
45
+ { position: 100, color: '--color-warning-500' },
46
+ ],
47
+ },
48
+ ];
49
+ }
50
+
51
+ function formatGradientStop(s: GradientTokenStop): string {
52
+ const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
53
+ const opacity = s.opacity ?? 100;
54
+ const color = opacity >= 100
55
+ ? base
56
+ : `color-mix(in srgb, ${base} ${opacity}%, transparent)`;
57
+ return `${color} ${s.position}%`;
58
+ }
59
+
60
+ /** Stops portion only — used by the palette selector to materialize a
61
+ * linear-gradient with a per-slot angle override while keeping the token's
62
+ * stop list (and its `var(--color-…)` refs, which propagate palette edits). */
63
+ export function formatGradientStops(t: GradientToken): string {
64
+ return t.stops.map(formatGradientStop).join(', ');
65
+ }
66
+
67
+ function formatGradient(t: GradientToken): string {
68
+ const stops = formatGradientStops(t);
69
+ if (t.type === 'linear') return `linear-gradient(${t.angle}deg, ${stops})`;
70
+ return `radial-gradient(${stops})`;
71
+ }
72
+
73
+ export function gradientsToVars(g: EditorState['gradients']): Record<string, string> {
74
+ const out: Record<string, string> = {};
75
+ for (const t of g.tokens) out[t.variable] = formatGradient(t);
76
+ return out;
77
+ }
78
+
79
+ function findGradient(s: EditorState, variable: string): GradientToken | undefined {
80
+ return s.gradients.tokens.find((t) => t.variable === variable);
81
+ }
82
+
83
+ /** Replace a gradient's type, angle, and stops in one shot. Used by the editor
84
+ * to restore a pre-edit snapshot on Cancel. */
85
+ export function setGradient(
86
+ variable: string,
87
+ next: { type: GradientType; angle: number; stops: GradientTokenStop[] },
88
+ ): void {
89
+ mutate(`replace gradient ${variable}`, (s) => {
90
+ const t = findGradient(s, variable);
91
+ if (!t) return;
92
+ t.type = next.type;
93
+ t.angle = next.angle;
94
+ t.stops = next.stops.map((st) => ({ ...st }));
95
+ });
96
+ }
97
+
98
+ export function setGradientType(variable: string, type: GradientType): void {
99
+ mutate(`set gradient type ${variable}`, (s) => {
100
+ const t = findGradient(s, variable);
101
+ if (t) t.type = type;
102
+ });
103
+ }
104
+
105
+ export function setGradientAngle(variable: string, angle: number): void {
106
+ mutate(`set gradient angle ${variable}`, (s) => {
107
+ const t = findGradient(s, variable);
108
+ if (t) t.angle = angle;
109
+ });
110
+ }
111
+
112
+ export function setGradientStop(variable: string, index: number, stop: Partial<GradientTokenStop>): void {
113
+ mutate(`set gradient stop ${variable}[${index}]`, (s) => {
114
+ const t = findGradient(s, variable);
115
+ if (!t || !t.stops[index]) return;
116
+ if (stop.position !== undefined) t.stops[index].position = stop.position;
117
+ if (stop.color !== undefined) t.stops[index].color = stop.color;
118
+ if (stop.opacity !== undefined) t.stops[index].opacity = stop.opacity;
119
+ });
120
+ }
121
+
122
+ export function addGradientStop(variable: string, stop: GradientTokenStop): void {
123
+ mutate(`add gradient stop ${variable}`, (s) => {
124
+ const t = findGradient(s, variable);
125
+ if (!t) return;
126
+ t.stops.push(stop);
127
+ t.stops.sort((a, b) => a.position - b.position);
128
+ });
129
+ }
130
+
131
+ export function removeGradientStop(variable: string, index: number): void {
132
+ mutate(`remove gradient stop ${variable}[${index}]`, (s) => {
133
+ const t = findGradient(s, variable);
134
+ if (!t || t.stops.length <= 2) return;
135
+ t.stops.splice(index, 1);
136
+ });
137
+ }
138
+
139
+ export function addGradientToken(token: GradientToken): void {
140
+ mutate(`add gradient ${token.variable}`, (s) => {
141
+ if (findGradient(s, token.variable)) return;
142
+ s.gradients.tokens.push({
143
+ ...token,
144
+ stops: token.stops.map((st) => ({ ...st })),
145
+ });
146
+ });
147
+ }
148
+
149
+ export function removeGradientToken(variable: string): void {
150
+ mutate(`remove gradient ${variable}`, (s) => {
151
+ s.gradients.tokens = s.gradients.tokens.filter((t) => t.variable !== variable);
152
+ });
153
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Overlays slice — overlay + hover RGBA tokens. Defaults are editor-defined
3
+ * (mirror what `VariablesTab` historically initialised into local let state)
4
+ * and diverge from tokens.css by design: the editor starts with a neutral
5
+ * palette and tokens.css continues to win until first edit.
6
+ */
7
+ import type { EditorState, OverlayToken } from '../editorTypes';
8
+
9
+ export function makeDefaultOverlayTokens(): OverlayToken[] {
10
+ return [
11
+ { variable: '--overlay-lowest', label: 'Lowest', r: 0, g: 0, b: 0, opacity: 0.05 },
12
+ { variable: '--overlay-lower', label: 'Lower', r: 0, g: 0, b: 0, opacity: 0.1 },
13
+ { variable: '--overlay-low', label: 'Low', r: 0, g: 0, b: 0, opacity: 0.2 },
14
+ { variable: '--overlay', label: 'Base', r: 0, g: 0, b: 0, opacity: 0.3 },
15
+ { variable: '--overlay-high', label: 'High', r: 0, g: 0, b: 0, opacity: 0.5 },
16
+ { variable: '--overlay-higher', label: 'Higher', r: 0, g: 0, b: 0, opacity: 0.7 },
17
+ { variable: '--overlay-highest',label: 'Highest',r: 0, g: 0, b: 0, opacity: 0.95 },
18
+ ];
19
+ }
20
+
21
+ export function makeDefaultHoverTokens(): OverlayToken[] {
22
+ return [
23
+ { variable: '--hover-low', label: 'Low', r: 255, g: 255, b: 255, opacity: 0.05 },
24
+ { variable: '--hover', label: 'Base', r: 255, g: 255, b: 255, opacity: 0.1 },
25
+ { variable: '--hover-high', label: 'High', r: 255, g: 255, b: 255, opacity: 0.15 },
26
+ ];
27
+ }
28
+
29
+ export function makeDefaultOverlaysState(): EditorState['overlays'] {
30
+ return {
31
+ tokens: makeDefaultOverlayTokens(),
32
+ hoverTokens: makeDefaultHoverTokens(),
33
+ globals: {
34
+ overlay: { hue: 0, saturation: 0, lightness: 0, opacityMin: 0.05, opacityMax: 0.95 },
35
+ hover: { hue: 0, saturation: 0, lightness: 100, opacityMin: 0.05, opacityMax: 0.15 },
36
+ },
37
+ };
38
+ }
39
+
40
+ export const OVERLAY_VAR_NAMES = [
41
+ '--overlay-lowest', '--overlay-lower', '--overlay-low', '--overlay',
42
+ '--overlay-high', '--overlay-higher', '--overlay-highest',
43
+ '--hover-low', '--hover', '--hover-high',
44
+ ] as const;
45
+
46
+ // Accepts rgb(), rgba(), and #rrggbb[aa] — themes saved by the editor
47
+ // always use rgba(), but loading hand-written files shouldn't break.
48
+ export const RGBA_RE = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i;
49
+ export const HEX_RE = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i;
50
+
51
+ export function parseRgba(raw: string): { r: number; g: number; b: number; opacity: number } | null {
52
+ const s = raw.trim();
53
+ const m = s.match(RGBA_RE);
54
+ if (m) {
55
+ const r = parseInt(m[1], 10);
56
+ const g = parseInt(m[2], 10);
57
+ const b = parseInt(m[3], 10);
58
+ const a = m[4] !== undefined ? parseFloat(m[4]) : 1;
59
+ if (![r, g, b].every((n) => Number.isFinite(n) && n >= 0 && n <= 255)) return null;
60
+ return { r, g, b, opacity: Number.isFinite(a) ? a : 1 };
61
+ }
62
+ const h = s.match(HEX_RE);
63
+ if (h) {
64
+ const hex = h[1];
65
+ const alpha = h[2] !== undefined ? parseInt(h[2], 16) / 255 : 1;
66
+ return {
67
+ r: parseInt(hex.slice(0, 2), 16),
68
+ g: parseInt(hex.slice(2, 4), 16),
69
+ b: parseInt(hex.slice(4, 6), 16),
70
+ opacity: Math.round(alpha * 100) / 100,
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+
76
+ export function overlayTokenToRgba(t: OverlayToken): string {
77
+ return `rgba(${t.r}, ${t.g}, ${t.b}, ${t.opacity})`;
78
+ }
79
+
80
+ export function overlaysToVars(o: EditorState['overlays']): Record<string, string> {
81
+ const out: Record<string, string> = {};
82
+ for (const t of o.tokens) out[t.variable] = overlayTokenToRgba(t);
83
+ for (const t of o.hoverTokens) out[t.variable] = overlayTokenToRgba(t);
84
+ return out;
85
+ }
86
+
87
+ function tokensEqualDefault(tokens: OverlayToken[], defaults: OverlayToken[]): boolean {
88
+ if (tokens.length !== defaults.length) return false;
89
+ for (let i = 0; i < tokens.length; i++) {
90
+ const a = tokens[i]; const b = defaults[i];
91
+ if (a.variable !== b.variable || a.r !== b.r || a.g !== b.g || a.b !== b.b || a.opacity !== b.opacity) return false;
92
+ }
93
+ return true;
94
+ }
95
+
96
+ /**
97
+ * Same pattern as columns: only emit overlay CSS vars once state diverges
98
+ * from the editor defaults. tokens.css owns the rgba values until the
99
+ * user touches any overlay control (or loads a theme that already
100
+ * contains overrides).
101
+ */
102
+ export function overlaysEqualsDefault(o: EditorState['overlays']): boolean {
103
+ return tokensEqualDefault(o.tokens, makeDefaultOverlayTokens())
104
+ && tokensEqualDefault(o.hoverTokens, makeDefaultHoverTokens());
105
+ }
106
+
107
+ export function applyOverlayVarsToState(overlays: EditorState['overlays'], vars: Record<string, string>): void {
108
+ const applyTo = (list: OverlayToken[]) => {
109
+ for (const t of list) {
110
+ const raw = vars[t.variable];
111
+ if (!raw) continue;
112
+ const parsed = parseRgba(raw);
113
+ if (!parsed) continue;
114
+ t.r = parsed.r; t.g = parsed.g; t.b = parsed.b; t.opacity = parsed.opacity;
115
+ }
116
+ };
117
+ applyTo(overlays.tokens);
118
+ applyTo(overlays.hoverTokens);
119
+ }
120
+
121
+ /**
122
+ * Loader: route overlay/hover entries from a freshly-loaded theme's vars
123
+ * bag into `next.overlays` and remove them from the bag. Mutates `next`
124
+ * and `rawVars` in place.
125
+ */
126
+ export function loadOverlaysFromVars(
127
+ next: EditorState,
128
+ rawVars: Record<string, string>,
129
+ ): void {
130
+ applyOverlayVarsToState(next.overlays, rawVars);
131
+ for (const name of OVERLAY_VAR_NAMES) delete rawVars[name];
132
+ }