@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.
- package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
- package/README.md +84 -29
- package/dist-plugin/index.cjs +177 -125
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +177 -125
- package/package.json +8 -2
- package/src/editor/component-editor/BadgeEditor.svelte +44 -42
- package/src/editor/component-editor/ButtonEditor.svelte +224 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
- package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
- package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
- package/src/editor/component-editor/InputEditor.svelte +272 -0
- package/src/editor/component-editor/NotificationEditor.svelte +44 -65
- package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
- package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
- package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +138 -28
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
- package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
- package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
- package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +2 -1
- package/src/editor/core/components/componentConfigKeys.ts +14 -3
- package/src/editor/core/components/componentConfigService.ts +7 -6
- package/src/editor/core/manifests/manifestService.ts +5 -4
- package/src/editor/core/storage/apiBase.ts +15 -0
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
- package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
- package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
- package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +9 -0
- package/src/editor/core/themes/themeInit.ts +3 -2
- package/src/editor/core/themes/themeService.ts +3 -2
- package/src/editor/index.ts +10 -1
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/ui/UIEasingSelector.svelte +240 -0
- package/src/editor/ui/variantScales.ts +34 -0
- package/src/system/components/Button.svelte +34 -85
- package/src/system/components/CollapsibleSection.svelte +1 -48
- package/src/system/components/CornerBadge.svelte +72 -138
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/ImageLightbox.svelte +578 -0
- package/src/system/components/Input.svelte +387 -0
- package/src/system/components/ProgressBar.svelte +62 -258
- package/src/system/components/SectionDivider.svelte +117 -43
- package/src/system/components/SegmentedControl.svelte +81 -15
- package/src/system/components/SideNavigation.svelte +777 -0
- package/src/system/styles/tokens.css +43 -0
- package/src/system/styles/tokens.generated.css +4 -183
- package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
|
@@ -582,10 +582,47 @@
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
.preview-actions {
|
|
585
|
-
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
5
|
+
* merged component registry (built-ins + runtime registrations).
|
|
6
6
|
*/
|
|
7
7
|
export function componentSourceFile(component: string): string {
|
|
8
|
-
return
|
|
8
|
+
return getComponentRegistry()[component]?.sourceFile ?? '';
|
|
9
9
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { ComponentSection } from './componentSectionType';
|
|
2
|
-
import {
|
|
2
|
+
import { getComponentRegistryEntries } from '../registry';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Default editor sections — derived from the
|
|
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
|
-
*
|
|
7
|
+
* filename, server scan, and `setComponentAlias` key); `label` is the display
|
|
8
|
+
* string; `component` is the editor Svelte component.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
17
|
-
|
|
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
|
|
8
|
-
//
|
|
9
|
-
// via
|
|
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
|
|
7
|
-
* has its own lifecycle: default.json (generated from the `.svelte`
|
|
8
|
-
* plus user-authored named configs, each with its own active /
|
|
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(
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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>(
|
|
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>(
|
|
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(
|
|
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:
|
|
29
|
+
baseUrl: `${API_BASE}/themes`,
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
export async function listThemes(): Promise<ThemeMeta[]> {
|
package/src/editor/index.ts
CHANGED
|
@@ -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 {
|
|
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';
|