@motion-proto/live-tokens 0.24.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.claude/skills/live-tokens-create-component/SKILL.md +38 -24
  2. package/bin/check-component.mjs +52 -4
  3. package/dist-plugin/index.cjs +7 -0
  4. package/dist-plugin/index.js +7 -0
  5. package/package.json +1 -1
  6. package/src/editor/component-editor/CalloutEditor.svelte +1 -1
  7. package/src/editor/component-editor/CardEditor.svelte +1 -1
  8. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -1
  9. package/src/editor/component-editor/DialogEditor.svelte +1 -1
  10. package/src/editor/component-editor/PanelEditor.svelte +81 -0
  11. package/src/editor/component-editor/ProgressBarEditor.svelte +1 -1
  12. package/src/editor/component-editor/RadioButtonEditor.svelte +1 -1
  13. package/src/editor/component-editor/SectionDividerEditor.svelte +2 -2
  14. package/src/editor/component-editor/SideNavigationEditor.svelte +18 -15
  15. package/src/editor/component-editor/TabBarEditor.svelte +1 -1
  16. package/src/editor/component-editor/TableEditor.svelte +8 -17
  17. package/src/editor/component-editor/TooltipEditor.svelte +1 -1
  18. package/src/editor/component-editor/index.ts +8 -1
  19. package/src/editor/component-editor/registry.ts +11 -0
  20. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +120 -17
  21. package/src/editor/component-editor/scaffolding/types.ts +4 -0
  22. package/src/editor/overlay/LiveEditorOverlay.svelte +79 -3
  23. package/src/editor/styles/ui-editor.css +8 -2
  24. package/src/system/assets/github-mark-white.svg +1 -0
  25. package/src/system/assets/npm-mark-white.svg +1 -0
  26. package/src/system/assets/offering.webp +0 -0
  27. package/src/system/components/Button.svelte +12 -12
  28. package/src/system/components/CodeSnippet.svelte +4 -1
  29. package/src/system/components/Panel.svelte +44 -0
  30. package/src/system/components/SectionDivider.svelte +10 -10
  31. package/src/system/components/SideNavigation.svelte +13 -0
  32. package/src/system/styles/CONVENTIONS.md +13 -6
@@ -25,21 +25,24 @@ For pattern reference, read any shipped component's source directly from the con
25
25
 
26
26
  1. **Runtime file** — `src/system/components/MyWidget.svelte`. Declare every editable slot as a CSS custom property inside `:global(:root)`, defaulting to a theme token (never a raw value). The plugin parses `:global(:root)` to seed `component-configs/<id>/default.json`; variables declared anywhere else can't be edited.
27
27
  2. **Editor file** — `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block, declare `const component = 'mywidget'`, build a `states: Record<string, Token[]>` for each VariantGroup, and export the flat union as `allTokens: Token[]`. Components with linked siblings also build a `linkableContexts: Map<string, string>` (see the linked-siblings extension below). Components with structural/display controls that aren't token values (alignment, element visibility, layout position) also export an `intrinsics: IntrinsicSpec[]` (see the intrinsics extension below). In the runtime `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.
28
- 3. **Register** — in `src/main.ts` before `mount(App, ...)`:
28
+ 3. **Register** — pass the component to `bootLiveTokens` in `src/main.ts`. This is the standard boot the scaffold generates and the README documents; `bootLiveTokens` calls `registerComponent` internally at the right point — after its editor init hooks (`cssVarSync.init`, `editorStore.init`), before it seeds configs and mounts the app:
29
29
  ```ts
30
- import { registerComponent } from '@motion-proto/live-tokens';
30
+ import { bootLiveTokens } from '@motion-proto/live-tokens';
31
+ import App from './App.svelte';
31
32
  import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';
32
33
 
33
- registerComponent({
34
- id: 'mywidget',
35
- label: 'My Widget',
36
- icon: 'fas fa-magic',
37
- sourceFile: 'src/system/components/MyWidget.svelte',
38
- editorComponent: MyWidgetEditor,
39
- schema: myWidgetTokens,
34
+ bootLiveTokens(App, '#app', {
35
+ components: [{
36
+ id: 'mywidget',
37
+ label: 'My Widget',
38
+ icon: 'fas fa-magic',
39
+ sourceFile: 'src/system/components/MyWidget.svelte',
40
+ editorComponent: MyWidgetEditor,
41
+ schema: myWidgetTokens,
42
+ }],
40
43
  });
41
44
  ```
42
- The schema side-effect happens inside `registerComponent`, so you don't call `registerComponentSchema` separately.
45
+ The schema side-effect happens inside `registerComponent` (which `bootLiveTokens` calls for you), so you don't call `registerComponentSchema` separately. **Do not place a standalone `registerComponent(...)` *before* `bootLiveTokens`** — that registers before the editor's init hooks run, which is the wrong window and can leave editor changes disconnected from the live page. Only call `registerComponent` directly if your app mounts manually (no `bootLiveTokens`), in which case call it before `mount(App, ...)`.
43
46
  4. **Tell the picker** — open `.claude/skills/live-tokens-pick-component/SKILL.md` and add your new component to the **Catalogue** line under the family it belongs to (Action / Input / Selection / Containers / Messaging / Display). If it's confusable with an existing component (a second selection control, a competing container), add a row to that family's decision table explaining the use-case it owns. Without this step, the component exists but [[live-tokens-pick-component]] can't recommend it when a user asks "which component should I use?" — the same rule applies whether the component is first-party (update the picker shipped in this package) or consumer-authored (update the local copy at `.claude/skills/live-tokens-pick-component/SKILL.md` that `setup-claude` placed in your project).
44
47
  5. **Verify** — open `/components` and run the verification checklist at the bottom of this file.
45
48
 
@@ -107,7 +110,17 @@ The authoritative recognised list lives in `bin/check-component.mjs` (`KNOWN_SUF
107
110
  - **Defaults reference theme tokens, never raw values.** `var(--surface-primary)` ✓ — `#6a4ce8` ✗.
108
111
  - **No abbreviations.** `bg` → `surface`; `fg` → `text`; component ids are never abbreviated.
109
112
  - **Text aliases.** Neutral scale is `--text-primary` / `--text-secondary` / `--text-tertiary` / `--text-muted` / `--text-disabled`. Family-tinted is `--text-primary-color`, `--text-accent`, `--text-success`. There is no `--text-neutral`.
110
- - **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` ✓ — bare `groupKey: 'font-family'` silently merges them into one link tree ✗. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears.
113
+ - **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` ✓ — bare `groupKey: 'font-family'` silently merges them into one link tree ✗. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears. The same trap applies to type-group **colors** (two slots ending in `-text` collapsing to one `text` key). Let the helpers handle both: see "Deriving groupKeys" below.
114
+ - **Let the type-group helpers derive slot-scoped keys; never rely on the bare last-dash default.** When you build typography tokens with `buildTypeGroupColorTokens` / `buildTypeGroupTokens` / `buildTypeGroupFontTokens`, **pass `{ component, variants }`** so each slot gets a distinct, structural `groupKey`:
115
+
116
+ ```ts
117
+ // variants = the variant/state segment strings as they appear in the variable name
118
+ const VARIANTS = ['default', 'hover'] as const;
119
+ ...buildTypeGroupColorTokens(typeGroups, { component, variants: [...VARIANTS] }),
120
+ ...buildTypeGroupFontTokens(typeGroups, { component, variants: [...VARIANTS] }),
121
+ ```
122
+
123
+ The helper strips the `--<component>-` prefix and those segments, keeping the rest: `--mywidget-header-default-text` → `header-text`, `--mywidget-header-default-text-font-family` → `header-text-font-family`. Two parts ending in the same word stay distinct; one slot across variants collapses to one key. To override a single derived key, set `colorGroupKey` on that type-group config — it wins and is never recomputed, so your fix survives. There is no name-based fallback: a bare `buildTypeGroupColorTokens` call emits un-grouped (solo) colors rather than guessing, and a bare *font* helper across multiple slots is a `check-component` warning (its default `font-family`/… keys would merge the slots' fonts).
111
124
 
112
125
  ### Linked siblings
113
126
 
@@ -158,7 +171,7 @@ import {
158
171
  import type { Token } from '@motion-proto/live-tokens/component-editor';
159
172
  ```
160
173
 
161
- That covers everything the worked examples use. Additional primitives (`LinkedBlock`, `TypeEditor`, `TokenLayout`, `buildTypeGroupTokens`, more types) are exported from the same paths for advanced cases.
174
+ That covers everything the worked examples use. Additional primitives (`LinkedBlock`, `TypeEditor`, `TokenLayout`, `buildTypeGroupTokens`, `buildTypeGroupColorTokens`, `buildTypeGroupFontTokens`, `buildTypeGroupShareableContexts`, the `TypeGroupConfig` type, more types) are exported from the same paths for advanced cases.
162
175
 
163
176
  **Never deep-import `node_modules/@motion-proto/live-tokens/src/...`.** Reading those files for pattern reference is fine; importing them at runtime is not. If you need something not exported, file an issue rather than reaching in.
164
177
 
@@ -376,22 +389,23 @@ What to notice:
376
389
  ### Register: `src/main.ts`
377
390
 
378
391
  ```ts
379
- import { registerComponent } from '@motion-proto/live-tokens';
392
+ import { bootLiveTokens } from '@motion-proto/live-tokens';
393
+ import App from './App.svelte';
380
394
  import ToggleEditor, { allTokens as toggleTokens } from './system/components/ToggleEditor.svelte';
381
395
 
382
- registerComponent({
383
- id: 'mytoggle', // unique id; don't reuse 'toggle'
384
- label: 'My Toggle',
385
- icon: 'fas fa-toggle-on',
386
- sourceFile: 'src/system/components/Toggle.svelte',
387
- editorComponent: ToggleEditor,
388
- schema: toggleTokens,
396
+ bootLiveTokens(App, '#app', {
397
+ components: [{
398
+ id: 'mytoggle', // unique id; don't reuse 'toggle'
399
+ label: 'My Toggle',
400
+ icon: 'fas fa-toggle-on',
401
+ sourceFile: 'src/system/components/Toggle.svelte',
402
+ editorComponent: ToggleEditor,
403
+ schema: toggleTokens,
404
+ }],
389
405
  });
390
-
391
- // then mount(App, ...)
392
406
  ```
393
407
 
394
- If you do this with `id: 'toggle'`, the consumer's component wins over the built-in (with a console warning). The collision rule protects you, but for a fresh component pick an id that doesn't collide.
408
+ If your app mounts manually instead of via `bootLiveTokens`, call `registerComponent({ id: 'mytoggle', ... })` yourself before `mount(App, ...)`. If you reuse `id: 'toggle'`, the consumer's component wins over the built-in (with a console warning). The collision rule protects you, but for a fresh component pick an id that doesn't collide.
395
409
 
396
410
  ## Extension: linked siblings
397
411
 
@@ -530,7 +544,7 @@ npx live-tokens check-component <id>
530
544
  # or: npx @motion-proto/live-tokens check-component <id>
531
545
  ```
532
546
 
533
- It enforces the file layout, the `:global(:root)` block, token-suffix vocabulary, state-before-property rule, theme-token defaults (no raw colour literals), public-imports rule, and the `registerComponent({ id })` call. Exit code 0 means the static contract is met.
547
+ It enforces the file layout, the `:global(:root)` block, token-suffix vocabulary, state-before-property rule, theme-token defaults (no raw colour literals), public-imports rule, and that the id is registered — via either `bootLiveTokens({ components: [{ id }] })` or a direct `registerComponent({ id })` call. It also *warns* (non-fatal) when a type-group font helper is called bare across multiple slots, which would merge their fonts into one link tree. Exit code 0 means the static contract is met; resolve warnings before shipping.
534
548
 
535
549
  **Then run the registry contract test.** If you're authoring inside the package itself, `src/editor/component-editor/registryContract.test.ts` runs `describe.each(getComponentRegistryEntries())` and verifies, per component:
536
550
 
@@ -88,6 +88,28 @@ function detectStateAfterProperty(token) {
88
88
  return null;
89
89
  }
90
90
 
91
+ // True if `source` calls `fnName(` at least once with a single argument (no
92
+ // top-level comma before the matching close paren). Brackets/braces are balanced
93
+ // so commas inside nested objects/arrays/calls don't count.
94
+ function hasBareCall(source, fnName) {
95
+ const needle = fnName + '(';
96
+ let idx = 0;
97
+ while ((idx = source.indexOf(needle, idx)) !== -1) {
98
+ let i = idx + needle.length;
99
+ let depth = 1;
100
+ let topComma = false;
101
+ for (; i < source.length && depth > 0; i++) {
102
+ const c = source[i];
103
+ if (c === '(' || c === '[' || c === '{') depth++;
104
+ else if (c === ')' || c === ']' || c === '}') depth--;
105
+ else if (c === ',' && depth === 1) topComma = true;
106
+ }
107
+ if (!topComma) return true;
108
+ idx = i;
109
+ }
110
+ return false;
111
+ }
112
+
91
113
  function findFilesRecursive(dir, exts) {
92
114
  if (!existsSync(dir)) return [];
93
115
  const out = [];
@@ -184,6 +206,27 @@ export function checkComponent(id, root = process.cwd()) {
184
206
  errors.push(`${relative(root, editorPath)}: missing 'export const allTokens'`);
185
207
  }
186
208
 
209
+ // Editor: phantom-link guard. The font type-group helpers fall back to bare
210
+ // `font-family`/`font-size`/… keys when called with a single argument (no
211
+ // derivation). Across more than one slot that silently links every slot's fonts
212
+ // into one tree. Passing `{ component, variants }` (a second arg) opts into
213
+ // distinct, structural keys and suppresses the check, so this only fires on the
214
+ // silent inference path. (The color helper no longer infers — a bare call there
215
+ // emits solo, un-grouped colors, which can't phantom-link.)
216
+ const colorPatterns = new Set();
217
+ for (const m of editor.matchAll(/colorVariable\s*:\s*[`'"]([^`'"]+)[`'"]/g)) {
218
+ colorPatterns.add(m[1].replace(/\$\{[^}]*\}/g, '*'));
219
+ }
220
+ const slots = colorPatterns.size;
221
+ const fontBare =
222
+ hasBareCall(editor, 'buildTypeGroupFontTokens') || hasBareCall(editor, 'buildTypeGroupTokens');
223
+ if (slots > 1 && fontBare) {
224
+ warnings.push(
225
+ `${relative(root, editorPath)}: a type-group font helper is called across ${slots} slots without a derivation; ` +
226
+ `its bare font-family/font-size/… keys would phantom-link every slot's fonts. Pass { component, variants } to buildTypeGroupTokens/buildTypeGroupFontTokens.`,
227
+ );
228
+ }
229
+
187
230
  // Imports across runtime + editor: reject deep imports into the package.
188
231
  for (const [path, source] of [[runtimePath, runtime], [editorPath, editor]]) {
189
232
  for (const imp of extractImports(source)) {
@@ -195,13 +238,18 @@ export function checkComponent(id, root = process.cwd()) {
195
238
  }
196
239
  }
197
240
 
198
- // Registration: registerComponent({ id: '<id>', ... }) somewhere under src/.
241
+ // Registration: either a direct registerComponent({ id }) call or the id
242
+ // passed through bootLiveTokens({ components: [{ id }] }) — the standard
243
+ // scaffold boot. Accept both, somewhere under src/.
199
244
  const srcFiles = findFilesRecursive(join(root, 'src'), ['.ts', '.js', '.svelte', '.mjs']);
200
- const regPattern = new RegExp(`registerComponent\\s*\\(\\s*\\{[^}]*id\\s*:\\s*['"]${id}['"]`, 's');
245
+ const idLiteral = `id\\s*:\\s*['"]${id}['"]`;
246
+ const directPattern = new RegExp(`registerComponent\\s*\\(\\s*\\{[^}]*${idLiteral}`, 's');
247
+ const bootPattern = new RegExp(`bootLiveTokens\\s*\\([\\s\\S]*?components\\s*:\\s*\\[[\\s\\S]*?${idLiteral}`, 's');
201
248
  let registrationFile = null;
202
249
  for (const file of srcFiles) {
203
250
  try {
204
- if (regPattern.test(readFileSync(file, 'utf8'))) {
251
+ const src = readFileSync(file, 'utf8');
252
+ if (directPattern.test(src) || bootPattern.test(src)) {
205
253
  registrationFile = file;
206
254
  break;
207
255
  }
@@ -210,7 +258,7 @@ export function checkComponent(id, root = process.cwd()) {
210
258
  }
211
259
  }
212
260
  if (!registrationFile) {
213
- errors.push(`registerComponent({ id: '${id}', ... }) not found anywhere under src/`);
261
+ errors.push(`no registration for '${id}' under src/ — expected registerComponent({ id: '${id}', ... }) or bootLiveTokens({ components: [{ id: '${id}', ... }] })`);
214
262
  } else {
215
263
  // Check the registration file's imports too.
216
264
  const regSource = readFileSync(registrationFile, 'utf8');
@@ -1114,6 +1114,13 @@ ${lines.join("\n")}
1114
1114
  } catch {
1115
1115
  }
1116
1116
  }
1117
+ if (existingAliases) {
1118
+ for (const [token, val] of Object.entries(existingAliases)) {
1119
+ if (val !== null && typeof val === "object" && !(token in aliases)) {
1120
+ aliases[token] = val;
1121
+ }
1122
+ }
1123
+ }
1117
1124
  const aliasesUnchanged = existingAliases !== void 0 && JSON.stringify(existingAliases) === JSON.stringify(aliases);
1118
1125
  const defaultConfig = {
1119
1126
  name: "default",
@@ -854,6 +854,13 @@ ${lines.join("\n")}
854
854
  } catch {
855
855
  }
856
856
  }
857
+ if (existingAliases) {
858
+ for (const [token, val] of Object.entries(existingAliases)) {
859
+ if (val !== null && typeof val === "object" && !(token in aliases)) {
860
+ aliases[token] = val;
861
+ }
862
+ }
863
+ }
857
864
  const aliasesUnchanged = existingAliases !== void 0 && JSON.stringify(existingAliases) === JSON.stringify(aliases);
858
865
  const defaultConfig = {
859
866
  name: "default",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.24.2",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -54,7 +54,7 @@
54
54
  }
55
55
  export const allTokens: Token[] = variants.flatMap((v) => [
56
56
  ...variantTokens(v),
57
- ...buildTypeGroupColorTokens(variantTypeGroups(v)),
57
+ ...buildTypeGroupColorTokens(variantTypeGroups(v), { component, variants: [...variants] }),
58
58
  ...variantTypeGroupTokens(v),
59
59
  ]);
60
60
 
@@ -80,7 +80,7 @@
80
80
  ]);
81
81
  export const allTokens: Token[] = [
82
82
  ...Object.values(states).flat(),
83
- ...buildTypeGroupColorTokens(typeGroups),
83
+ ...buildTypeGroupColorTokens(typeGroups, { component, variants: ['default'] }),
84
84
  ...typeGroupTokens,
85
85
  ];
86
86
  </script>
@@ -93,7 +93,7 @@
93
93
 
94
94
  export const allTokens: Token[] = [
95
95
  ...VARIANTS.flatMap((v) => Object.values(variantStates(v)).flat()),
96
- ...VARIANTS.flatMap((v) => buildTypeGroupColorTokens(variantTypeGroups(v))),
96
+ ...VARIANTS.flatMap((v) => buildTypeGroupColorTokens(variantTypeGroups(v), { component, variants: [...VARIANTS, ...HEADER_STATES] })),
97
97
  ...headerTypeGroupTokens,
98
98
  ];
99
99
 
@@ -78,7 +78,7 @@
78
78
 
79
79
  export const allTokens: Token[] = [
80
80
  ...Object.values(frameStates).flat(),
81
- ...buildTypeGroupColorTokens(frameTypeGroups),
81
+ ...buildTypeGroupColorTokens(frameTypeGroups, { component }),
82
82
  ...frameTypeGroupTokens,
83
83
  ];
84
84
 
@@ -0,0 +1,81 @@
1
+ <script module lang="ts">
2
+ import type { Token } from './scaffolding/types';
3
+ export const component = 'panel';
4
+
5
+ // Frame + the non-surface stage props render as ordinary token rows. The
6
+ // stage surface is a structured (tokenized) gradient edited via the
7
+ // GradientEditor below, so it lives outside the row list — `kind: 'gradient'`
8
+ // marks it as a structured payload and folds it back into `allTokens` for the
9
+ // schema without rendering a flat color row for it.
10
+ const states: Record<string, Token[]> = {
11
+ default: [
12
+ { label: 'border color', variable: '--panel-frame-border', element: 'frame' },
13
+ { label: 'border width', variable: '--panel-frame-border-width', element: 'frame' },
14
+ { label: 'corner radius', variable: '--panel-frame-radius', element: 'frame' },
15
+
16
+ { label: 'vertical padding', variable: '--panel-stage-padding', splittable: false, element: 'stage' },
17
+ { label: 'side padding', variable: '--panel-stage-inline-padding', splittable: false, element: 'stage' },
18
+ { label: 'content gap', variable: '--panel-stage-gap', element: 'stage' },
19
+ ],
20
+ };
21
+ const surfaceToken: Token = {
22
+ label: 'surface',
23
+ variable: '--panel-stage-surface',
24
+ kind: 'gradient',
25
+ family: 'neutral',
26
+ element: 'stage',
27
+ };
28
+ export const allTokens: Token[] = [...Object.values(states).flat(), surfaceToken];
29
+ </script>
30
+
31
+ <script lang="ts">
32
+ import Panel from '../../system/components/Panel.svelte';
33
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
34
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
35
+ import GradientEditor from '../ui/GradientEditor.svelte';
36
+ import { componentGradientSource } from '../core/store/gradientSource';
37
+
38
+ const surfaceGradient = componentGradientSource(component, surfaceToken.variable);
39
+ </script>
40
+
41
+ <ComponentEditorBase
42
+ {component}
43
+ title="Panel"
44
+ description="Framed container with a tokenized border and gradient surface."
45
+ tokens={allTokens}
46
+ >
47
+ <VariantGroup name="panel" title="Panel" {states} {component} elementOrder={['frame', 'stage']}>
48
+ {#snippet compositeControls(_stateName)}
49
+ <div class="panel-surface-section">
50
+ <GradientEditor
51
+ sectionLabel="Surface"
52
+ source={surfaceGradient}
53
+ stopIdPrefix="panel-surface"
54
+ familyFilter="neutral"
55
+ />
56
+ </div>
57
+ {/snippet}
58
+ {#snippet children()}
59
+ <div class="panel-demo">
60
+ <Panel minHeight="6rem">
61
+ <span class="demo-chip">Stage content</span>
62
+ <span class="demo-chip">Stage content</span>
63
+ </Panel>
64
+ </div>
65
+ {/snippet}
66
+ </VariantGroup>
67
+ </ComponentEditorBase>
68
+
69
+ <style>
70
+ .panel-demo {
71
+ width: 100%;
72
+ max-width: 34rem;
73
+ }
74
+ .demo-chip {
75
+ padding: var(--ui-space-4) var(--ui-space-8);
76
+ border: 1px solid var(--ui-border);
77
+ border-radius: var(--ui-radius-sm);
78
+ font-size: var(--ui-font-size-sm);
79
+ color: var(--ui-text-secondary);
80
+ }
81
+ </style>
@@ -68,7 +68,7 @@
68
68
 
69
69
  export const allTokens: Token[] = [
70
70
  ...Object.values(states).flat(),
71
- ...buildTypeGroupColorTokens(typeGroups),
71
+ ...buildTypeGroupColorTokens(typeGroups, { component }),
72
72
  ...typeGroupTokens,
73
73
  ];
74
74
  </script>
@@ -76,7 +76,7 @@
76
76
  ]);
77
77
  export const allTokens: Token[] = [
78
78
  ...Object.values(states).flat(),
79
- ...buildTypeGroupColorTokens(typeGroups),
79
+ ...buildTypeGroupColorTokens(typeGroups, { component, variants: Object.keys(states) }),
80
80
  ...typeGroupTokens,
81
81
  ];
82
82
  </script>
@@ -165,7 +165,7 @@
165
165
  variants: ['lg', 'md', 'sm'],
166
166
  variable: (v) => `--sectiondivider-${v}-align`,
167
167
  values: ['start', 'center'],
168
- default: { lg: 'start', md: 'start', sm: 'start' },
168
+ default: { lg: 'start', md: 'start', sm: 'center' },
169
169
  },
170
170
  {
171
171
  key: 'eyebrow-display',
@@ -193,7 +193,7 @@
193
193
  variants: ['lg', 'md', 'sm'],
194
194
  variable: (v) => `--sectiondivider-${v}-hairline`,
195
195
  values: ['none', ...HAIRLINE_POSITIONS],
196
- default: { lg: 'below-description', md: 'below-label', sm: 'below-label' },
196
+ default: { lg: 'below-description', md: 'below-label', sm: 'none' },
197
197
  // 'above-description' renders identically to 'below-label'; the position
198
198
  // dropdown omits it, so coerce on read to keep the control's value valid.
199
199
  normalize: (raw) => (raw === 'above-description' ? 'below-label' : raw),
@@ -40,9 +40,9 @@
40
40
 
41
41
  // --- Title --------------------------------------------------------------
42
42
  // Title is a flex card containing the label box + toggle box. Per-state
43
- // tokens drive the card chrome (surface, border, padding); stateless gap
44
- // and radius live in `titleLayoutTokens` since they don't vary across
45
- // default/hover/active.
43
+ // tokens drive the card chrome (surface, border, padding); the stateless
44
+ // bar + label-box structure lives in `titleBlockTokens` since it doesn't
45
+ // vary across default/hover/active.
46
46
  function titleStateTokens(s: StatefulState): Token[] {
47
47
  return [
48
48
  { label: 'surface color', groupKey: 'title-surface', variable: `--sidenavigation-title-${s}-surface` },
@@ -51,9 +51,16 @@
51
51
  { label: 'padding', canBeLinked: true, groupKey: 'title-padding', variable: `--sidenavigation-title-${s}-padding` },
52
52
  ];
53
53
  }
54
- const titleLayoutTokens: Token[] = [
54
+ // Stateless title-bar structure ("Title Block" tab): the bar's own geometry
55
+ // plus the inner label box. Separate from the stateful Title tab because
56
+ // these don't vary by interaction state. Label-box rows are prefixed "label"
57
+ // to disambiguate from the bar's own corner radius.
58
+ const titleBlockTokens: Token[] = [
55
59
  { label: 'gap', canBeLinked: true, groupKey: 'title-gap', variable: '--sidenavigation-title-gap' },
56
60
  { label: 'corner radius', canBeLinked: true, groupKey: 'title-radius', variable: '--sidenavigation-title-radius' },
61
+ { label: 'label surface color', groupKey: 'title-label-surface', variable: '--sidenavigation-title-label-surface' },
62
+ { label: 'label corner radius', canBeLinked: true, groupKey: 'title-label-radius', variable: '--sidenavigation-title-label-radius' },
63
+ { label: 'label padding', canBeLinked: true, groupKey: 'title-label-padding', variable: '--sidenavigation-title-label-padding' },
57
64
  ];
58
65
  function titleStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
59
66
  return [{
@@ -65,14 +72,6 @@
65
72
  lineHeightVariable: `--sidenavigation-title-${s}-label-line-height`,
66
73
  }];
67
74
  }
68
- // Title label — structural inner box (stateless, like Panel). Sits inside
69
- // the title bar so the header reads as: outer bar → [label box] [toggle box].
70
- const titleLabelTokens: Token[] = [
71
- { label: 'surface color', groupKey: 'title-label-surface', variable: '--sidenavigation-title-label-surface' },
72
- { label: 'corner radius', canBeLinked: true, groupKey: 'title-label-radius', variable: '--sidenavigation-title-label-radius' },
73
- { label: 'padding', canBeLinked: true, groupKey: 'title-label-padding', variable: '--sidenavigation-title-label-padding' },
74
- ];
75
-
76
75
  const titleTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
77
76
  { label: 'font family', canBeLinked: true, groupKey: 'title-label-font-family', variable: `--sidenavigation-title-${s}-label-font-family` },
78
77
  { label: 'font size', canBeLinked: true, groupKey: 'title-label-font-size', variable: `--sidenavigation-title-${s}-label-font-size` },
@@ -182,8 +181,7 @@
182
181
  const states: Record<string, Token[]> = {
183
182
  'Panel': panelTokens,
184
183
  ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Title / ${STATE_LABELS[s]}`, titleStateTokens(s)])),
185
- 'Title Layout': titleLayoutTokens,
186
- 'Title Label': titleLabelTokens,
184
+ 'Title Block': titleBlockTokens,
187
185
  ...Object.fromEntries(TOGGLE_STATES.map((s) => [`Toggle / ${STATE_LABELS[s]}`, toggleStateTokens(s)])),
188
186
  ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Section / ${STATE_LABELS[s]}`, sectionStateTokens(s)])),
189
187
  ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Item / ${STATE_LABELS[s]}`, itemStateTokens(s)])),
@@ -197,9 +195,14 @@
197
195
  ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Footer / ${STATE_LABELS[s]}`, footerStateTypeGroups(s)])),
198
196
  };
199
197
 
198
+ // Color groupKeys derive structurally from the part+role: stripping the
199
+ // `--sidenavigation-` prefix and the state segment leaves `section-text`,
200
+ // `item-text`, `footer-text`, `title-label` — so the per-part text colors stay
201
+ // distinct instead of collapsing into one inferred `text` link group (which
202
+ // would phantom-link parts the per-part font groupKeys deliberately keep apart).
200
203
  export const allTokens: Token[] = [
201
204
  ...Object.values(states).flat(),
202
- ...buildTypeGroupColorTokens(typeGroups),
205
+ ...buildTypeGroupColorTokens(typeGroups, { component, variants: [...STATEFUL_STATES] }),
203
206
  ...titleTypographyTokens,
204
207
  ...sectionTypographyTokens,
205
208
  ...itemTypographyTokens,
@@ -65,7 +65,7 @@
65
65
  ]);
66
66
  export const allTokens: Token[] = [
67
67
  ...Object.values(states).flat(),
68
- ...buildTypeGroupColorTokens(typeGroups),
68
+ ...buildTypeGroupColorTokens(typeGroups, { component, variants: [...tabStateNames] }),
69
69
  ...tabTypeGroupTokens,
70
70
  ];
71
71
 
@@ -1,4 +1,5 @@
1
1
  <script module lang="ts">
2
+ import { buildTypeGroupColorTokens, buildTypeGroupFontTokens } from './scaffolding/buildTypeGroupTokens';
2
3
  import type { Token, TypeGroupConfig } from './scaffolding/types';
3
4
 
4
5
  export const component = 'table';
@@ -60,26 +61,16 @@
60
61
  lineHeightVariable: '--table-default-cell-line-height',
61
62
  }],
62
63
  };
63
- // Hand-rolled (not buildTypeGroupColorTokens) because its suffix-derived groupKey would phantom-link header-text and cell-text.
64
- const typeGroupColorTokens: Token[] = [
65
- { label: 'color', groupKey: 'header-text', variable: '--table-default-header-text' },
66
- { label: 'color', groupKey: 'cell-text', variable: '--table-default-cell-text' },
67
- ];
68
- const typeGroupTokens: Token[] = [
69
- { label: 'font family', groupKey: 'header-font-family', variable: '--table-default-header-font-family' },
70
- { label: 'font size', groupKey: 'header-font-size', variable: '--table-default-header-font-size' },
71
- { label: 'font weight', groupKey: 'header-font-weight', variable: '--table-default-header-font-weight' },
72
- { label: 'line height', groupKey: 'header-line-height', variable: '--table-default-header-line-height' },
73
- { label: 'font family', groupKey: 'cell-font-family', variable: '--table-default-cell-font-family' },
74
- { label: 'font size', groupKey: 'cell-font-size', variable: '--table-default-cell-font-size' },
75
- { label: 'font weight', groupKey: 'cell-font-weight', variable: '--table-default-cell-font-weight' },
76
- { label: 'line height', groupKey: 'cell-line-height', variable: '--table-default-cell-line-height' },
77
- ];
64
+ // Structural derivation keeps header / cell text apart: stripping the `--table-`
65
+ // prefix and the `default` state segment leaves `header-text` / `cell-text` (and
66
+ // `header-font-family`, `cell-line-height`, …). A bare last-dash key would
67
+ // phantom-link header-text and cell-text into one `text` group.
68
+ const derivation = { component, variants: ['default'] } as const;
78
69
 
79
70
  export const allTokens: Token[] = [
80
71
  ...Object.values(states).flat(),
81
- ...typeGroupColorTokens,
82
- ...typeGroupTokens,
72
+ ...buildTypeGroupColorTokens(typeGroups, derivation),
73
+ ...buildTypeGroupFontTokens(typeGroups, derivation),
83
74
  ];
84
75
  </script>
85
76
 
@@ -35,7 +35,7 @@
35
35
  ];
36
36
  export const allTokens: Token[] = [
37
37
  ...Object.values(states).flat(),
38
- ...buildTypeGroupColorTokens(typeGroups),
38
+ ...buildTypeGroupColorTokens(typeGroups, { component }),
39
39
  ...typeGroupTokens,
40
40
  ];
41
41
  </script>
@@ -14,7 +14,14 @@ export { buildSiblings } from './scaffolding/siblings';
14
14
  export type { Sibling } from './scaffolding/siblings';
15
15
  export { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
16
16
  export type { LinkedToken, LinkedGroup, LinkedBlockResult } from './scaffolding/linkedBlock';
17
- export { buildTypeGroupTokens } from './scaffolding/buildTypeGroupTokens';
17
+ export {
18
+ buildTypeGroupTokens,
19
+ buildTypeGroupColorTokens,
20
+ buildTypeGroupFontTokens,
21
+ buildTypeGroupShareableContexts,
22
+ structuralGroupKey,
23
+ } from './scaffolding/buildTypeGroupTokens';
24
+ export type { TypeGroupConfig } from './scaffolding/types';
18
25
 
19
26
  // Token schema type — the shape of an entry in an editor's `allTokens` array.
20
27
  export type { Token } from './scaffolding/types';
@@ -16,6 +16,7 @@ import InlineEditActionsEditor, { allTokens as inlineEditActionsTokens } from '.
16
16
  import InputEditor, { allTokens as inputTokens } from './InputEditor.svelte';
17
17
  import MenuSelectEditor, { allTokens as menuSelectTokens } from './MenuSelectEditor.svelte';
18
18
  import NotificationEditor, { allTokens as notificationTokens } from './NotificationEditor.svelte';
19
+ import PanelEditor, { allTokens as panelTokens } from './PanelEditor.svelte';
19
20
  import ProgressBarEditor, { allTokens as progressBarTokens } from './ProgressBarEditor.svelte';
20
21
  import RadioButtonEditor, { allTokens as radioButtonTokens } from './RadioButtonEditor.svelte';
21
22
  import SectionDividerEditor, { allTokens as sectionDividerTokens, intrinsics as sectionDividerIntrinsics } from './SectionDividerEditor.svelte';
@@ -43,6 +44,7 @@ type BuiltInComponentId =
43
44
  | 'inlineeditactions'
44
45
  | 'input'
45
46
  | 'menuselect'
47
+ | 'panel'
46
48
  | 'sectiondivider'
47
49
  | 'collapsiblesection'
48
50
  | 'sidenavigation'
@@ -223,6 +225,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
223
225
  schema: menuSelectTokens,
224
226
  origin: 'system',
225
227
  },
228
+ panel: {
229
+ id: 'panel',
230
+ label: 'Panel',
231
+ icon: 'fas fa-window-maximize',
232
+ sourceFile: 'src/system/components/Panel.svelte',
233
+ editorComponent: PanelEditor,
234
+ schema: panelTokens,
235
+ origin: 'system',
236
+ },
226
237
  sectiondivider: {
227
238
  id: 'sectiondivider',
228
239
  label: 'Section Divider',