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