@motion-proto/live-tokens 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +84 -29
  3. package/dist-plugin/index.cjs +177 -125
  4. package/dist-plugin/index.d.cts +3 -2
  5. package/dist-plugin/index.d.ts +3 -2
  6. package/dist-plugin/index.js +177 -125
  7. package/package.json +8 -2
  8. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  9. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  10. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
  11. package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
  12. package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
  13. package/src/editor/component-editor/InputEditor.svelte +272 -0
  14. package/src/editor/component-editor/NotificationEditor.svelte +44 -65
  15. package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
  16. package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
  17. package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
  18. package/src/editor/component-editor/index.ts +16 -1
  19. package/src/editor/component-editor/registry.ts +138 -28
  20. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  21. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  22. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  23. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  24. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  25. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  26. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  27. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  28. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  29. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  30. package/src/editor/core/components/componentConfigKeys.ts +14 -3
  31. package/src/editor/core/components/componentConfigService.ts +7 -6
  32. package/src/editor/core/manifests/manifestService.ts +5 -4
  33. package/src/editor/core/storage/apiBase.ts +15 -0
  34. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  35. package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
  36. package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
  37. package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
  38. package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
  39. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
  40. package/src/editor/core/themes/migrations/index.ts +10 -0
  41. package/src/editor/core/themes/slices/components.ts +9 -0
  42. package/src/editor/core/themes/themeInit.ts +3 -2
  43. package/src/editor/core/themes/themeService.ts +3 -2
  44. package/src/editor/index.ts +10 -1
  45. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  46. package/src/editor/pages/EditorShell.svelte +53 -3
  47. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  48. package/src/editor/ui/variantScales.ts +34 -0
  49. package/src/system/components/Button.svelte +34 -85
  50. package/src/system/components/CollapsibleSection.svelte +1 -48
  51. package/src/system/components/CornerBadge.svelte +72 -138
  52. package/src/system/components/Dialog.svelte +24 -4
  53. package/src/system/components/ImageLightbox.svelte +578 -0
  54. package/src/system/components/Input.svelte +387 -0
  55. package/src/system/components/ProgressBar.svelte +62 -258
  56. package/src/system/components/SectionDivider.svelte +117 -43
  57. package/src/system/components/SegmentedControl.svelte +81 -15
  58. package/src/system/components/SideNavigation.svelte +777 -0
  59. package/src/system/styles/tokens.css +43 -0
  60. package/src/system/styles/tokens.generated.css +4 -183
  61. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -5,23 +5,27 @@ import { registerComponentSchema } from '../core/store/editorStore';
5
5
  import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
6
6
  import CalloutEditor, { allTokens as calloutTokens } from './CalloutEditor.svelte';
7
7
  import CornerBadgeEditor, { allTokens as cornerBadgeTokens } from './CornerBadgeEditor.svelte';
8
- import StandardButtonsEditor, { allTokens as buttonTokens } from './StandardButtonsEditor.svelte';
8
+ import ButtonEditor, { allTokens as buttonTokens } from './ButtonEditor.svelte';
9
9
  import CardEditor, { allTokens as cardTokens } from './CardEditor.svelte';
10
10
  import CollapsibleSectionEditor, { allTokens as collapsibleSectionTokens } from './CollapsibleSectionEditor.svelte';
11
11
  import DialogEditor, { allTokens as dialogTokens } from './DialogEditor.svelte';
12
12
  import ImageEditor, { allTokens as imageTokens } from './ImageEditor.svelte';
13
+ import ImageLightboxEditor, { allTokens as imageLightboxTokens } from './ImageLightboxEditor.svelte';
13
14
  import InlineEditActionsEditor, { allTokens as inlineEditActionsTokens } from './InlineEditActionsEditor.svelte';
15
+ import InputEditor, { allTokens as inputTokens } from './InputEditor.svelte';
14
16
  import MenuSelectEditor, { allTokens as menuSelectTokens } from './MenuSelectEditor.svelte';
15
17
  import NotificationEditor, { allTokens as notificationTokens } from './NotificationEditor.svelte';
16
18
  import ProgressBarEditor, { allTokens as progressBarTokens } from './ProgressBarEditor.svelte';
17
19
  import RadioButtonEditor, { allTokens as radioButtonTokens } from './RadioButtonEditor.svelte';
18
20
  import SectionDividerEditor, { allTokens as sectionDividerTokens } from './SectionDividerEditor.svelte';
19
21
  import SegmentedControlEditor, { allTokens as segmentedControlTokens } from './SegmentedControlEditor.svelte';
22
+ import SideNavigationEditor, { allTokens as sideNavigationTokens } from './SideNavigationEditor.svelte';
20
23
  import TableEditor, { allTokens as tableTokens } from './TableEditor.svelte';
21
24
  import TabBarEditor, { allTokens as tabBarTokens } from './TabBarEditor.svelte';
22
25
  import TooltipEditor, { allTokens as tooltipTokens } from './TooltipEditor.svelte';
23
26
 
24
- export type ComponentId =
27
+ /** Internal narrowed union of the first-party component ids. Not exposed publicly. */
28
+ type BuiltInComponentId =
25
29
  | 'segmentedcontrol'
26
30
  | 'button'
27
31
  | 'notification'
@@ -32,15 +36,25 @@ export type ComponentId =
32
36
  | 'callout'
33
37
  | 'cornerbadge'
34
38
  | 'image'
39
+ | 'imagelightbox'
35
40
  | 'inlineeditactions'
41
+ | 'input'
36
42
  | 'menuselect'
37
43
  | 'sectiondivider'
38
44
  | 'collapsiblesection'
45
+ | 'sidenavigation'
39
46
  | 'table'
40
47
  | 'tabbar'
41
48
  | 'tooltip'
42
49
  | 'progressbar';
43
50
 
51
+ /**
52
+ * Public component id type. Widened to `string` because consumers can register
53
+ * their own components at runtime via `registerComponent()`. Internal code that
54
+ * needs to narrow to first-party ids can reference `BuiltInComponentId`.
55
+ */
56
+ export type ComponentId = string;
57
+
44
58
  export interface RegistryEntry {
45
59
  /** Canonical id — lowercase, matches the runtime component filename + server scan + `setComponentAlias` key. */
46
60
  id: ComponentId;
@@ -54,21 +68,18 @@ export interface RegistryEntry {
54
68
  editorComponent: Component<any, any, any>;
55
69
  /** Flat token list — the editor's declarative description of its token surface. */
56
70
  schema: Token[];
71
+ /** `'system'` for first-party entries; `'custom'` for entries added via `registerComponent()`. */
72
+ origin: 'system' | 'custom';
57
73
  }
58
74
 
59
75
  /**
60
- * Single source of truth for every component editor. Each entry binds the
61
- * canonical id to its label, icon, source file, editor component, and token
62
- * schema. Display order in the nav rail is sorted alphabetically by label
63
- * (see `componentRegistryEntries` below) — order in this object literal does
64
- * not affect the UI.
65
- *
66
- * Adding a component:
67
- * 1. Author `src/components/<Name>.svelte` (declares CSS vars in `:global(:root)`)
68
- * 2. Author `src/component-editor/<Name>Editor.svelte` (exports `allTokens` from a `<script context="module">` block)
76
+ * First-party registry. Frozen; runtime additions go in `customRegistry`.
77
+ * Adding a first-party component:
78
+ * 1. Author `src/system/components/<Name>.svelte` (declares CSS vars in `:global(:root)`)
79
+ * 2. Author `src/editor/component-editor/<Name>Editor.svelte` (exports `allTokens`)
69
80
  * 3. Add an entry below.
70
81
  */
71
- export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = Object.freeze({
82
+ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Object.freeze({
72
83
  segmentedcontrol: {
73
84
  id: 'segmentedcontrol',
74
85
  label: 'Segmented Control',
@@ -76,14 +87,16 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
76
87
  sourceFile: 'src/system/components/SegmentedControl.svelte',
77
88
  editorComponent: SegmentedControlEditor,
78
89
  schema: segmentedControlTokens,
90
+ origin: 'system',
79
91
  },
80
92
  button: {
81
93
  id: 'button',
82
94
  label: 'Button',
83
95
  icon: 'fas fa-square',
84
96
  sourceFile: 'src/system/components/Button.svelte',
85
- editorComponent: StandardButtonsEditor,
97
+ editorComponent: ButtonEditor,
86
98
  schema: buttonTokens,
99
+ origin: 'system',
87
100
  },
88
101
  notification: {
89
102
  id: 'notification',
@@ -92,6 +105,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
92
105
  sourceFile: 'src/system/components/Notification.svelte',
93
106
  editorComponent: NotificationEditor,
94
107
  schema: notificationTokens,
108
+ origin: 'system',
95
109
  },
96
110
  dialog: {
97
111
  id: 'dialog',
@@ -100,6 +114,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
100
114
  sourceFile: 'src/system/components/Dialog.svelte',
101
115
  editorComponent: DialogEditor,
102
116
  schema: dialogTokens,
117
+ origin: 'system',
103
118
  },
104
119
  radiobutton: {
105
120
  id: 'radiobutton',
@@ -108,6 +123,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
108
123
  sourceFile: 'src/system/components/RadioButton.svelte',
109
124
  editorComponent: RadioButtonEditor,
110
125
  schema: radioButtonTokens,
126
+ origin: 'system',
111
127
  },
112
128
  card: {
113
129
  id: 'card',
@@ -116,6 +132,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
116
132
  sourceFile: 'src/system/components/Card.svelte',
117
133
  editorComponent: CardEditor,
118
134
  schema: cardTokens,
135
+ origin: 'system',
119
136
  },
120
137
  badge: {
121
138
  id: 'badge',
@@ -124,6 +141,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
124
141
  sourceFile: 'src/system/components/Badge.svelte',
125
142
  editorComponent: BadgeEditor,
126
143
  schema: badgeTokens,
144
+ origin: 'system',
127
145
  },
128
146
  callout: {
129
147
  id: 'callout',
@@ -132,6 +150,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
132
150
  sourceFile: 'src/system/components/Callout.svelte',
133
151
  editorComponent: CalloutEditor,
134
152
  schema: calloutTokens,
153
+ origin: 'system',
135
154
  },
136
155
  cornerbadge: {
137
156
  id: 'cornerbadge',
@@ -140,6 +159,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
140
159
  sourceFile: 'src/system/components/CornerBadge.svelte',
141
160
  editorComponent: CornerBadgeEditor,
142
161
  schema: cornerBadgeTokens,
162
+ origin: 'system',
143
163
  },
144
164
  image: {
145
165
  id: 'image',
@@ -148,6 +168,16 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
148
168
  sourceFile: 'src/system/components/Image.svelte',
149
169
  editorComponent: ImageEditor,
150
170
  schema: imageTokens,
171
+ origin: 'system',
172
+ },
173
+ imagelightbox: {
174
+ id: 'imagelightbox',
175
+ label: 'Image Lightbox',
176
+ icon: 'fas fa-expand',
177
+ sourceFile: 'src/system/components/ImageLightbox.svelte',
178
+ editorComponent: ImageLightboxEditor,
179
+ schema: imageLightboxTokens,
180
+ origin: 'system',
151
181
  },
152
182
  inlineeditactions: {
153
183
  id: 'inlineeditactions',
@@ -156,6 +186,16 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
156
186
  sourceFile: 'src/system/components/InlineEditActions.svelte',
157
187
  editorComponent: InlineEditActionsEditor,
158
188
  schema: inlineEditActionsTokens,
189
+ origin: 'system',
190
+ },
191
+ input: {
192
+ id: 'input',
193
+ label: 'Input',
194
+ icon: 'fas fa-i-cursor',
195
+ sourceFile: 'src/system/components/Input.svelte',
196
+ editorComponent: InputEditor,
197
+ schema: inputTokens,
198
+ origin: 'system',
159
199
  },
160
200
  menuselect: {
161
201
  id: 'menuselect',
@@ -164,6 +204,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
164
204
  sourceFile: 'src/system/components/MenuSelect.svelte',
165
205
  editorComponent: MenuSelectEditor,
166
206
  schema: menuSelectTokens,
207
+ origin: 'system',
167
208
  },
168
209
  sectiondivider: {
169
210
  id: 'sectiondivider',
@@ -172,6 +213,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
172
213
  sourceFile: 'src/system/components/SectionDivider.svelte',
173
214
  editorComponent: SectionDividerEditor,
174
215
  schema: sectionDividerTokens,
216
+ origin: 'system',
175
217
  },
176
218
  collapsiblesection: {
177
219
  id: 'collapsiblesection',
@@ -180,6 +222,16 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
180
222
  sourceFile: 'src/system/components/CollapsibleSection.svelte',
181
223
  editorComponent: CollapsibleSectionEditor,
182
224
  schema: collapsibleSectionTokens,
225
+ origin: 'system',
226
+ },
227
+ sidenavigation: {
228
+ id: 'sidenavigation',
229
+ label: 'Side Navigation',
230
+ icon: 'fas fa-bars-staggered',
231
+ sourceFile: 'src/system/components/SideNavigation.svelte',
232
+ editorComponent: SideNavigationEditor,
233
+ schema: sideNavigationTokens,
234
+ origin: 'system',
183
235
  },
184
236
  table: {
185
237
  id: 'table',
@@ -188,6 +240,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
188
240
  sourceFile: 'src/system/components/Table.svelte',
189
241
  editorComponent: TableEditor,
190
242
  schema: tableTokens,
243
+ origin: 'system',
191
244
  },
192
245
  tabbar: {
193
246
  id: 'tabbar',
@@ -196,6 +249,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
196
249
  sourceFile: 'src/system/components/TabBar.svelte',
197
250
  editorComponent: TabBarEditor,
198
251
  schema: tabBarTokens,
252
+ origin: 'system',
199
253
  },
200
254
  tooltip: {
201
255
  id: 'tooltip',
@@ -204,6 +258,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
204
258
  sourceFile: 'src/system/components/Tooltip.svelte',
205
259
  editorComponent: TooltipEditor,
206
260
  schema: tooltipTokens,
261
+ origin: 'system',
207
262
  },
208
263
  progressbar: {
209
264
  id: 'progressbar',
@@ -212,34 +267,89 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
212
267
  sourceFile: 'src/system/components/ProgressBar.svelte',
213
268
  editorComponent: ProgressBarEditor,
214
269
  schema: progressBarTokens,
270
+ origin: 'system',
215
271
  },
216
272
  });
217
273
 
218
- /** Display-ordered list of registry entries sorted alphabetically by label. Iteration order matches the nav rail. */
219
- export const componentRegistryEntries: ReadonlyArray<RegistryEntry> = Object.freeze(
220
- Object.values(componentRegistry).sort((a, b) => a.label.localeCompare(b.label)),
221
- );
274
+ /** Mutable map of consumer-registered components, populated by `registerComponent()`. */
275
+ const customRegistry = new Map<string, RegistryEntry>();
276
+
277
+ /** Argument shape for `registerComponent()`. `origin` is set internally to `'custom'`. */
278
+ export type RegisterComponentEntry = Omit<RegistryEntry, 'origin'>;
222
279
 
223
- /** All canonical component ids, in display order. */
224
- export const componentIds: ReadonlyArray<ComponentId> = Object.freeze(
225
- componentRegistryEntries.map((e) => e.id),
226
- );
280
+ /**
281
+ * Register a consumer-authored component at runtime. Call from `main.ts`
282
+ * before app mount.
283
+ *
284
+ * Collision rule: if `entry.id` matches a built-in id, a warning is logged and
285
+ * the custom entry wins (the custom editor and schema replace the built-in for
286
+ * the rest of the session).
287
+ *
288
+ * Side effect: registers the schema with the editor store so reset-to-default
289
+ * and sibling-group resolution work for the new component.
290
+ */
291
+ export function registerComponent(entry: RegisterComponentEntry): void {
292
+ if (entry.id in builtInRegistry) {
293
+ console.warn(
294
+ `[registerComponent] custom component "${entry.id}" overrides a built-in. The custom editor will be used.`,
295
+ );
296
+ }
297
+ const stored: RegistryEntry = { ...entry, origin: 'custom' };
298
+ customRegistry.set(entry.id, stored);
299
+ registerComponentSchema(entry.id, entry.schema);
300
+ }
301
+
302
+ /**
303
+ * Merged registry: built-ins overlaid with customs (custom wins on id collision).
304
+ * Recomputed on each call so callers see runtime registrations made after their
305
+ * own module-load order.
306
+ */
307
+ export function getComponentRegistry(): Readonly<Record<string, RegistryEntry>> {
308
+ const merged: Record<string, RegistryEntry> = { ...builtInRegistry };
309
+ for (const [id, entry] of customRegistry) {
310
+ merged[id] = entry;
311
+ }
312
+ return merged;
313
+ }
314
+
315
+ /**
316
+ * Display-ordered entries: system first (alphabetical by label), then custom
317
+ * (alphabetical by label). Iteration order matches the nav rail's grouping.
318
+ * The nav rail renders a divider between the two groups when customs exist.
319
+ */
320
+ export function getComponentRegistryEntries(): ReadonlyArray<RegistryEntry> {
321
+ const merged = getComponentRegistry();
322
+ const system: RegistryEntry[] = [];
323
+ const custom: RegistryEntry[] = [];
324
+ for (const entry of Object.values(merged)) {
325
+ (entry.origin === 'system' ? system : custom).push(entry);
326
+ }
327
+ system.sort((a, b) => a.label.localeCompare(b.label));
328
+ custom.sort((a, b) => a.label.localeCompare(b.label));
329
+ return [...system, ...custom];
330
+ }
331
+
332
+ /** All component ids, in display order. */
333
+ export function getComponentIds(): ReadonlyArray<string> {
334
+ return getComponentRegistryEntries().map((e) => e.id);
335
+ }
227
336
 
228
- // Eager schema registration. Replaces the side-effect-on-import pattern that
229
- // each editor module previously used (top-of-script `registerComponentSchema(...)`).
230
- // Runs once at module load, before any editor instance mounts.
231
- for (const entry of componentRegistryEntries) {
337
+ // Eager schema registration for built-ins. Customs register lazily inside
338
+ // `registerComponent()` so the store knows about every component before any
339
+ // editor instance mounts.
340
+ for (const entry of Object.values(builtInRegistry)) {
232
341
  registerComponentSchema(entry.id, entry.schema);
233
342
  }
234
343
 
235
344
  /**
236
- * Validate that the server's filesystem scan matches the registry's id list.
345
+ * Validate that the server's filesystem scan matches the merged registry's id list.
237
346
  * Logs a warning when ids drift. Called at boot from the editor page.
238
347
  */
239
348
  export function validateRegistryAgainstServerScan(serverIds: ReadonlyArray<string>): void {
240
- const registrySet = new Set<string>(componentIds);
349
+ const ids = getComponentIds();
350
+ const registrySet = new Set<string>(ids);
241
351
  const serverSet = new Set<string>(serverIds);
242
- const missingOnServer = componentIds.filter((id) => !serverSet.has(id));
352
+ const missingOnServer = ids.filter((id) => !serverSet.has(id));
243
353
  const extraOnServer = serverIds.filter((id) => !registrySet.has(id));
244
354
  if (missingOnServer.length > 0) {
245
355
  console.warn(
@@ -28,6 +28,7 @@
28
28
  import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../core/themes/migrations';
29
29
  import type { CssVarRef } from '../../core/store/editorTypes';
30
30
  import { safeFetch } from '../../core/storage/storage';
31
+ import { API_BASE } from '../../core/storage/apiBase';
31
32
  import { flashStatus } from '../../core/flashStatus';
32
33
  import ComponentFileMenu from './ComponentFileMenu.svelte';
33
34
  import SaveAsDialog from './SaveAsDialog.svelte';
@@ -91,7 +92,7 @@
91
92
  const data = await safeFetch<{
92
93
  files: ComponentConfigMeta[];
93
94
  activeFile: string;
94
- }>(`/api/component-configs/${encodeURIComponent(component)}`);
95
+ }>(`${API_BASE}/component-configs/${encodeURIComponent(component)}`);
95
96
  if (!data) return;
96
97
  files = data.files;
97
98
  activeFileName = data.activeFile;
@@ -103,7 +104,7 @@
103
104
  // Preserve existing productionInfo on transient fetch failure rather than
104
105
  // clobbering it to null — same behaviour as the previous empty catch.
105
106
  const info = await safeFetch<ComponentProductionInfo>(
106
- `/api/component-configs/${encodeURIComponent(component)}/production`,
107
+ `${API_BASE}/component-configs/${encodeURIComponent(component)}/production`,
107
108
  );
108
109
  if (info) productionInfo = info;
109
110
  }
@@ -1,13 +1,13 @@
1
1
  <script lang="ts">
2
2
  import type { ComponentSection } from './componentSectionType';
3
- import { defaultSections } from './defaultSections';
3
+ import { getDefaultSections } from './defaultSections';
4
4
 
5
5
  interface Props {
6
6
  sections?: ComponentSection[];
7
7
  selectedComponent?: string;
8
8
  }
9
9
 
10
- let { sections = defaultSections, selectedComponent = sections[0]?.id ?? '' }: Props = $props();
10
+ let { sections = getDefaultSections(), selectedComponent = sections[0]?.id ?? '' }: Props = $props();
11
11
  </script>
12
12
 
13
13
  <div class="components-container">
@@ -250,19 +250,18 @@
250
250
  padding-top: calc(var(--ui-font-size-xs) + var(--ui-space-4));
251
251
  }
252
252
 
253
- /* Element-grouped mode: subsections fan out across available width, each
254
- labeled by the element it targets (e.g. Frame / Header / Body). Three
255
- columns at typical editor widths, dropping to two then one as the panel
256
- narrows the auto-fit + minmax does the responsive work without media
257
- queries. `align-items: start` keeps columns of different heights aligned
258
- to their top edge instead of stretching the shorter ones.
253
+ /* Element-grouped mode: subsections sit flush against their content width
254
+ with a 1rem gap between them, wrapping to a new row when the panel is too
255
+ narrow to fit them side-by-side. Flex (not grid 1fr) so wide viewports
256
+ don't spread the columns apart sections cluster left and consume only
257
+ as much width as their controls need.
259
258
  Within a section the two-col split (typography fieldsets + property grid)
260
259
  still applies when the section has both. */
261
260
  .state-controls.element-grouped {
262
- display: grid;
263
- grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
264
- gap: var(--ui-space-20) var(--ui-space-32);
265
- align-items: start;
261
+ display: flex;
262
+ flex-wrap: wrap;
263
+ gap: var(--ui-space-16);
264
+ align-items: flex-start;
266
265
  }
267
266
 
268
267
  /* Each element section stacks typography fieldset(s) above the property
@@ -7,7 +7,9 @@
7
7
  import UIFontSizeSelector from '../../ui/UIFontSizeSelector.svelte';
8
8
  import UILineHeightSelector from '../../ui/UILineHeightSelector.svelte';
9
9
  import UIPaddingSelector from '../../ui/UIPaddingSelector.svelte';
10
- import { BLUR, BORDER_WIDTH, DOT_SIZE, RADIUS, SHADOW, DIVIDER_HEIGHT } from '../../ui/variantScales';
10
+ import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
11
+ import UIEasingSelector from '../../ui/UIEasingSelector.svelte';
12
+ import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SHADOW, DIVIDER_HEIGHT, DIVIDER_INSET } from '../../ui/variantScales';
11
13
  import {
12
14
  editorState,
13
15
  getComponentPropertySiblings,
@@ -26,6 +28,7 @@
26
28
  | 'radius'
27
29
  | 'divider-width'
28
30
  | 'divider-height'
31
+ | 'divider-inset'
29
32
  | 'dot-size'
30
33
  | 'blur'
31
34
  | 'shadow'
@@ -33,10 +36,13 @@
33
36
  | 'font-weight'
34
37
  | 'font-size'
35
38
  | 'line-height'
39
+ | 'letter-spacing'
36
40
  | 'padding'
37
41
  | 'padding-split'
38
42
  | 'gap'
39
- | 'extras';
43
+ | 'duration'
44
+ | 'easing'
45
+ | 'text-color';
40
46
 
41
47
  type Entry = { kind: Kind; token: Token };
42
48
 
@@ -75,44 +81,61 @@
75
81
  onchange,
76
82
  }: Props = $props();
77
83
 
78
- /** Suffix/prefix patterns mapped to kinds — single source of truth used by `categorize`.
79
- Order matters: `text` must run before `border`/`surface` because `--text-*` would
80
- otherwise match `surface` checks if any pattern overlapped. */
84
+ /** Suffix/prefix patterns mapped to kinds — single source of truth used by `rawKind`.
85
+ Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
86
+ would otherwise match `surface`/`border` if any pattern overlapped. Variables
87
+ that don't match any pattern fall through to `text-color` (renders as a palette
88
+ picker). Tokens with unconventional suffixes should be renamed. */
81
89
  const KIND_PATTERNS: Array<{ kind: Kind; matches: (v: string) => boolean }> = [
82
- { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
83
- { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
84
- { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') },
85
- { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
86
- { kind: 'extras', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
87
- { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
88
- { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
90
+ { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
91
+ { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
92
+ { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') },
93
+ { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
94
+ { kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
95
+ { kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
96
+ { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
97
+ { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
89
98
  { kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
90
- { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
91
- { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
92
- { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
93
- { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
94
- { kind: 'gap', matches: (v) => v.endsWith('-gap') },
95
- { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
96
- { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
97
- { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
99
+ { kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
100
+ { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
101
+ { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
102
+ { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
103
+ { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
104
+ { kind: 'gap', matches: (v) => v.endsWith('-gap') },
105
+ { kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
106
+ { kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
107
+ { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
108
+ { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
109
+ { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
98
110
  ];
99
111
 
112
+ function rawKind(variable: string): Kind {
113
+ for (const { kind, matches } of KIND_PATTERNS) {
114
+ if (matches(variable)) return kind;
115
+ }
116
+ return 'text-color';
117
+ }
118
+
100
119
  /** Fixed internal order for tokens within a layout. `padding-split` co-orders with `padding`. */
101
120
  const baseKindOrder: Kind[] = [
102
121
  'font-family',
103
122
  'font-weight',
104
123
  'font-size',
105
124
  'line-height',
125
+ 'letter-spacing',
106
126
  'divider-width',
107
127
  'divider-height',
128
+ 'divider-inset',
108
129
  'dot-size',
109
130
  'radius',
110
131
  'padding',
111
132
  'padding-split',
112
133
  'gap',
134
+ 'duration',
135
+ 'easing',
113
136
  'blur',
114
137
  'shadow',
115
- 'extras',
138
+ 'text-color',
116
139
  'surface',
117
140
  'border-width',
118
141
  'border',
@@ -121,13 +144,6 @@
121
144
  baseKindOrder.map((k, i) => [k, i]),
122
145
  ) as Record<Kind, number>;
123
146
 
124
- function rawKind(v: string): Kind {
125
- for (const { kind, matches } of KIND_PATTERNS) {
126
- if (matches(v)) return kind;
127
- }
128
- return 'extras';
129
- }
130
-
131
147
  /** A padding token is "split" when its per-side variables exist for this component. */
132
148
  function paddingIsSplit(varName: string, comp: string | undefined, state: typeof $editorState): boolean {
133
149
  const sides = ['top', 'right', 'bottom', 'left'];
@@ -148,8 +164,8 @@
148
164
  }
149
165
 
150
166
  /** For sibling/grouping checks we want the canonical kind, not the split-vs-single distinction. */
151
- function groupingKind(v: string): Kind {
152
- return rawKind(v);
167
+ function groupingKind(variable: string): Kind {
168
+ return rawKind(variable);
153
169
  }
154
170
 
155
171
  /** Selector registry: one entry per kind. `extra` props (e.g. UIPaddingSelector's
@@ -167,9 +183,11 @@
167
183
  'font-weight': { component: UIFontWeightSelector },
168
184
  'font-size': { component: UIFontSizeSelector },
169
185
  'line-height': { component: UILineHeightSelector },
186
+ 'letter-spacing': { component: UILetterSpacingSelector },
170
187
  'border-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
171
188
  'divider-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
172
189
  'divider-height': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_HEIGHT }) },
190
+ 'divider-inset': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_INSET }) },
173
191
  'dot-size': { component: UIVariantSelector, extra: () => ({ ...DOT_SIZE }) },
174
192
  'radius': { component: UIVariantSelector, extra: () => ({ ...RADIUS }) },
175
193
  'padding': { component: UIPaddingSelector, extra: (t) => ({ mode: 'single', splittable: t.splittable !== false }) },
@@ -184,31 +202,37 @@
184
202
  extra: () => ({ mode: 'sides' }),
185
203
  },
186
204
  'gap': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
205
+ 'duration': { component: UIVariantSelector, extra: () => ({ ...DURATION }) },
206
+ 'easing': { component: UIEasingSelector },
187
207
  'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
188
208
  'shadow': { component: UIVariantSelector, extra: () => ({ ...SHADOW }) },
189
209
  'surface': { component: UIPaletteSelector },
190
210
  'border': { component: UIPaletteSelector },
191
- 'extras': { component: UIPaletteSelector },
211
+ 'text-color': { component: UIPaletteSelector },
192
212
  };
193
213
 
194
- /** Multi-col rank: same as `orderRank` but with `extras` (text-color-like) hoisted
195
- between `line-height` and `border-width` so typography reads as one logical
196
- block in column flow. Single-col mode keeps `orderRank` (linked-first sort
197
- already segregates extras to the bottom). */
214
+ /** Multi-col rank: same as `orderRank` but with `text-color` hoisted between
215
+ `line-height` and `divider-width` so typography reads as one logical block
216
+ in column flow. Single-col mode keeps `orderRank` (linked-first sort
217
+ already segregates text-color to the bottom). */
198
218
  const multiColRank: Record<Kind, number> = (() => {
199
219
  const reordered: Kind[] = [
200
220
  'font-family',
201
221
  'font-weight',
202
222
  'font-size',
203
223
  'line-height',
204
- 'extras',
224
+ 'letter-spacing',
225
+ 'text-color',
205
226
  'divider-width',
206
227
  'divider-height',
228
+ 'divider-inset',
207
229
  'dot-size',
208
230
  'radius',
209
231
  'padding',
210
232
  'padding-split',
211
233
  'gap',
234
+ 'duration',
235
+ 'easing',
212
236
  'blur',
213
237
  'shadow',
214
238
  'surface',