@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
@@ -582,10 +582,47 @@
582
582
  }
583
583
 
584
584
  .preview-actions {
585
- margin-left: auto;
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: var(--ui-space-12);
588
+ }
589
+
590
+ /* Labels inside previewActions read at the UI body size, matching the
591
+ variant tab strip they sit beside. Greyscale only. */
592
+ .preview-actions :global(label) {
586
593
  display: inline-flex;
587
594
  align-items: center;
588
595
  gap: var(--ui-space-8);
596
+ font-size: var(--ui-font-size-md);
597
+ color: var(--ui-text-secondary);
598
+ }
599
+
600
+ /* Native <select> styled to match the property-row trigger chrome so
601
+ toolbar selects share one visual vocabulary with property dropdowns. */
602
+ .preview-actions :global(select) {
603
+ appearance: none;
604
+ -webkit-appearance: none;
605
+ padding: 0 var(--ui-space-24) 0 var(--ui-space-10);
606
+ min-height: 1.75rem;
607
+ background-color: var(--ui-surface-low);
608
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
609
+ background-repeat: no-repeat;
610
+ background-position: right var(--ui-space-8) center;
611
+ border: 1px solid var(--ui-border);
612
+ border-radius: var(--ui-radius-md);
613
+ color: var(--ui-text-primary);
614
+ font-family: var(--ui-font-sans);
615
+ font-size: var(--ui-font-size-md);
616
+ cursor: pointer;
617
+ transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
618
+ }
619
+ .preview-actions :global(select:hover) {
620
+ background-color: var(--ui-surface-high);
621
+ border-color: var(--ui-border-higher);
622
+ }
623
+ .preview-actions :global(select:focus-visible) {
624
+ outline: 2px solid var(--ui-highlight);
625
+ outline-offset: 2px;
589
626
  }
590
627
 
591
628
  /* Sits in the backdrop's right-rail column (ShadowBackdrop owns the two-column
@@ -31,7 +31,7 @@ export type BuildTypeGroupTokensOptions = {
31
31
  out of the linked block while still appearing in the editor's full token surface
32
32
  (used by the reset-button and the design-token resolution test).
33
33
 
34
- Mirrors the `flatMap`/loop pattern in StandardButtonsEditor and RadioButtonEditor so
34
+ Mirrors the `flatMap`/loop pattern in ButtonEditor and RadioButtonEditor so
35
35
  editors don't have to hand-list 16+ near-identical Token entries. */
36
36
  export function buildTypeGroupTokens(
37
37
  typeGroups: Record<string, TypeGroupConfig[]>,
@@ -1,9 +1,9 @@
1
- import { componentRegistry } from '../registry';
1
+ import { getComponentRegistry } from '../registry';
2
2
 
3
3
  /**
4
4
  * Resolve a component id to its runtime source file path. Reads from the
5
- * single component registry no parallel mapping to maintain.
5
+ * merged component registry (built-ins + runtime registrations).
6
6
  */
7
7
  export function componentSourceFile(component: string): string {
8
- return componentRegistry[component as keyof typeof componentRegistry]?.sourceFile ?? '';
8
+ return getComponentRegistry()[component]?.sourceFile ?? '';
9
9
  }
@@ -1,16 +1,21 @@
1
1
  import type { ComponentSection } from './componentSectionType';
2
- import { componentRegistryEntries } from '../registry';
2
+ import { getComponentRegistryEntries } from '../registry';
3
3
 
4
4
  /**
5
- * Default editor sections — derived from the single component registry. Each
5
+ * Default editor sections — derived from the merged component registry. Each
6
6
  * section's `id` is the canonical lowercase component id (matches the runtime
7
- * filename, server scan, and `setComponentAlias` key); `label` is the
8
- * display string; `component` is the editor Svelte component.
7
+ * filename, server scan, and `setComponentAlias` key); `label` is the display
8
+ * string; `component` is the editor Svelte component.
9
9
  *
10
- * To add or reorder sections, edit `src/component-editor/registry.ts`.
10
+ * Recomputed on each call so consumer-registered components (added via
11
+ * `registerComponent()`) appear after the first-party set in iteration order.
12
+ *
13
+ * To add or reorder first-party sections, edit `src/editor/component-editor/registry.ts`.
11
14
  */
12
- export const defaultSections: ComponentSection[] = componentRegistryEntries.map((entry) => ({
13
- id: entry.id,
14
- label: entry.label,
15
- component: entry.editorComponent,
16
- }));
15
+ export function getDefaultSections(): ComponentSection[] {
16
+ return getComponentRegistryEntries().map((entry) => ({
17
+ id: entry.id,
18
+ label: entry.label,
19
+ component: entry.editorComponent,
20
+ }));
21
+ }
@@ -13,8 +13,8 @@ export type Sibling = {
13
13
 
14
14
  `variantStates(v)` and `variantTypeGroups(v)` return the same shape the
15
15
  parent VariantGroup gets for its own `states` / `typeGroups` props — a map
16
- keyed by state name. For single-state-per-variant editors (Badge, Notification,
17
- ProgressBar), wrap the single-state builders inline:
16
+ keyed by state name. For single-state-per-variant editors (Badge,
17
+ Notification), wrap the single-state builders inline:
18
18
  `(v) => ({ [v]: variantTokens(v) })`. */
19
19
  export function buildSiblings<V extends string>(
20
20
  variants: readonly V[],
@@ -27,7 +27,8 @@ export type Token = {
27
27
  /** Hint to the editor that this token's alias is a structured payload
28
28
  (currently only `kind: 'gradient'`). Drives Copy-from's per-kind
29
29
  branch — gradient aliases need family-swap of in-family stop colors
30
- rather than a verbatim ref copy. */
30
+ rather than a verbatim ref copy. Distinct from `picker`: `kind`
31
+ marks the value's data shape; `picker` selects the editor control. */
31
32
  kind?: 'gradient';
32
33
  /** Color-family slug for this token's owning variant (e.g. `brand`,
33
34
  `accent`). Set on gradient-kind tokens so Copy-from's family-swap
@@ -4,9 +4,11 @@
4
4
  // migration that splits legacy single-bucket aliases into the new
5
5
  // {aliases, config} shape.
6
6
  //
7
- // What goes here: literal-valued knobs that don't translate to CSS vars
8
- // (e.g. Dialog's confirm/cancel variant string is consumed by Dialog.svelte
9
- // via `$editorState`, not via CSS cascade).
7
+ // What goes here: literal-valued knobs that live in the config bucket rather
8
+ // than the alias bucket. Some are runtime CSS values consumed by live
9
+ // components via the cascade (see CASCADING_COMPONENT_CONFIG_KEYS below);
10
+ // others are editor-only metadata that drive alias rewrites without ever
11
+ // reaching :root.
10
12
  //
11
13
  // What does NOT go here: aliases whose values are themselves CSS-var refs
12
14
  // — even if the value space is constrained (e.g. `--button-shimmer` →
@@ -25,3 +27,12 @@ export const KNOWN_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
25
27
  '--sectiondivider-md-color-family',
26
28
  '--sectiondivider-sm-color-family',
27
29
  ]);
30
+
31
+ // Subset of KNOWN_COMPONENT_CONFIG_KEYS that the renderer emits to :root as
32
+ // CSS vars so live components can read them via the cascade. Editor-only
33
+ // metadata (e.g. `--sectiondivider-*-color-family`, which drives an alias
34
+ // rewrite rather than a runtime value) is intentionally excluded.
35
+ export const CASCADING_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
36
+ '--dialog-confirm-variant',
37
+ '--dialog-cancel-variant',
38
+ ]);
@@ -1,12 +1,13 @@
1
1
  import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
2
2
  import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
3
+ import { API_BASE } from '../storage/apiBase';
3
4
 
4
5
  /**
5
6
  * REST client for per-component config files. Parallel to `themeService.ts`
6
- * but scoped to `/api/component-configs/*`. Each component (button, card, …)
7
- * has its own lifecycle: default.json (generated from the `.svelte` source),
8
- * plus user-authored named configs, each with its own active / production
9
- * pointer.
7
+ * but scoped to `${API_BASE}/component-configs/*`. Each component (button,
8
+ * card, …) has its own lifecycle: default.json (generated from the `.svelte`
9
+ * source), plus user-authored named configs, each with its own active /
10
+ * production pointer.
10
11
  *
11
12
  * Both this and `themeService` consume `versionedFileResource(...)`. Adding a
12
13
  * third file-managed resource — per the user's "mirror theme-file lifecycle
@@ -34,7 +35,7 @@ export interface ComponentConfigList {
34
35
  }
35
36
 
36
37
  export async function listComponents(): Promise<ComponentSummary[]> {
37
- const res = await fetch('/api/component-configs');
38
+ const res = await fetch(`${API_BASE}/component-configs`);
38
39
  if (!res.ok) throw new Error('Failed to list components');
39
40
  const data = await res.json();
40
41
  return data.components;
@@ -42,7 +43,7 @@ export async function listComponents(): Promise<ComponentSummary[]> {
42
43
 
43
44
  function resourceFor(component: string) {
44
45
  return versionedFileResource<ComponentConfig, ComponentConfigMeta, ComponentProductionInfo>({
45
- baseUrl: `/api/component-configs/${encodeURIComponent(component)}`,
46
+ baseUrl: `${API_BASE}/component-configs/${encodeURIComponent(component)}`,
46
47
  });
47
48
  }
48
49
 
@@ -1,5 +1,6 @@
1
1
  import type { Manifest, ManifestMeta, ManifestBundle, Theme, ComponentConfig } from '../themes/themeTypes';
2
2
  import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
3
+ import { API_BASE } from '../storage/apiBase';
3
4
  import { listComponents } from '../components/componentConfigService';
4
5
  import { getActiveTheme } from '../themes/themeService';
5
6
 
@@ -13,7 +14,7 @@ import { getActiveTheme } from '../themes/themeService';
13
14
  */
14
15
 
15
16
  const manifestsResource = versionedFileResource<Manifest, ManifestMeta, never>({
16
- baseUrl: '/api/manifests',
17
+ baseUrl: `${API_BASE}/manifests`,
17
18
  });
18
19
 
19
20
  export const listManifests = async (): Promise<ManifestMeta[]> => {
@@ -47,7 +48,7 @@ export interface ApplyManifestResult {
47
48
  * world" action.
48
49
  */
49
50
  export async function applyManifest(fileName: string): Promise<ApplyManifestResult> {
50
- const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/apply`, {
51
+ const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/apply`, {
51
52
  method: 'PUT',
52
53
  });
53
54
  if (!res.ok) {
@@ -132,7 +133,7 @@ export interface ImportManifestResult {
132
133
  * See temp/manifest-robustness-plan.md §11.
133
134
  */
134
135
  export async function exportManifest(fileName: string): Promise<void> {
135
- const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/export`);
136
+ const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/export`);
136
137
  if (!res.ok) {
137
138
  const err = await res.json().catch(() => ({ error: 'Export failed' }));
138
139
  throw new Error(err.error || 'Export failed');
@@ -158,7 +159,7 @@ export async function exportManifest(fileName: string): Promise<void> {
158
159
  * the rename map so the UI can surface what got renamed.
159
160
  */
160
161
  export async function importManifest(bundle: ManifestBundle): Promise<ImportManifestResult> {
161
- const res = await fetch('/api/manifests/import', {
162
+ const res = await fetch(`${API_BASE}/manifests/import`, {
162
163
  method: 'POST',
163
164
  headers: { 'Content-Type': 'application/json' },
164
165
  body: JSON.stringify(bundle),
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolved API base path for the editor's REST client. Injected by the
3
+ * `themeFileApi` vite plugin via `config().define` so the client and server
4
+ * always agree on the route prefix, even when a consumer overrides `apiBase`.
5
+ *
6
+ * Fallback (no plugin running) matches the plugin's default. The strings must
7
+ * stay in sync — there is no single source of truth across the build-time /
8
+ * runtime boundary, but the cost of one constant in two files is small.
9
+ */
10
+ declare const __LIVE_TOKENS_API_BASE__: string | undefined;
11
+
12
+ export const API_BASE: string =
13
+ typeof __LIVE_TOKENS_API_BASE__ !== 'undefined' && __LIVE_TOKENS_API_BASE__
14
+ ? __LIVE_TOKENS_API_BASE__
15
+ : '/api/live-tokens';
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  export interface VersionedFileResourceClientOptions {
15
- /** REST endpoint root, e.g. `/api/themes` or `/api/component-configs/button`. */
15
+ /** REST endpoint root, e.g. `${API_BASE}/themes` or `${API_BASE}/component-configs/button`. */
16
16
  baseUrl: string;
17
17
  }
18
18
 
@@ -0,0 +1,28 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * 2026-05-24: drop the `active` header state from collapsiblesection.
5
+ *
6
+ * The state only paid rent when the component was used as a nav link (an
7
+ * externally-supplied `active={true}` paired with `href`). No consumer was
8
+ * using it, so the runtime now omits both the prop and the `&.active` CSS
9
+ * branch. Strip the matching saved aliases from every variant.
10
+ */
11
+ function dropActiveState(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
12
+ if (meta.component !== 'collapsiblesection') return rawVars;
13
+ const re = /^--collapsiblesection-(chromeless|divider|container)-active-/;
14
+ const out: Record<string, string> = {};
15
+ for (const [key, value] of Object.entries(rawVars)) {
16
+ if (re.test(key)) continue;
17
+ out[key] = value;
18
+ }
19
+ return out;
20
+ }
21
+
22
+ export const componentMigration_2026_05_24_collapsiblesectionDropActiveState: Migration = {
23
+ id: '2026-05-24-collapsiblesection-drop-active-state',
24
+ fromVersion: 14,
25
+ toVersion: 15,
26
+ appliesTo: 'component-config',
27
+ apply: dropActiveState,
28
+ };
@@ -0,0 +1,41 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-05-24, progressbar only): ProgressBar
5
+ * collapsed from a per-variant token namespace (primary/success/warning/
6
+ * danger/info) to a single token set. Fill color is now a runtime `fill`
7
+ * prop on the consumer side, not a variant axis.
8
+ *
9
+ * Strategy: keep the `primary` namespace's values as the canonical defaults
10
+ * (rename `--progressbar-primary-X` → `--progressbar-X`); drop the four
11
+ * remaining variant namespaces entirely. Consumers that previously selected
12
+ * a non-primary variant lose their variant-specific surface/border/fill
13
+ * customizations and fall back to the merged single set.
14
+ */
15
+ const PRIMARY_PREFIX = '--progressbar-primary-';
16
+ const NEW_PREFIX = '--progressbar-';
17
+ const DROP_PREFIXES = [
18
+ '--progressbar-success-',
19
+ '--progressbar-warning-',
20
+ '--progressbar-danger-',
21
+ '--progressbar-info-',
22
+ ];
23
+
24
+ export const componentMigration_2026_05_24_progressbarCollapseVariants: Migration = {
25
+ id: '2026-05-24-progressbar-collapse-variants',
26
+ fromVersion: 13,
27
+ toVersion: 14,
28
+ appliesTo: 'component-config',
29
+ apply(rawVars, meta) {
30
+ if (meta.component !== 'progressbar') return { ...rawVars };
31
+ const out: Record<string, string> = {};
32
+ for (const [oldKey, value] of Object.entries(rawVars)) {
33
+ if (DROP_PREFIXES.some((p) => oldKey.startsWith(p))) continue;
34
+ const key = oldKey.startsWith(PRIMARY_PREFIX)
35
+ ? NEW_PREFIX + oldKey.slice(PRIMARY_PREFIX.length)
36
+ : oldKey;
37
+ if (!(key in out)) out[key] = value;
38
+ }
39
+ return out;
40
+ },
41
+ };
@@ -0,0 +1,59 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-05-24, button + segmentedcontrol):
5
+ *
6
+ * Promotes shape/icon-size properties that don't actually vary per state into
7
+ * their own "base" part. The editor's per-state shape rows were dead UI — the
8
+ * runtime treated them as separate tokens that were always linked anyway. The
9
+ * runtime CSS now reads the default-state token in hover/disabled rules, so
10
+ * the per-state tokens are dropped entirely.
11
+ *
12
+ * Saved per-state overrides are dropped (rare case; users re-customize once).
13
+ * The default-state value remains the authoritative one.
14
+ *
15
+ * button drops --button-{v}-{hover|disabled}-{padding|radius|border-width}
16
+ * for v in primary/secondary/outline/success/danger/warning.
17
+ *
18
+ * segmentedcontrol drops --segmentedcontrol-{selected|option-hover|disabled}-icon-size.
19
+ */
20
+
21
+ const BUTTON_VARIANTS = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'] as const;
22
+ const BUTTON_STATES = ['hover', 'disabled'] as const;
23
+ const BUTTON_PROPS = ['padding', 'radius', 'border-width'] as const;
24
+
25
+ const buttonDeadKeys = new Set<string>(
26
+ BUTTON_VARIANTS.flatMap((v) =>
27
+ BUTTON_STATES.flatMap((s) =>
28
+ BUTTON_PROPS.map((p) => `--button-${v}-${s}-${p}`),
29
+ ),
30
+ ),
31
+ );
32
+
33
+ const segmentedcontrolDeadKeys = new Set<string>([
34
+ '--segmentedcontrol-selected-icon-size',
35
+ '--segmentedcontrol-option-hover-icon-size',
36
+ '--segmentedcontrol-disabled-icon-size',
37
+ ]);
38
+
39
+ export const componentMigration_2026_05_24_promoteStateSharedTokens: Migration = {
40
+ id: '2026-05-24-promote-state-shared-tokens',
41
+ fromVersion: 12,
42
+ toVersion: 13,
43
+ appliesTo: 'component-config',
44
+ apply(rawVars, meta) {
45
+ const dead =
46
+ meta.component === 'button'
47
+ ? buttonDeadKeys
48
+ : meta.component === 'segmentedcontrol'
49
+ ? segmentedcontrolDeadKeys
50
+ : null;
51
+ if (!dead) return { ...rawVars };
52
+ const out: Record<string, string> = {};
53
+ for (const [key, value] of Object.entries(rawVars)) {
54
+ if (dead.has(key)) continue;
55
+ out[key] = value;
56
+ }
57
+ return out;
58
+ },
59
+ };
@@ -0,0 +1,29 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-05-24, segmentedcontrol only):
5
+ * `--segmentedcontrol-divider-height` was retired in favor of
6
+ * `--segmentedcontrol-divider-inset`. The old token set a percentage / pixel
7
+ * height on a centered flex item; with `Full` (100%) it collapsed to zero in
8
+ * auto-sized inline-flex parents. The new token is `margin-block` trimmed
9
+ * from a stretched divider, so Full = 0 inset = bar-height divider.
10
+ *
11
+ * Value semantic flipped (larger old value meant taller divider; larger new
12
+ * value means shorter divider), so the saved customization is dropped rather
13
+ * than copied across. Users fall back to the new default and re-pick once.
14
+ */
15
+ export const componentMigration_2026_05_24_segmentedcontrolDividerInset: Migration = {
16
+ id: '2026-05-24-segmentedcontrol-divider-inset',
17
+ fromVersion: 11,
18
+ toVersion: 12,
19
+ appliesTo: 'component-config',
20
+ apply(rawVars, meta) {
21
+ if (meta.component !== 'segmentedcontrol') return { ...rawVars };
22
+ const out: Record<string, string> = {};
23
+ for (const [key, value] of Object.entries(rawVars)) {
24
+ if (key === '--segmentedcontrol-divider-height') continue;
25
+ out[key] = value;
26
+ }
27
+ return out;
28
+ },
29
+ };
@@ -0,0 +1,46 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * 2026-05-25: drop the per-variant token axis from cornerbadge.
5
+ *
6
+ * CornerBadge's "variants" carried only shape + spacing + type aliases — never
7
+ * colors (those come from the composed Badge). With every variant's defaults
8
+ * identical, the axis was 10× duplication that the editor had to tile across
9
+ * a variant strip for no semantic gain. Collapsing to a single flat token set
10
+ * removes the strip and the duplication.
11
+ *
12
+ * For each `--corner-badge-{variant}-{prop}`, fold into `--corner-badge-{prop}`.
13
+ * First-occurrence wins on conflict — defaults were uniform so this is a no-op
14
+ * in the typical case, and any tuned divergence is rare enough that picking
15
+ * one is better than silently averaging.
16
+ */
17
+ const VARIANTS = ['primary', 'accent', 'neutral', 'alternate', 'canvas', 'special', 'success', 'warning', 'danger', 'info'] as const;
18
+ const RE = new RegExp(`^--corner-badge-(${VARIANTS.join('|')})-(.+)$`);
19
+
20
+ function flattenVariants(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
21
+ if (meta.component !== 'cornerbadge') return rawVars;
22
+ const out: Record<string, string> = {};
23
+ const collected: Map<string, string> = new Map();
24
+ for (const [key, value] of Object.entries(rawVars)) {
25
+ const m = key.match(RE);
26
+ if (m) {
27
+ const prop = m[2];
28
+ if (!collected.has(prop)) collected.set(prop, value);
29
+ continue;
30
+ }
31
+ out[key] = value;
32
+ }
33
+ for (const [prop, value] of collected) {
34
+ const flatKey = `--corner-badge-${prop}`;
35
+ if (!(flatKey in out)) out[flatKey] = value;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ export const componentMigration_2026_05_25_cornerbadgeFlattenVariants: Migration = {
41
+ id: '2026-05-25-cornerbadge-flatten-variants',
42
+ fromVersion: 15,
43
+ toVersion: 16,
44
+ appliesTo: 'component-config',
45
+ apply: flattenVariants,
46
+ };
@@ -45,6 +45,11 @@ import { componentMigration_2026_05_19_sectiondividerRichGradient } from './2026
45
45
  import { componentMigration_2026_05_20_sectiondividerSlimVariants } from './2026-05-20-sectiondivider-slim-variants';
46
46
  import { componentMigration_2026_05_21_sectiondividerSpacingToPadding } from './2026-05-21-sectiondivider-spacing-to-padding';
47
47
  import { componentMigration_2026_05_22_sectiondividerIntrinsicsToCss } from './2026-05-22-sectiondivider-intrinsics-to-css';
48
+ import { componentMigration_2026_05_24_segmentedcontrolDividerInset } from './2026-05-24-segmentedcontrol-divider-inset';
49
+ import { componentMigration_2026_05_24_promoteStateSharedTokens } from './2026-05-24-promote-state-shared-tokens';
50
+ import { componentMigration_2026_05_24_progressbarCollapseVariants } from './2026-05-24-progressbar-collapse-variants';
51
+ import { componentMigration_2026_05_24_collapsiblesectionDropActiveState } from './2026-05-24-collapsiblesection-drop-active-state';
52
+ import { componentMigration_2026_05_25_cornerbadgeFlattenVariants } from './2026-05-25-cornerbadge-flatten-variants';
48
53
 
49
54
  /**
50
55
  * Registered migrations. Order in this array does not matter — the runner
@@ -64,6 +69,11 @@ export const MIGRATIONS: Migration[] = [
64
69
  componentMigration_2026_05_20_sectiondividerSlimVariants,
65
70
  componentMigration_2026_05_21_sectiondividerSpacingToPadding,
66
71
  componentMigration_2026_05_22_sectiondividerIntrinsicsToCss,
72
+ componentMigration_2026_05_24_segmentedcontrolDividerInset,
73
+ componentMigration_2026_05_24_promoteStateSharedTokens,
74
+ componentMigration_2026_05_24_progressbarCollapseVariants,
75
+ componentMigration_2026_05_24_collapsiblesectionDropActiveState,
76
+ componentMigration_2026_05_25_cornerbadgeFlattenVariants,
67
77
  ];
68
78
 
69
79
  function countFor(kind: 'theme' | 'component-config'): number {
@@ -33,6 +33,7 @@ import { writable, derived, get, type Readable } from 'svelte/store';
33
33
  import type { CssVarRef, EditorState } from '../../store/editorTypes';
34
34
  import { store, mutate } from '../../store/editorCore';
35
35
  import { formatGradientValue } from './gradients';
36
+ import { CASCADING_COMPONENT_CONFIG_KEYS } from '../../components/componentConfigKeys';
36
37
 
37
38
  const EMPTY_COMPONENT_BASELINE = JSON.stringify({ aliases: {}, config: {} });
38
39
 
@@ -48,6 +49,11 @@ export function componentsToVars(components: EditorState['components']): Record<
48
49
  else if (ref.kind === 'literal') out[varName] = ref.value;
49
50
  else out[varName] = formatGradientValue(ref.value);
50
51
  }
52
+ for (const [key, value] of Object.entries(slice.config)) {
53
+ if (CASCADING_COMPONENT_CONFIG_KEYS.has(key) && typeof value === 'string') {
54
+ out[key] = value;
55
+ }
56
+ }
51
57
  }
52
58
  return out;
53
59
  }
@@ -56,6 +62,9 @@ export function getComponentOwnedVarNames(state: EditorState): string[] {
56
62
  const names: string[] = [];
57
63
  for (const slice of Object.values(state.components)) {
58
64
  for (const name of Object.keys(slice.aliases)) names.push(name);
65
+ for (const key of Object.keys(slice.config)) {
66
+ if (CASCADING_COMPONENT_CONFIG_KEYS.has(key)) names.push(key);
67
+ }
59
68
  }
60
69
  return names;
61
70
  }
@@ -5,6 +5,7 @@ import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
5
5
  import { loadFromFile, seedComponentsFromApi } from '../store/editorStore';
6
6
  import { getActiveComponentConfig } from '../components/componentConfigService';
7
7
  import { safeFetch } from '../storage/storage';
8
+ import { API_BASE } from '../storage/apiBase';
8
9
 
9
10
  interface ComponentSummaryDto {
10
11
  name: string;
@@ -34,7 +35,7 @@ interface ListComponentsDto {
34
35
  * `safeFetch` (instead of empty try/catch) to make the silence intentional.
35
36
  */
36
37
  export async function initializeTheme(): Promise<void> {
37
- const theme = await safeFetch<Theme>('/api/themes/active');
38
+ const theme = await safeFetch<Theme>(`${API_BASE}/themes/active`);
38
39
  if (theme) {
39
40
  migrateThemeFonts(theme);
40
41
  loadFromFile(theme);
@@ -48,7 +49,7 @@ export async function initializeTheme(): Promise<void> {
48
49
  activeFileName.set(fileName);
49
50
  }
50
51
 
51
- const list = await safeFetch<ListComponentsDto>('/api/component-configs');
52
+ const list = await safeFetch<ListComponentsDto>(`${API_BASE}/component-configs`);
52
53
  if (list && Array.isArray(list.components)) {
53
54
  const configs: Record<
54
55
  string,
@@ -5,6 +5,7 @@ import {
5
5
  versionedFileResource,
6
6
  sanitizeFileName as sanitizeFileNameImpl,
7
7
  } from '../storage/files/versionedFileResourceClient';
8
+ import { API_BASE } from '../storage/apiBase';
8
9
  import { loadFromFile as loadEditorState, toTheme, markSaved } from '../store/editorStore';
9
10
  import { activeFileName } from '../store/editorConfigStore';
10
11
  import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
@@ -12,7 +13,7 @@ import { migrateThemeFonts } from '../fonts/fontMigration';
12
13
 
13
14
  // ── API helpers ──────────────────────────────────────────────
14
15
  //
15
- // All theme CRUD goes through `versionedFileResource('/api/themes')` —
16
+ // All theme CRUD goes through `versionedFileResource(`${API_BASE}/themes`)` —
16
17
  // shared with `componentConfigService`'s per-component clients. Theme-specific
17
18
  // response shapes (ThemeMeta list payload, ProductionInfo) are layered on top
18
19
  // via the generic type parameters.
@@ -25,7 +26,7 @@ export interface ProductionInfo {
25
26
  }
26
27
 
27
28
  const themeResource = versionedFileResource<Theme, ThemeMeta, ProductionInfo>({
28
- baseUrl: '/api/themes',
29
+ baseUrl: `${API_BASE}/themes`,
29
30
  });
30
31
 
31
32
  export async function listThemes(): Promise<ThemeMeta[]> {
@@ -7,7 +7,13 @@ export { configureEditor, storageKey } from './core/store/editorConfig';
7
7
  export { activeFileName } from './core/store/editorConfigStore';
8
8
  export { init as initRouter, route, navigate } from './core/routing/router';
9
9
  export { init as initCssVarSync } from './core/cssVarSync';
10
- export { init as initEditorStore } from './core/store/editorStore';
10
+ export {
11
+ init as initEditorStore,
12
+ editorState,
13
+ setComponentAlias,
14
+ setComponentConfig,
15
+ registerComponentSchema,
16
+ } from './core/store/editorStore';
11
17
 
12
18
  export { setCssVar, removeCssVar } from './core/cssVarSync';
13
19
 
@@ -67,3 +73,6 @@ export { hexToOklch, oklchToHex, gamutClamp } from './core/palettes/oklch';
67
73
  export type { Oklch } from './core/palettes/oklch';
68
74
 
69
75
  export { initializeTheme } from './core/themes/themeInit';
76
+
77
+ export { registerComponent } from './component-editor/registry';
78
+ export type { RegisterComponentEntry, RegistryEntry, ComponentId } from './component-editor/registry';