@motion-proto/live-tokens 0.8.0 → 0.9.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 +34 -0
- package/package.json +5 -2
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +103 -26
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
- package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
- package/src/editor/core/components/componentConfigKeys.ts +14 -3
- package/src/editor/core/themes/slices/components.ts +9 -0
- 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/system/components/Dialog.svelte +24 -4
- package/src/system/components/SectionDivider.svelte +117 -43
|
@@ -21,7 +21,8 @@ import TableEditor, { allTokens as tableTokens } from './TableEditor.svelte';
|
|
|
21
21
|
import TabBarEditor, { allTokens as tabBarTokens } from './TabBarEditor.svelte';
|
|
22
22
|
import TooltipEditor, { allTokens as tooltipTokens } from './TooltipEditor.svelte';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** Internal narrowed union of the first-party component ids. Not exposed publicly. */
|
|
25
|
+
type BuiltInComponentId =
|
|
25
26
|
| 'segmentedcontrol'
|
|
26
27
|
| 'button'
|
|
27
28
|
| 'notification'
|
|
@@ -41,6 +42,13 @@ export type ComponentId =
|
|
|
41
42
|
| 'tooltip'
|
|
42
43
|
| 'progressbar';
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Public component id type. Widened to `string` because consumers can register
|
|
47
|
+
* their own components at runtime via `registerComponent()`. Internal code that
|
|
48
|
+
* needs to narrow to first-party ids can reference `BuiltInComponentId`.
|
|
49
|
+
*/
|
|
50
|
+
export type ComponentId = string;
|
|
51
|
+
|
|
44
52
|
export interface RegistryEntry {
|
|
45
53
|
/** Canonical id — lowercase, matches the runtime component filename + server scan + `setComponentAlias` key. */
|
|
46
54
|
id: ComponentId;
|
|
@@ -54,21 +62,18 @@ export interface RegistryEntry {
|
|
|
54
62
|
editorComponent: Component<any, any, any>;
|
|
55
63
|
/** Flat token list — the editor's declarative description of its token surface. */
|
|
56
64
|
schema: Token[];
|
|
65
|
+
/** `'system'` for first-party entries; `'custom'` for entries added via `registerComponent()`. */
|
|
66
|
+
origin: 'system' | 'custom';
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* (
|
|
64
|
-
* not affect the UI.
|
|
65
|
-
*
|
|
66
|
-
* Adding a component:
|
|
67
|
-
* 1. Author `src/components/<Name>.svelte` (declares CSS vars in `:global(:root)`)
|
|
68
|
-
* 2. Author `src/component-editor/<Name>Editor.svelte` (exports `allTokens` from a `<script context="module">` block)
|
|
70
|
+
* First-party registry. Frozen; runtime additions go in `customRegistry`.
|
|
71
|
+
* Adding a first-party component:
|
|
72
|
+
* 1. Author `src/system/components/<Name>.svelte` (declares CSS vars in `:global(:root)`)
|
|
73
|
+
* 2. Author `src/editor/component-editor/<Name>Editor.svelte` (exports `allTokens`)
|
|
69
74
|
* 3. Add an entry below.
|
|
70
75
|
*/
|
|
71
|
-
|
|
76
|
+
const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Object.freeze({
|
|
72
77
|
segmentedcontrol: {
|
|
73
78
|
id: 'segmentedcontrol',
|
|
74
79
|
label: 'Segmented Control',
|
|
@@ -76,6 +81,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
76
81
|
sourceFile: 'src/system/components/SegmentedControl.svelte',
|
|
77
82
|
editorComponent: SegmentedControlEditor,
|
|
78
83
|
schema: segmentedControlTokens,
|
|
84
|
+
origin: 'system',
|
|
79
85
|
},
|
|
80
86
|
button: {
|
|
81
87
|
id: 'button',
|
|
@@ -84,6 +90,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
84
90
|
sourceFile: 'src/system/components/Button.svelte',
|
|
85
91
|
editorComponent: StandardButtonsEditor,
|
|
86
92
|
schema: buttonTokens,
|
|
93
|
+
origin: 'system',
|
|
87
94
|
},
|
|
88
95
|
notification: {
|
|
89
96
|
id: 'notification',
|
|
@@ -92,6 +99,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
92
99
|
sourceFile: 'src/system/components/Notification.svelte',
|
|
93
100
|
editorComponent: NotificationEditor,
|
|
94
101
|
schema: notificationTokens,
|
|
102
|
+
origin: 'system',
|
|
95
103
|
},
|
|
96
104
|
dialog: {
|
|
97
105
|
id: 'dialog',
|
|
@@ -100,6 +108,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
100
108
|
sourceFile: 'src/system/components/Dialog.svelte',
|
|
101
109
|
editorComponent: DialogEditor,
|
|
102
110
|
schema: dialogTokens,
|
|
111
|
+
origin: 'system',
|
|
103
112
|
},
|
|
104
113
|
radiobutton: {
|
|
105
114
|
id: 'radiobutton',
|
|
@@ -108,6 +117,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
108
117
|
sourceFile: 'src/system/components/RadioButton.svelte',
|
|
109
118
|
editorComponent: RadioButtonEditor,
|
|
110
119
|
schema: radioButtonTokens,
|
|
120
|
+
origin: 'system',
|
|
111
121
|
},
|
|
112
122
|
card: {
|
|
113
123
|
id: 'card',
|
|
@@ -116,6 +126,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
116
126
|
sourceFile: 'src/system/components/Card.svelte',
|
|
117
127
|
editorComponent: CardEditor,
|
|
118
128
|
schema: cardTokens,
|
|
129
|
+
origin: 'system',
|
|
119
130
|
},
|
|
120
131
|
badge: {
|
|
121
132
|
id: 'badge',
|
|
@@ -124,6 +135,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
124
135
|
sourceFile: 'src/system/components/Badge.svelte',
|
|
125
136
|
editorComponent: BadgeEditor,
|
|
126
137
|
schema: badgeTokens,
|
|
138
|
+
origin: 'system',
|
|
127
139
|
},
|
|
128
140
|
callout: {
|
|
129
141
|
id: 'callout',
|
|
@@ -132,6 +144,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
132
144
|
sourceFile: 'src/system/components/Callout.svelte',
|
|
133
145
|
editorComponent: CalloutEditor,
|
|
134
146
|
schema: calloutTokens,
|
|
147
|
+
origin: 'system',
|
|
135
148
|
},
|
|
136
149
|
cornerbadge: {
|
|
137
150
|
id: 'cornerbadge',
|
|
@@ -140,6 +153,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
140
153
|
sourceFile: 'src/system/components/CornerBadge.svelte',
|
|
141
154
|
editorComponent: CornerBadgeEditor,
|
|
142
155
|
schema: cornerBadgeTokens,
|
|
156
|
+
origin: 'system',
|
|
143
157
|
},
|
|
144
158
|
image: {
|
|
145
159
|
id: 'image',
|
|
@@ -148,6 +162,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
148
162
|
sourceFile: 'src/system/components/Image.svelte',
|
|
149
163
|
editorComponent: ImageEditor,
|
|
150
164
|
schema: imageTokens,
|
|
165
|
+
origin: 'system',
|
|
151
166
|
},
|
|
152
167
|
inlineeditactions: {
|
|
153
168
|
id: 'inlineeditactions',
|
|
@@ -156,6 +171,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
156
171
|
sourceFile: 'src/system/components/InlineEditActions.svelte',
|
|
157
172
|
editorComponent: InlineEditActionsEditor,
|
|
158
173
|
schema: inlineEditActionsTokens,
|
|
174
|
+
origin: 'system',
|
|
159
175
|
},
|
|
160
176
|
menuselect: {
|
|
161
177
|
id: 'menuselect',
|
|
@@ -164,6 +180,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
164
180
|
sourceFile: 'src/system/components/MenuSelect.svelte',
|
|
165
181
|
editorComponent: MenuSelectEditor,
|
|
166
182
|
schema: menuSelectTokens,
|
|
183
|
+
origin: 'system',
|
|
167
184
|
},
|
|
168
185
|
sectiondivider: {
|
|
169
186
|
id: 'sectiondivider',
|
|
@@ -172,6 +189,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
172
189
|
sourceFile: 'src/system/components/SectionDivider.svelte',
|
|
173
190
|
editorComponent: SectionDividerEditor,
|
|
174
191
|
schema: sectionDividerTokens,
|
|
192
|
+
origin: 'system',
|
|
175
193
|
},
|
|
176
194
|
collapsiblesection: {
|
|
177
195
|
id: 'collapsiblesection',
|
|
@@ -180,6 +198,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
180
198
|
sourceFile: 'src/system/components/CollapsibleSection.svelte',
|
|
181
199
|
editorComponent: CollapsibleSectionEditor,
|
|
182
200
|
schema: collapsibleSectionTokens,
|
|
201
|
+
origin: 'system',
|
|
183
202
|
},
|
|
184
203
|
table: {
|
|
185
204
|
id: 'table',
|
|
@@ -188,6 +207,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
188
207
|
sourceFile: 'src/system/components/Table.svelte',
|
|
189
208
|
editorComponent: TableEditor,
|
|
190
209
|
schema: tableTokens,
|
|
210
|
+
origin: 'system',
|
|
191
211
|
},
|
|
192
212
|
tabbar: {
|
|
193
213
|
id: 'tabbar',
|
|
@@ -196,6 +216,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
196
216
|
sourceFile: 'src/system/components/TabBar.svelte',
|
|
197
217
|
editorComponent: TabBarEditor,
|
|
198
218
|
schema: tabBarTokens,
|
|
219
|
+
origin: 'system',
|
|
199
220
|
},
|
|
200
221
|
tooltip: {
|
|
201
222
|
id: 'tooltip',
|
|
@@ -204,6 +225,7 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
204
225
|
sourceFile: 'src/system/components/Tooltip.svelte',
|
|
205
226
|
editorComponent: TooltipEditor,
|
|
206
227
|
schema: tooltipTokens,
|
|
228
|
+
origin: 'system',
|
|
207
229
|
},
|
|
208
230
|
progressbar: {
|
|
209
231
|
id: 'progressbar',
|
|
@@ -212,34 +234,89 @@ export const componentRegistry: Readonly<Record<ComponentId, RegistryEntry>> = O
|
|
|
212
234
|
sourceFile: 'src/system/components/ProgressBar.svelte',
|
|
213
235
|
editorComponent: ProgressBarEditor,
|
|
214
236
|
schema: progressBarTokens,
|
|
237
|
+
origin: 'system',
|
|
215
238
|
},
|
|
216
239
|
});
|
|
217
240
|
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
241
|
+
/** Mutable map of consumer-registered components, populated by `registerComponent()`. */
|
|
242
|
+
const customRegistry = new Map<string, RegistryEntry>();
|
|
243
|
+
|
|
244
|
+
/** Argument shape for `registerComponent()`. `origin` is set internally to `'custom'`. */
|
|
245
|
+
export type RegisterComponentEntry = Omit<RegistryEntry, 'origin'>;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Register a consumer-authored component at runtime. Call from `main.ts`
|
|
249
|
+
* before app mount.
|
|
250
|
+
*
|
|
251
|
+
* Collision rule: if `entry.id` matches a built-in id, a warning is logged and
|
|
252
|
+
* the custom entry wins (the custom editor and schema replace the built-in for
|
|
253
|
+
* the rest of the session).
|
|
254
|
+
*
|
|
255
|
+
* Side effect: registers the schema with the editor store so reset-to-default
|
|
256
|
+
* and sibling-group resolution work for the new component.
|
|
257
|
+
*/
|
|
258
|
+
export function registerComponent(entry: RegisterComponentEntry): void {
|
|
259
|
+
if (entry.id in builtInRegistry) {
|
|
260
|
+
console.warn(
|
|
261
|
+
`[registerComponent] custom component "${entry.id}" overrides a built-in. The custom editor will be used.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const stored: RegistryEntry = { ...entry, origin: 'custom' };
|
|
265
|
+
customRegistry.set(entry.id, stored);
|
|
266
|
+
registerComponentSchema(entry.id, entry.schema);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Merged registry: built-ins overlaid with customs (custom wins on id collision).
|
|
271
|
+
* Recomputed on each call so callers see runtime registrations made after their
|
|
272
|
+
* own module-load order.
|
|
273
|
+
*/
|
|
274
|
+
export function getComponentRegistry(): Readonly<Record<string, RegistryEntry>> {
|
|
275
|
+
const merged: Record<string, RegistryEntry> = { ...builtInRegistry };
|
|
276
|
+
for (const [id, entry] of customRegistry) {
|
|
277
|
+
merged[id] = entry;
|
|
278
|
+
}
|
|
279
|
+
return merged;
|
|
280
|
+
}
|
|
222
281
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Display-ordered entries: system first (alphabetical by label), then custom
|
|
284
|
+
* (alphabetical by label). Iteration order matches the nav rail's grouping.
|
|
285
|
+
* The nav rail renders a divider between the two groups when customs exist.
|
|
286
|
+
*/
|
|
287
|
+
export function getComponentRegistryEntries(): ReadonlyArray<RegistryEntry> {
|
|
288
|
+
const merged = getComponentRegistry();
|
|
289
|
+
const system: RegistryEntry[] = [];
|
|
290
|
+
const custom: RegistryEntry[] = [];
|
|
291
|
+
for (const entry of Object.values(merged)) {
|
|
292
|
+
(entry.origin === 'system' ? system : custom).push(entry);
|
|
293
|
+
}
|
|
294
|
+
system.sort((a, b) => a.label.localeCompare(b.label));
|
|
295
|
+
custom.sort((a, b) => a.label.localeCompare(b.label));
|
|
296
|
+
return [...system, ...custom];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** All component ids, in display order. */
|
|
300
|
+
export function getComponentIds(): ReadonlyArray<string> {
|
|
301
|
+
return getComponentRegistryEntries().map((e) => e.id);
|
|
302
|
+
}
|
|
227
303
|
|
|
228
|
-
// Eager schema registration.
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
for (const entry of
|
|
304
|
+
// Eager schema registration for built-ins. Customs register lazily inside
|
|
305
|
+
// `registerComponent()` so the store knows about every component before any
|
|
306
|
+
// editor instance mounts.
|
|
307
|
+
for (const entry of Object.values(builtInRegistry)) {
|
|
232
308
|
registerComponentSchema(entry.id, entry.schema);
|
|
233
309
|
}
|
|
234
310
|
|
|
235
311
|
/**
|
|
236
|
-
* Validate that the server's filesystem scan matches the registry's id list.
|
|
312
|
+
* Validate that the server's filesystem scan matches the merged registry's id list.
|
|
237
313
|
* Logs a warning when ids drift. Called at boot from the editor page.
|
|
238
314
|
*/
|
|
239
315
|
export function validateRegistryAgainstServerScan(serverIds: ReadonlyArray<string>): void {
|
|
240
|
-
const
|
|
316
|
+
const ids = getComponentIds();
|
|
317
|
+
const registrySet = new Set<string>(ids);
|
|
241
318
|
const serverSet = new Set<string>(serverIds);
|
|
242
|
-
const missingOnServer =
|
|
319
|
+
const missingOnServer = ids.filter((id) => !serverSet.has(id));
|
|
243
320
|
const extraOnServer = serverIds.filter((id) => !registrySet.has(id));
|
|
244
321
|
if (missingOnServer.length > 0) {
|
|
245
322
|
console.warn(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ComponentSection } from './componentSectionType';
|
|
3
|
-
import {
|
|
3
|
+
import { getDefaultSections } from './defaultSections';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
sections?: ComponentSection[];
|
|
7
7
|
selectedComponent?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
let { sections =
|
|
10
|
+
let { sections = getDefaultSections(), selectedComponent = sections[0]?.id ?? '' }: Props = $props();
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<div class="components-container">
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
]);
|
|
@@ -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
|
}
|
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';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
|
|
6
6
|
import ManifestFileManager from '../ui/ManifestFileManager.svelte';
|
|
7
7
|
import { navigate } from '../core/routing/router';
|
|
8
|
-
import {
|
|
8
|
+
import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
|
|
9
9
|
import { listComponents } from '../core/components/componentConfigService';
|
|
10
10
|
import { selectedComponent } from '../core/store/editorViewStore';
|
|
11
11
|
import { componentDirty } from '../core/store/editorStore';
|
|
@@ -96,7 +96,9 @@
|
|
|
96
96
|
window.removeEventListener('keydown', handleKeydown);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
|
|
100
|
+
const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
|
|
101
|
+
const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
|
|
100
102
|
</script>
|
|
101
103
|
|
|
102
104
|
<!--
|
|
@@ -146,7 +148,7 @@
|
|
|
146
148
|
{/if}
|
|
147
149
|
</div>
|
|
148
150
|
<div class="nav-items">
|
|
149
|
-
{#each
|
|
151
|
+
{#each systemNavItems as item}
|
|
150
152
|
<button
|
|
151
153
|
class="nav-item"
|
|
152
154
|
class:active={$selectedComponent === item.id}
|
|
@@ -162,6 +164,27 @@
|
|
|
162
164
|
{/if}
|
|
163
165
|
</button>
|
|
164
166
|
{/each}
|
|
167
|
+
{#if customNavItems.length > 0}
|
|
168
|
+
<div class="nav-divider">
|
|
169
|
+
<span class="nav-divider-label">Custom</span>
|
|
170
|
+
</div>
|
|
171
|
+
{#each customNavItems as item}
|
|
172
|
+
<button
|
|
173
|
+
class="nav-item"
|
|
174
|
+
class:active={$selectedComponent === item.id}
|
|
175
|
+
class:dirty={$componentDirty[item.id]}
|
|
176
|
+
onmouseenter={(e) => showHint(item.label, e.currentTarget)}
|
|
177
|
+
onmouseleave={hideHint}
|
|
178
|
+
onclick={() => selectComponent(item.id)}
|
|
179
|
+
>
|
|
180
|
+
<i class={item.icon}></i>
|
|
181
|
+
<span class="rail-label">{item.label}</span>
|
|
182
|
+
{#if $componentDirty[item.id]}
|
|
183
|
+
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
184
|
+
{/if}
|
|
185
|
+
</button>
|
|
186
|
+
{/each}
|
|
187
|
+
{/if}
|
|
165
188
|
</div>
|
|
166
189
|
{#if drawerOpen}
|
|
167
190
|
<div class="sidebar-footer">
|
|
@@ -331,6 +354,33 @@
|
|
|
331
354
|
background: black;
|
|
332
355
|
}
|
|
333
356
|
|
|
357
|
+
/* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
|
|
358
|
+
dimmer border token (sub-element separator), with an uppercase eyebrow
|
|
359
|
+
label that fades out when the rail is collapsed so the line still reads. */
|
|
360
|
+
.nav-divider {
|
|
361
|
+
display: grid;
|
|
362
|
+
grid-template-columns: 48px 1fr;
|
|
363
|
+
align-items: center;
|
|
364
|
+
height: 28px;
|
|
365
|
+
margin-top: var(--ui-space-8);
|
|
366
|
+
border-top: 1px solid var(--ui-border-low);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.nav-divider-label {
|
|
370
|
+
grid-column: 2;
|
|
371
|
+
font-size: var(--ui-font-size-xs);
|
|
372
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
373
|
+
color: var(--ui-text-tertiary);
|
|
374
|
+
text-transform: uppercase;
|
|
375
|
+
letter-spacing: 0.04em;
|
|
376
|
+
opacity: 0;
|
|
377
|
+
transition: opacity 180ms ease;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.components-shell.rail-expanded .nav-divider-label {
|
|
381
|
+
opacity: 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
334
384
|
.sidebar-footer {
|
|
335
385
|
flex-shrink: 0;
|
|
336
386
|
margin-top: auto;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { editorState } from '../core/store/editorStore';
|
|
14
14
|
import { editorView, sidebarCondensed, selectedComponent } from '../core/store/editorViewStore';
|
|
15
15
|
import { componentDirty } from '../core/store/editorStore';
|
|
16
|
-
import {
|
|
16
|
+
import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
|
|
17
17
|
import { listComponents } from '../core/components/componentConfigService';
|
|
18
18
|
|
|
19
19
|
const tokenNavItems = [
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
{ id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
|
|
33
|
+
const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
|
|
34
|
+
const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
|
|
33
35
|
|
|
34
36
|
let selectedTokenSection: string | null = $state(null);
|
|
35
37
|
let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
|
|
@@ -149,7 +151,7 @@
|
|
|
149
151
|
{/if}
|
|
150
152
|
{:else}
|
|
151
153
|
<div class="nav-items">
|
|
152
|
-
{#each
|
|
154
|
+
{#each systemNavItems as item}
|
|
153
155
|
<button
|
|
154
156
|
class="nav-item"
|
|
155
157
|
class:active={$selectedComponent === item.id}
|
|
@@ -165,6 +167,27 @@
|
|
|
165
167
|
{/if}
|
|
166
168
|
</button>
|
|
167
169
|
{/each}
|
|
170
|
+
{#if customNavItems.length > 0}
|
|
171
|
+
<div class="nav-divider">
|
|
172
|
+
<span class="nav-divider-label">Custom</span>
|
|
173
|
+
</div>
|
|
174
|
+
{#each customNavItems as item}
|
|
175
|
+
<button
|
|
176
|
+
class="nav-item"
|
|
177
|
+
class:active={$selectedComponent === item.id}
|
|
178
|
+
class:dirty={$componentDirty[item.id]}
|
|
179
|
+
onmouseenter={(e) => showHint(item.label, e.currentTarget)}
|
|
180
|
+
onmouseleave={hideHint}
|
|
181
|
+
onclick={() => selectComponent(item.id)}
|
|
182
|
+
>
|
|
183
|
+
<i class={item.icon}></i>
|
|
184
|
+
<span class="nav-label">{item.label}</span>
|
|
185
|
+
{#if $componentDirty[item.id]}
|
|
186
|
+
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
187
|
+
{/if}
|
|
188
|
+
</button>
|
|
189
|
+
{/each}
|
|
190
|
+
{/if}
|
|
168
191
|
</div>
|
|
169
192
|
{#if !condensed}
|
|
170
193
|
<div class="sidebar-footer">
|
|
@@ -248,6 +271,33 @@
|
|
|
248
271
|
flex-shrink: 0;
|
|
249
272
|
}
|
|
250
273
|
|
|
274
|
+
/* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
|
|
275
|
+
dimmer border token (sub-element separator), with an uppercase eyebrow
|
|
276
|
+
label that fades out when the rail is condensed so the line still reads. */
|
|
277
|
+
.nav-divider {
|
|
278
|
+
display: grid;
|
|
279
|
+
grid-template-columns: 48px 1fr;
|
|
280
|
+
align-items: center;
|
|
281
|
+
height: 28px;
|
|
282
|
+
margin-top: var(--ui-space-8);
|
|
283
|
+
border-top: 1px solid var(--ui-border-low);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.nav-divider-label {
|
|
287
|
+
grid-column: 2;
|
|
288
|
+
font-size: var(--ui-font-size-xs);
|
|
289
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
290
|
+
color: var(--ui-text-tertiary);
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
letter-spacing: 0.04em;
|
|
293
|
+
opacity: 1;
|
|
294
|
+
transition: opacity 180ms ease;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.layout.condensed .nav-divider-label {
|
|
298
|
+
opacity: 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
251
301
|
.nav-item {
|
|
252
302
|
position: relative;
|
|
253
303
|
display: grid;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { createEventDispatcher, tick } from 'svelte';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
4
|
import Button from './Button.svelte';
|
|
5
|
-
import { editorState } from '../../editor/core/store/editorStore';
|
|
6
5
|
import type { ButtonVariant, DialogButtonSpec } from './types';
|
|
7
6
|
|
|
8
7
|
const BUTTON_VARIANTS: readonly ButtonVariant[] = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'];
|
|
@@ -10,6 +9,18 @@
|
|
|
10
9
|
return v && (BUTTON_VARIANTS as readonly string[]).includes(v) ? (v as ButtonVariant) : fallback;
|
|
11
10
|
}
|
|
12
11
|
|
|
12
|
+
// Read the configured Button variants from :root. The editor mutates these
|
|
13
|
+
// inline on documentElement via cssVarSync; a MutationObserver picks the
|
|
14
|
+
// changes up without coupling this component to the editor module graph.
|
|
15
|
+
// In production the var lives in the generated stylesheet and never
|
|
16
|
+
// changes, so the observer registers but never fires.
|
|
17
|
+
function readCssVar(name: string): string {
|
|
18
|
+
if (typeof document === 'undefined') return '';
|
|
19
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
20
|
+
}
|
|
21
|
+
let confirmVarValue = $state(readCssVar('--dialog-confirm-variant'));
|
|
22
|
+
let cancelVarValue = $state(readCssVar('--dialog-cancel-variant'));
|
|
23
|
+
|
|
13
24
|
interface Props {
|
|
14
25
|
show?: boolean;
|
|
15
26
|
title?: string;
|
|
@@ -39,9 +50,8 @@
|
|
|
39
50
|
children,
|
|
40
51
|
}: Props = $props();
|
|
41
52
|
|
|
42
|
-
let
|
|
43
|
-
let
|
|
44
|
-
let effectiveCancelVariant = $derived(cancel?.variant ?? asVariant(configuredConfig['--dialog-cancel-variant'] as string | undefined, 'outline'));
|
|
53
|
+
let effectiveConfirmVariant = $derived(confirm?.variant ?? asVariant(confirmVarValue, 'primary'));
|
|
54
|
+
let effectiveCancelVariant = $derived(cancel?.variant ?? asVariant(cancelVarValue, 'outline'));
|
|
45
55
|
|
|
46
56
|
// Dual-fire bridge — see Button.svelte for the deprecation timeline.
|
|
47
57
|
const dispatch = createEventDispatcher<{
|
|
@@ -52,6 +62,16 @@
|
|
|
52
62
|
let cancelButtonRef: HTMLButtonElement = $state()!;
|
|
53
63
|
let closeButtonRef: HTMLButtonElement = $state()!;
|
|
54
64
|
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (typeof document === 'undefined') return;
|
|
67
|
+
const obs = new MutationObserver(() => {
|
|
68
|
+
confirmVarValue = readCssVar('--dialog-confirm-variant');
|
|
69
|
+
cancelVarValue = readCssVar('--dialog-cancel-variant');
|
|
70
|
+
});
|
|
71
|
+
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
|
|
72
|
+
return () => obs.disconnect();
|
|
73
|
+
});
|
|
74
|
+
|
|
55
75
|
// Focus the primary button when dialog opens (skip in inline mode so the editor doesn't steal focus).
|
|
56
76
|
$effect(() => {
|
|
57
77
|
if (show && !inline) {
|