@motion-proto/live-tokens 0.1.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -21
- package/dist-plugin/index.cjs +823 -336
- package/dist-plugin/index.d.cts +9 -7
- package/dist-plugin/index.d.ts +9 -7
- package/dist-plugin/index.js +822 -335
- package/package.json +46 -20
- package/src/assets/newspaper.webp +0 -0
- package/src/assets/offering.webp +0 -0
- package/src/component-editor/BadgeEditor.svelte +170 -0
- package/src/component-editor/CalloutEditor.svelte +103 -0
- package/src/component-editor/CardEditor.svelte +184 -0
- package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
- package/src/component-editor/CornerBadgeEditor.svelte +207 -0
- package/src/component-editor/DialogEditor.svelte +172 -0
- package/src/component-editor/ImageEditor.svelte +72 -0
- package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
- package/src/component-editor/NotificationEditor.svelte +160 -0
- package/src/component-editor/ProgressBarEditor.svelte +124 -0
- package/src/component-editor/RadioButtonEditor.svelte +140 -0
- package/src/component-editor/SectionDividerEditor.svelte +263 -0
- package/src/component-editor/SegmentedControlEditor.svelte +154 -0
- package/src/component-editor/StandardButtonsEditor.svelte +178 -0
- package/src/component-editor/TabBarEditor.svelte +137 -0
- package/src/component-editor/TableEditor.svelte +128 -0
- package/src/component-editor/TooltipEditor.svelte +122 -0
- package/src/component-editor/editorTokens.test.ts +93 -0
- package/src/component-editor/groupKeySlots.test.ts +67 -0
- package/src/component-editor/groupKeySnapshot.test.ts +52 -0
- package/src/component-editor/index.ts +5 -0
- package/src/component-editor/registry.ts +246 -0
- package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
- package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
- package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
- package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
- package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
- package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
- package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
- package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
- package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
- package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
- package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
- package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
- package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
- package/src/component-editor/scaffolding/componentSources.ts +9 -0
- package/src/component-editor/scaffolding/defaultSections.ts +16 -0
- package/src/component-editor/scaffolding/editorContext.ts +44 -0
- package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
- package/src/component-editor/scaffolding/siblings.ts +33 -0
- package/src/component-editor/scaffolding/types.ts +39 -0
- package/src/components/Badge.svelte +231 -42
- package/src/components/Button.svelte +324 -124
- package/src/components/Callout.svelte +145 -0
- package/src/components/Card.svelte +123 -25
- package/src/components/CollapsibleSection.svelte +213 -35
- package/src/components/CornerBadge.svelte +224 -0
- package/src/components/Dialog.svelte +137 -114
- package/src/components/Image.svelte +43 -0
- package/src/components/InlineEditActions.svelte +74 -14
- package/src/components/Notification.svelte +184 -163
- package/src/components/ProgressBar.svelte +216 -22
- package/src/components/RadioButton.svelte +110 -40
- package/src/components/SectionDivider.svelte +428 -74
- package/src/components/SegmentedControl.svelte +203 -0
- package/src/components/TabBar.svelte +146 -21
- package/src/components/Table.svelte +102 -0
- package/src/components/Tooltip.svelte +45 -19
- package/src/components/types.ts +51 -0
- package/src/data/google-fonts.json +75 -0
- package/src/lib/ColumnsOverlay.svelte +20 -7
- package/src/lib/LiveEditorOverlay.svelte +257 -78
- package/src/lib/columnsOverlay.ts +21 -17
- package/src/lib/componentConfig.test.ts +204 -0
- package/src/lib/componentConfigKeys.ts +19 -0
- package/src/lib/componentConfigService.ts +88 -0
- package/src/lib/copyPopover.ts +30 -0
- package/src/lib/cssVarSync.ts +59 -7
- package/src/lib/editorConfigStore.ts +0 -10
- package/src/lib/editorCore.ts +402 -0
- package/src/lib/editorKeybindings.ts +52 -0
- package/src/lib/editorPersistence.ts +106 -0
- package/src/lib/editorRenderer.ts +74 -0
- package/src/lib/editorStore.test.ts +328 -0
- package/src/lib/editorStore.ts +412 -0
- package/src/lib/editorTypes.ts +100 -0
- package/src/lib/editorViewStore.ts +55 -0
- package/src/lib/files/versionedFileResource.ts +140 -0
- package/src/lib/fontLoader.ts +130 -0
- package/src/lib/fontMigration.ts +140 -0
- package/src/lib/fontParse.ts +168 -0
- package/src/lib/index.ts +48 -30
- package/src/lib/lazyConfig.test.ts +54 -0
- package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
- package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
- package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
- package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
- package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
- package/src/lib/migrations/index.ts +93 -0
- package/src/lib/migrations/migrations.test.ts +341 -0
- package/src/lib/navLinkTypes.ts +1 -0
- package/src/lib/overlayState.ts +3 -0
- package/src/lib/paletteDerivation.ts +300 -0
- package/src/lib/parentRouteStore.ts +42 -0
- package/src/lib/parsers/globalRootBlock.ts +32 -0
- package/src/lib/presetService.ts +94 -0
- package/src/lib/router.ts +42 -10
- package/src/lib/scrollSection.ts +45 -0
- package/src/lib/slices/columns.ts +59 -0
- package/src/lib/slices/components.ts +362 -0
- package/src/lib/slices/domainVars.ts +15 -0
- package/src/lib/slices/fonts.ts +30 -0
- package/src/lib/slices/gradients.ts +153 -0
- package/src/lib/slices/overlays.ts +132 -0
- package/src/lib/slices/palettes.ts +26 -0
- package/src/lib/slices/shadows.ts +123 -0
- package/src/lib/storage.ts +88 -0
- package/src/lib/themeInit.ts +74 -0
- package/src/lib/themeService.ts +101 -0
- package/src/lib/themeTypes.ts +146 -0
- package/src/lib/tokenRegistry.ts +148 -0
- package/src/pages/ComponentEditorPage.svelte +384 -0
- package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
- package/src/pages/Editor.svelte +98 -0
- package/src/pages/Editor.svelte.d.ts +2 -0
- package/src/pages/EditorShell.svelte +348 -0
- package/src/styles/_padding.scss +34 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
- package/src/styles/fonts.css +22 -10
- package/src/styles/form-controls.css +14 -16
- package/src/styles/tokens.css +1322 -0
- package/src/styles/ui-editor.css +126 -0
- package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
- package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
- package/src/ui/EditorViewSwitcher.svelte +180 -0
- package/src/ui/FontStackEditor.svelte +360 -0
- package/src/ui/GradientEditor.svelte +461 -0
- package/src/ui/GradientStopPicker.svelte +74 -0
- package/src/ui/PaletteEditor.svelte +1590 -0
- package/src/ui/PaletteEditor.test.ts +108 -0
- package/src/ui/PresetFileManager.svelte +567 -0
- package/src/ui/ProjectFontsSection.svelte +645 -0
- package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
- package/src/{showcase → ui}/TextTab.svelte +27 -27
- package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
- package/src/ui/Toggle.svelte +108 -0
- package/src/ui/UICopyPopover.svelte +78 -0
- package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
- package/src/ui/UIFontFamilySelector.svelte +309 -0
- package/src/ui/UIFontSizeSelector.svelte +165 -0
- package/src/ui/UIFontWeightSelector.svelte +52 -0
- package/src/ui/UILineHeightSelector.svelte +47 -0
- package/src/ui/UILinkToggle.svelte +60 -0
- package/src/ui/UIOptionItem.svelte +74 -0
- package/src/ui/UIOptionList.svelte +27 -0
- package/src/ui/UIPaddingSelector.svelte +661 -0
- package/src/ui/UIPaletteSelector.svelte +1084 -0
- package/src/ui/UIRadio.svelte +72 -0
- package/src/ui/UIRadioGroup.svelte +59 -0
- package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
- package/src/ui/UITokenSelector.svelte +509 -0
- package/src/ui/UIVariantSelector.svelte +145 -0
- package/src/ui/VariablesTab.svelte +252 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/keepInViewport.ts +84 -0
- package/src/ui/palette/GradientStopEditor.svelte +482 -0
- package/src/ui/palette/OverridesPanel.svelte +526 -0
- package/src/ui/palette/PaletteBase.svelte +165 -0
- package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
- package/src/ui/palette/paletteEditorState.ts +89 -0
- package/src/ui/sections/ColumnsSection.svelte +273 -0
- package/src/ui/sections/GradientsSection.svelte +147 -0
- package/src/ui/sections/OverlaysSection.svelte +670 -0
- package/src/ui/sections/ShadowsSection.svelte +1250 -0
- package/src/ui/sections/TokenScaleTable.svelte +332 -0
- package/src/ui/sections/tokenScales.ts +81 -0
- package/src/ui/variantScales.ts +108 -0
- package/src/components/DetailNav.svelte +0 -78
- package/src/components/Toggle.svelte +0 -86
- package/src/lib/tokenInit.ts +0 -29
- package/src/lib/tokenService.ts +0 -144
- package/src/lib/tokenTypes.ts +0 -45
- package/src/pages/Admin.svelte +0 -100
- package/src/pages/ShowcasePage.svelte +0 -144
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -105
- package/src/showcase/PaletteEditor.svelte +0 -2579
- package/src/showcase/PaletteSelector.svelte +0 -627
- package/src/showcase/TokenMap.svelte +0 -54
- package/src/showcase/VariablesTab.svelte +0 -2655
- package/src/showcase/VisualsTab.svelte +0 -231
- package/src/showcase/demos/BadgeDemo.svelte +0 -56
- package/src/showcase/demos/CardDemo.svelte +0 -50
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
- package/src/showcase/demos/NotificationDemo.svelte +0 -147
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
- package/src/showcase/demos/TabBarDemo.svelte +0 -58
- package/src/showcase/demos/TooltipDemo.svelte +0 -52
- package/src/showcase/editor.css +0 -93
- package/src/showcase/index.ts +0 -17
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +0 -97
- package/src/styles/fonts/Domine/README.txt +0 -66
- /package/src/{showcase → ui}/curveEngine.ts +0 -0
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
2
|
import { storageKey } from './editorConfig';
|
|
3
|
+
import { quietGet, quietSet } from './storage';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function load(): boolean {
|
|
7
|
-
try {
|
|
8
|
-
return localStorage.getItem(STORAGE_KEY) === '1';
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
5
|
+
function getStorageKey(): string {
|
|
6
|
+
return storageKey('columns-visible');
|
|
12
7
|
}
|
|
13
8
|
|
|
14
|
-
export const columnsVisible = writable<boolean>(
|
|
15
|
-
|
|
16
|
-
columnsVisible.subscribe((v) => {
|
|
17
|
-
try {
|
|
18
|
-
localStorage.setItem(STORAGE_KEY, v ? '1' : '0');
|
|
19
|
-
} catch {
|
|
20
|
-
// ignore quota errors
|
|
21
|
-
}
|
|
22
|
-
});
|
|
9
|
+
export const columnsVisible = writable<boolean>(false);
|
|
23
10
|
|
|
24
11
|
export function toggleColumns() {
|
|
25
12
|
columnsVisible.update((v) => !v);
|
|
26
13
|
}
|
|
14
|
+
|
|
15
|
+
let initialised = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Idempotent host hook — call once during boot to hydrate the column-visibility
|
|
19
|
+
* store from localStorage and start persisting future writes. Importing the
|
|
20
|
+
* module no longer subscribes-then-persists, so SSR / test harnesses can pull
|
|
21
|
+
* `columnsVisible` without DOM/storage access.
|
|
22
|
+
*/
|
|
23
|
+
export function init(): void {
|
|
24
|
+
if (initialised) return;
|
|
25
|
+
initialised = true;
|
|
26
|
+
columnsVisible.set(quietGet(getStorageKey()) === '1');
|
|
27
|
+
columnsVisible.subscribe((v) => {
|
|
28
|
+
quietSet(getStorageKey(), v ? '1' : '0');
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { get } from 'svelte/store';
|
|
4
|
+
import {
|
|
5
|
+
editorState,
|
|
6
|
+
componentDirty,
|
|
7
|
+
setComponentAlias,
|
|
8
|
+
clearComponentAlias,
|
|
9
|
+
setComponentConfig,
|
|
10
|
+
clearComponentConfig,
|
|
11
|
+
loadComponentActive,
|
|
12
|
+
markComponentSaved,
|
|
13
|
+
seedComponentsFromApi,
|
|
14
|
+
undo,
|
|
15
|
+
redo,
|
|
16
|
+
__resetForTests,
|
|
17
|
+
} from './editorStore';
|
|
18
|
+
|
|
19
|
+
const tokenRef = (name: string) => ({ kind: 'token' as const, name });
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
__resetForTests();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('component aliases — editor-state round trip', () => {
|
|
26
|
+
it('setComponentAlias → undo restores previous state; redo reapplies', () => {
|
|
27
|
+
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-success-high'));
|
|
28
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
29
|
+
|
|
30
|
+
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
31
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-error-high'));
|
|
32
|
+
|
|
33
|
+
undo();
|
|
34
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
35
|
+
|
|
36
|
+
redo();
|
|
37
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-error-high'));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('clearComponentAlias removes the entry and is undoable', () => {
|
|
41
|
+
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-success-high'));
|
|
42
|
+
clearComponentAlias('button', '--button-primary-surface');
|
|
43
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toBeUndefined();
|
|
44
|
+
|
|
45
|
+
undo();
|
|
46
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('setComponentAlias implicitly registers the slice with activeFile "default"', () => {
|
|
50
|
+
setComponentAlias('card', '--card-radius', tokenRef('--radius-lg'));
|
|
51
|
+
expect(get(editorState).components.card.activeFile).toBe('default');
|
|
52
|
+
expect(get(editorState).components.card.aliases['--card-radius']).toEqual(tokenRef('--radius-lg'));
|
|
53
|
+
expect(get(editorState).components.card.config).toEqual({});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('component config — literal-valued knobs', () => {
|
|
58
|
+
it('setComponentConfig stores the value and is undoable', () => {
|
|
59
|
+
setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
|
|
60
|
+
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
|
|
61
|
+
|
|
62
|
+
setComponentConfig('dialog', '--dialog-confirm-variant', 'warning');
|
|
63
|
+
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('warning');
|
|
64
|
+
|
|
65
|
+
undo();
|
|
66
|
+
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('clearComponentConfig removes the entry', () => {
|
|
70
|
+
setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
|
|
71
|
+
clearComponentConfig('dialog', '--dialog-confirm-variant');
|
|
72
|
+
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('setComponentConfig implicitly registers the slice with empty aliases', () => {
|
|
76
|
+
setComponentConfig('dialog', '--dialog-cancel-variant', 'outline');
|
|
77
|
+
expect(get(editorState).components.dialog.activeFile).toBe('default');
|
|
78
|
+
expect(get(editorState).components.dialog.aliases).toEqual({});
|
|
79
|
+
expect(get(editorState).components.dialog.config['--dialog-cancel-variant']).toBe('outline');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('componentDirty — per-component scoping', () => {
|
|
84
|
+
it('marks a component dirty only after its aliases diverge from the saved baseline', () => {
|
|
85
|
+
loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
|
|
86
|
+
expect(get(componentDirty).button).toBe(false);
|
|
87
|
+
|
|
88
|
+
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
89
|
+
expect(get(componentDirty).button).toBe(true);
|
|
90
|
+
|
|
91
|
+
markComponentSaved('button');
|
|
92
|
+
expect(get(componentDirty).button).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('marks a component dirty when config diverges from the saved baseline', () => {
|
|
96
|
+
loadComponentActive('dialog', 'default', {}, { '--dialog-confirm-variant': 'primary' });
|
|
97
|
+
expect(get(componentDirty).dialog).toBe(false);
|
|
98
|
+
|
|
99
|
+
setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
|
|
100
|
+
expect(get(componentDirty).dialog).toBe(true);
|
|
101
|
+
|
|
102
|
+
markComponentSaved('dialog');
|
|
103
|
+
expect(get(componentDirty).dialog).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('editing one component does not dirty another', () => {
|
|
107
|
+
loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
|
|
108
|
+
loadComponentActive('card', 'default', { '--card-radius': '--radius-md' });
|
|
109
|
+
expect(get(componentDirty).button).toBe(false);
|
|
110
|
+
expect(get(componentDirty).card).toBe(false);
|
|
111
|
+
|
|
112
|
+
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
113
|
+
expect(get(componentDirty).button).toBe(true);
|
|
114
|
+
expect(get(componentDirty).card).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('undo after a saved state marks dirty again', () => {
|
|
118
|
+
loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
|
|
119
|
+
setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
|
|
120
|
+
markComponentSaved('button');
|
|
121
|
+
expect(get(componentDirty).button).toBe(false);
|
|
122
|
+
|
|
123
|
+
undo();
|
|
124
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
125
|
+
expect(get(componentDirty).button).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('loadComponentActive — split-on-load migration', () => {
|
|
130
|
+
it('routes legacy config keys from single-bucket aliases into the config bucket', () => {
|
|
131
|
+
loadComponentActive('dialog', 'default', {
|
|
132
|
+
'--dialog-surface': '--surface-neutral-low',
|
|
133
|
+
'--dialog-confirm-variant': 'danger',
|
|
134
|
+
});
|
|
135
|
+
const slice = get(editorState).components.dialog;
|
|
136
|
+
expect(slice.aliases['--dialog-surface']).toEqual(tokenRef('--surface-neutral-low'));
|
|
137
|
+
expect(slice.aliases['--dialog-confirm-variant']).toBeUndefined();
|
|
138
|
+
expect(slice.config['--dialog-confirm-variant']).toBe('danger');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('keeps CSS-var-valued aliases (e.g. --button-shimmer → --shimmer-on) in the aliases bucket', () => {
|
|
142
|
+
loadComponentActive('button', 'default', {
|
|
143
|
+
'--button-primary-surface': '--surface-success-high',
|
|
144
|
+
'--button-shimmer': '--shimmer-on',
|
|
145
|
+
});
|
|
146
|
+
const slice = get(editorState).components.button;
|
|
147
|
+
expect(slice.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
148
|
+
expect(slice.aliases['--button-shimmer']).toEqual(tokenRef('--shimmer-on'));
|
|
149
|
+
expect(slice.config['--button-shimmer']).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('classifies literal alias values as kind "literal"', () => {
|
|
153
|
+
loadComponentActive('myComp', 'default', {
|
|
154
|
+
'--my-comp-token': '--some-token',
|
|
155
|
+
'--my-comp-color': 'rebeccapurple',
|
|
156
|
+
});
|
|
157
|
+
const slice = get(editorState).components.myComp;
|
|
158
|
+
expect(slice.aliases['--my-comp-token']).toEqual(tokenRef('--some-token'));
|
|
159
|
+
expect(slice.aliases['--my-comp-color']).toEqual({ kind: 'literal', value: 'rebeccapurple' });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('explicit config field wins over legacy alias-bucketed value', () => {
|
|
163
|
+
loadComponentActive(
|
|
164
|
+
'dialog',
|
|
165
|
+
'default',
|
|
166
|
+
{ '--dialog-confirm-variant': 'primary' },
|
|
167
|
+
{ '--dialog-confirm-variant': 'danger' },
|
|
168
|
+
);
|
|
169
|
+
expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('seedComponentsFromApi — boot-time hydration', () => {
|
|
174
|
+
it('populates state and establishes the clean baseline', () => {
|
|
175
|
+
seedComponentsFromApi({
|
|
176
|
+
button: { activeFile: 'myConfig', aliases: { '--button-primary-surface': '--surface-success-high' } },
|
|
177
|
+
});
|
|
178
|
+
expect(get(editorState).components.button.activeFile).toBe('myConfig');
|
|
179
|
+
expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
|
|
180
|
+
expect(get(componentDirty).button).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('routes config keys when seeding from legacy single-bucket API payload', () => {
|
|
184
|
+
seedComponentsFromApi({
|
|
185
|
+
dialog: {
|
|
186
|
+
activeFile: 'default',
|
|
187
|
+
aliases: { '--dialog-confirm-variant': 'danger', '--dialog-shadow': '--shadow-2xl' },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const slice = get(editorState).components.dialog;
|
|
191
|
+
expect(slice.aliases['--dialog-shadow']).toEqual(tokenRef('--shadow-2xl'));
|
|
192
|
+
expect(slice.aliases['--dialog-confirm-variant']).toBeUndefined();
|
|
193
|
+
expect(slice.config['--dialog-confirm-variant']).toBe('danger');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('replaces the full components slice', () => {
|
|
197
|
+
setComponentAlias('card', '--card-radius', tokenRef('--radius-md'));
|
|
198
|
+
seedComponentsFromApi({
|
|
199
|
+
button: { activeFile: 'default', aliases: {} },
|
|
200
|
+
});
|
|
201
|
+
expect(get(editorState).components.card).toBeUndefined();
|
|
202
|
+
expect(get(editorState).components.button).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// TODO(M3 schemaVersion / Wave 4): replace this enumerated list with a
|
|
2
|
+
// per-component schema declaration (e.g. registerComponentSchema(...,
|
|
3
|
+
// { kind: 'config' })). For now it's a flat set used by the on-load
|
|
4
|
+
// migration that splits legacy single-bucket aliases into the new
|
|
5
|
+
// {aliases, config} shape.
|
|
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).
|
|
10
|
+
//
|
|
11
|
+
// What does NOT go here: aliases whose values are themselves CSS-var refs
|
|
12
|
+
// — even if the value space is constrained (e.g. `--button-shimmer` →
|
|
13
|
+
// `--shimmer-on` | `--shimmer-off`). Those are still aliases and must flow
|
|
14
|
+
// through `componentsToVars` so SCSS that does `var(--button-shimmer)`
|
|
15
|
+
// keeps resolving.
|
|
16
|
+
export const KNOWN_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
17
|
+
'--dialog-confirm-variant',
|
|
18
|
+
'--dialog-cancel-variant',
|
|
19
|
+
]);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ComponentConfig, ComponentConfigMeta } from './themeTypes';
|
|
2
|
+
import { versionedFileResource } from './files/versionedFileResource';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 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.
|
|
10
|
+
*
|
|
11
|
+
* Both this and `themeService` consume `versionedFileResource(...)`. Adding a
|
|
12
|
+
* third file-managed resource — per the user's "mirror theme-file lifecycle
|
|
13
|
+
* for new editor artifacts" invariant — is one helper call here, not a third
|
|
14
|
+
* round of copy-paste CRUD.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface ComponentSummary {
|
|
18
|
+
name: string;
|
|
19
|
+
activeFile: string;
|
|
20
|
+
productionFile: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ComponentProductionInfo {
|
|
24
|
+
fileName: string;
|
|
25
|
+
name: string;
|
|
26
|
+
aliases: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ComponentConfigList {
|
|
30
|
+
component: string;
|
|
31
|
+
files: ComponentConfigMeta[];
|
|
32
|
+
activeFile: string;
|
|
33
|
+
productionFile: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function listComponents(): Promise<ComponentSummary[]> {
|
|
37
|
+
const res = await fetch('/api/component-configs');
|
|
38
|
+
if (!res.ok) throw new Error('Failed to list components');
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
return data.components;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resourceFor(component: string) {
|
|
44
|
+
return versionedFileResource<ComponentConfig, ComponentConfigMeta, ComponentProductionInfo>({
|
|
45
|
+
baseUrl: `/api/component-configs/${encodeURIComponent(component)}`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function listComponentConfigs(component: string): Promise<ComponentConfigList> {
|
|
50
|
+
const data = await resourceFor(component).list();
|
|
51
|
+
return {
|
|
52
|
+
component,
|
|
53
|
+
files: data.files,
|
|
54
|
+
activeFile: data.activeFile ?? 'default',
|
|
55
|
+
productionFile: data.productionFile ?? 'default',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const loadComponentConfig = (
|
|
60
|
+
component: string,
|
|
61
|
+
fileName: string,
|
|
62
|
+
): Promise<ComponentConfig> => resourceFor(component).load(fileName);
|
|
63
|
+
|
|
64
|
+
export const saveComponentConfig = (
|
|
65
|
+
component: string,
|
|
66
|
+
fileName: string,
|
|
67
|
+
data: ComponentConfig,
|
|
68
|
+
): Promise<void> => resourceFor(component).save(fileName, data);
|
|
69
|
+
|
|
70
|
+
export const deleteComponentConfig = (component: string, fileName: string): Promise<void> =>
|
|
71
|
+
resourceFor(component).remove(fileName);
|
|
72
|
+
|
|
73
|
+
export const getActiveComponentConfig = (component: string): Promise<ComponentConfig | null> =>
|
|
74
|
+
resourceFor(component).getActive();
|
|
75
|
+
|
|
76
|
+
export const setActiveComponentFile = (component: string, fileName: string): Promise<void> =>
|
|
77
|
+
resourceFor(component).setActive(fileName);
|
|
78
|
+
|
|
79
|
+
export const getComponentProductionInfo = (component: string): Promise<ComponentProductionInfo> =>
|
|
80
|
+
resourceFor(component).getProductionInfo();
|
|
81
|
+
|
|
82
|
+
export async function setComponentProductionFile(
|
|
83
|
+
component: string,
|
|
84
|
+
fileName: string,
|
|
85
|
+
): Promise<{ ok: boolean; productionFile: string }> {
|
|
86
|
+
const data = await resourceFor(component).setProduction(fileName);
|
|
87
|
+
return { ok: data.ok, productionFile: data.fileName };
|
|
88
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export type CopyPopoverState = {
|
|
4
|
+
visible: boolean;
|
|
5
|
+
value: string;
|
|
6
|
+
/** Viewport-relative anchor rect for the trigger element. */
|
|
7
|
+
anchor: { top: number; left: number; width: number; height: number } | null;
|
|
8
|
+
/** Monotonic id so consecutive copies of the same value re-trigger the timer. */
|
|
9
|
+
nonce: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const initial: CopyPopoverState = { visible: false, value: '', anchor: null, nonce: 0 };
|
|
13
|
+
|
|
14
|
+
export const copyPopover = writable<CopyPopoverState>(initial);
|
|
15
|
+
|
|
16
|
+
let hideTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
+
|
|
18
|
+
export function showCopyPopover(value: string, target: EventTarget | null, durationMs = 1500) {
|
|
19
|
+
const el = target instanceof Element ? target : null;
|
|
20
|
+
const rect = el?.getBoundingClientRect() ?? null;
|
|
21
|
+
const anchor = rect ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height } : null;
|
|
22
|
+
|
|
23
|
+
copyPopover.update((s) => ({ visible: true, value, anchor, nonce: s.nonce + 1 }));
|
|
24
|
+
|
|
25
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
26
|
+
hideTimer = setTimeout(() => {
|
|
27
|
+
copyPopover.update((s) => ({ ...s, visible: false }));
|
|
28
|
+
hideTimer = null;
|
|
29
|
+
}, durationMs);
|
|
30
|
+
}
|
package/src/lib/cssVarSync.ts
CHANGED
|
@@ -4,14 +4,24 @@
|
|
|
4
4
|
* Writes to document.documentElement and — when running inside a same-origin
|
|
5
5
|
* iframe (the live-preview overlay) — also writes to
|
|
6
6
|
* window.parent.document.documentElement. This lets the overlay editor at
|
|
7
|
-
* /
|
|
7
|
+
* /editor drive the host site's :root in real time without any message-passing
|
|
8
8
|
* infrastructure.
|
|
9
9
|
*
|
|
10
|
-
* When the editor runs standalone at /
|
|
10
|
+
* When the editor runs standalone at /editor (not inside the overlay iframe),
|
|
11
11
|
* parentRoot is null and every call is a plain single-root write.
|
|
12
|
+
*
|
|
13
|
+
* Roots are resolved lazily — `init()` (or any setter call) populates them on
|
|
14
|
+
* first use so importing this module does not touch the DOM. This keeps the
|
|
15
|
+
* library importable from SSR / test harnesses and decouples consumers from
|
|
16
|
+
* module-load ordering.
|
|
12
17
|
*/
|
|
13
18
|
|
|
19
|
+
let selfRoot: HTMLElement | null = null;
|
|
20
|
+
let parentRoot: HTMLElement | null = null;
|
|
21
|
+
let resolved = false;
|
|
22
|
+
|
|
14
23
|
function resolveParentRoot(): HTMLElement | null {
|
|
24
|
+
if (typeof window === 'undefined') return null;
|
|
15
25
|
try {
|
|
16
26
|
if (window.parent !== window && window.parent?.document) {
|
|
17
27
|
return window.parent.document.documentElement;
|
|
@@ -22,17 +32,56 @@ function resolveParentRoot(): HTMLElement | null {
|
|
|
22
32
|
return null;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
function ensureResolved(): void {
|
|
36
|
+
if (resolved) return;
|
|
37
|
+
resolved = true;
|
|
38
|
+
selfRoot = typeof document !== 'undefined' ? document.documentElement : null;
|
|
39
|
+
parentRoot = resolveParentRoot();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Idempotent host hook — call once during boot to eagerly resolve the self
|
|
44
|
+
* and parent document roots. Optional in practice (any setter call resolves
|
|
45
|
+
* lazily), but explicit init makes ordering legible.
|
|
46
|
+
*/
|
|
47
|
+
export function init(): void {
|
|
48
|
+
ensureResolved();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return the self and parent document heads as a tuple, with parent omitted
|
|
53
|
+
* when not in an iframe. Consumers that need to mirror node injection (not
|
|
54
|
+
* just style properties) can iterate this list.
|
|
55
|
+
*/
|
|
56
|
+
export function getSyncedDocuments(): Document[] {
|
|
57
|
+
ensureResolved();
|
|
58
|
+
if (typeof document === 'undefined') return [];
|
|
59
|
+
const docs: Document[] = [document];
|
|
60
|
+
if (parentRoot && parentRoot.ownerDocument && parentRoot.ownerDocument !== document) {
|
|
61
|
+
docs.push(parentRoot.ownerDocument);
|
|
62
|
+
}
|
|
63
|
+
return docs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const CSS_VAR_CHANGE_EVENT = 'cssvar:change';
|
|
67
|
+
|
|
68
|
+
function notifyChange(name: string): void {
|
|
69
|
+
if (typeof document === 'undefined') return;
|
|
70
|
+
document.dispatchEvent(new CustomEvent(CSS_VAR_CHANGE_EVENT, { detail: { name } }));
|
|
71
|
+
}
|
|
27
72
|
|
|
28
73
|
export function setCssVar(name: string, value: string): void {
|
|
29
|
-
|
|
74
|
+
ensureResolved();
|
|
75
|
+
selfRoot?.style.setProperty(name, value);
|
|
30
76
|
parentRoot?.style.setProperty(name, value);
|
|
77
|
+
notifyChange(name);
|
|
31
78
|
}
|
|
32
79
|
|
|
33
80
|
export function removeCssVar(name: string): void {
|
|
34
|
-
|
|
81
|
+
ensureResolved();
|
|
82
|
+
selfRoot?.style.removeProperty(name);
|
|
35
83
|
parentRoot?.style.removeProperty(name);
|
|
84
|
+
notifyChange(name);
|
|
36
85
|
}
|
|
37
86
|
|
|
38
87
|
/** Apply a map of CSS variables to :root (and the parent :root when in an iframe). */
|
|
@@ -44,7 +93,8 @@ export function applyCssVariables(variables: Record<string, string>): void {
|
|
|
44
93
|
|
|
45
94
|
/** Remove all inline CSS custom properties from :root on both self and parent. */
|
|
46
95
|
export function clearAllCssVarOverrides(): void {
|
|
47
|
-
|
|
96
|
+
ensureResolved();
|
|
97
|
+
if (selfRoot) clearRoot(selfRoot);
|
|
48
98
|
if (parentRoot) clearRoot(parentRoot);
|
|
49
99
|
}
|
|
50
100
|
|
|
@@ -60,6 +110,8 @@ function clearRoot(el: HTMLElement): void {
|
|
|
60
110
|
|
|
61
111
|
/** Scrape all inline CSS custom properties currently on self :root. */
|
|
62
112
|
export function scrapeCssVariables(): Record<string, string> {
|
|
113
|
+
ensureResolved();
|
|
114
|
+
if (!selfRoot) return {};
|
|
63
115
|
const style = selfRoot.style;
|
|
64
116
|
const variables: Record<string, string> = {};
|
|
65
117
|
for (let i = 0; i < style.length; i++) {
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
|
-
import type { PaletteConfig } from './tokenTypes';
|
|
3
|
-
|
|
4
|
-
/** Each PaletteEditor pushes its config here keyed by label. Used when saving. */
|
|
5
|
-
export const editorConfigs = writable<Record<string, PaletteConfig>>({});
|
|
6
|
-
|
|
7
|
-
/** Set by the load flow. Each PaletteEditor watches for its label and applies the config. */
|
|
8
|
-
export const loadedConfigs = writable<Record<string, PaletteConfig> | null>(null);
|
|
9
|
-
|
|
10
|
-
/** True when editorConfigs were populated from a token file load or deliberate user edit, not just component defaults. */
|
|
11
|
-
export const configsLoadedFromFile = writable<boolean>(false);
|
|
12
2
|
|
|
13
3
|
/** The file name of the currently active token file. */
|
|
14
4
|
export const activeFileName = writable<string>('default');
|