@motion-proto/live-tokens 0.23.0 → 0.24.1

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.
@@ -24,7 +24,7 @@ For pattern reference, read any shipped component's source directly from the con
24
24
  ## 4-step recipe
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
- 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). In the runtime `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.
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
28
  3. **Register** — in `src/main.ts` before `mount(App, ...)`:
29
29
  ```ts
30
30
  import { registerComponent } from '@motion-proto/live-tokens';
@@ -462,6 +462,65 @@ Toggle's tokens are flat per state. Most multi-variant components (Badge, Card,
462
462
 
463
463
  Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps — `ComponentEditorBase` renders fine without a `{linked}` prop.
464
464
 
465
+ ## Extension: intrinsics
466
+
467
+ Some components expose **structural or display choices** that aren't token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a bespoke `<select>` or checkbox you author in an editor snippet, not the generic token grid, so they don't belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).
468
+
469
+ An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the "change" the user thinks they made. `:global(:root)` is the source of truth.
470
+
471
+ Declare intrinsics so the editor and the contract test stay honest:
472
+
473
+ 1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:
474
+
475
+ ```css
476
+ --mywidget-lg-align: start;
477
+ --mywidget-lg-eyebrow-display: block;
478
+ ```
479
+
480
+ 2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:
481
+
482
+ ```ts
483
+ import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';
484
+
485
+ export const intrinsics: IntrinsicSpec[] = [
486
+ {
487
+ key: 'align',
488
+ variants: ['lg', 'md', 'sm'],
489
+ variable: (v) => `--mywidget-${v}-align`,
490
+ values: ['start', 'center'],
491
+ default: { lg: 'start', md: 'start', sm: 'start' },
492
+ },
493
+ ];
494
+ ```
495
+
496
+ 3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:
497
+
498
+ ```ts
499
+ const byKey = new Map(intrinsics.map((i) => [i.key, i]));
500
+ function readIntrinsic(key: string, v: string): string {
501
+ const spec = byKey.get(key)!;
502
+ const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default
503
+ return spec.normalize ? spec.normalize(raw) : raw;
504
+ }
505
+ function getAlign(v: string) {
506
+ return readIntrinsic('align', v) === 'center' ? 'center' : 'start';
507
+ }
508
+ ```
509
+
510
+ Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.
511
+
512
+ 4. **Pass `intrinsics` to `registerComponent`** so the contract test can see it:
513
+
514
+ ```ts
515
+ registerComponent({
516
+ id: 'mywidget',
517
+ // ...label, icon, sourceFile, editorComponent, schema...
518
+ intrinsics: myWidgetIntrinsics,
519
+ });
520
+ ```
521
+
522
+ Use `normalize` only when two raw values render identically and the dropdown lists just one (SectionDivider folds `above-description` into `below-label`). Properties that look like intrinsics but aren't: preview-only props with no persistence (a size selector that only changes the demo), `setComponentConfig` editor metadata (Dialog's button variants), and token-valued selects (a control choosing between two tokens). None carry a duplicated runtime default, so none need an `IntrinsicSpec`.
523
+
465
524
  ## Verification checklist
466
525
 
467
526
  After saving, run the static validator first:
@@ -483,6 +542,8 @@ It enforces the file layout, the `:global(:root)` block, token-suffix vocabulary
483
542
 
484
543
  A new first-party component is auto-covered the moment it lands in `builtInRegistry` — `npm test` will fail if any of the five checks miss. For a consumer-authored component, mirror this pattern in your own test suite if you want the same drift protection (the same test logic works against any `registerComponent` registration; iterate `getComponentRegistryEntries()` after your `main.ts` has run).
485
544
 
545
+ **If your component declares `intrinsics`, the intrinsics contract test covers it too.** `src/editor/component-editor/intrinsicsContract.test.ts` iterates every entry with an `intrinsics` array and asserts, per (intrinsic, variant), that the runtime `:global(:root)` declares a default, the default is one of the spec's `values`, and the editor's `default` equals the runtime default. This is what would have caught a getter defaulting to `center` while `:global(:root)` says `start`. Same auto-coverage rule: declare `intrinsics` on the registry entry and the test picks it up.
546
+
486
547
  Finally navigate to `/components` and confirm the runtime behaviours no static check can see:
487
548
 
488
549
  - [ ] The new component appears in the nav rail under the **CUSTOM** group (system entries above, custom below the labeled divider).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -9,19 +9,20 @@
9
9
 
10
10
  function variantTokens(v: Variant): Token[] {
11
11
  return [
12
- { label: 'surface color', groupKey: 'surface', variable: `--callout-${v}-surface` },
13
- { label: 'border color', groupKey: 'border', variable: `--callout-${v}-border` },
14
- { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: `--callout-${v}-border-width` },
15
- { label: 'accent edge width', canBeLinked: true, groupKey: 'accent-width', variable: `--callout-${v}-accent-width` },
16
- { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--callout-${v}-radius` },
17
- { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--callout-${v}-padding` },
12
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: `--callout-${v}-surface` },
13
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: `--callout-${v}-border` },
14
+ { label: 'border width', element: 'frame', canBeLinked: true, groupKey: 'border-width', variable: `--callout-${v}-border-width` },
15
+ { label: 'accent edge width', element: 'frame', canBeLinked: true, groupKey: 'accent-width', variable: `--callout-${v}-accent-width` },
16
+ { label: 'corner radius', element: 'frame', canBeLinked: true, groupKey: 'radius', variable: `--callout-${v}-radius` },
17
+ { label: 'padding', element: 'frame', canBeLinked: true, groupKey: 'padding', variable: `--callout-${v}-padding` },
18
18
  ];
19
19
  }
20
20
  // Two type groups per variant: label (e.g. "Warning.") and message body.
21
21
  function variantTypeGroups(v: Variant): TypeGroupConfig[] {
22
22
  return [
23
23
  {
24
- legend: `${v} label`,
24
+ legend: '',
25
+ element: 'label',
25
26
  colorVariable: `--callout-${v}-label`,
26
27
  familyVariable: `--callout-${v}-label-font-family`,
27
28
  sizeVariable: `--callout-${v}-label-font-size`,
@@ -29,7 +30,8 @@
29
30
  lineHeightVariable: `--callout-${v}-label-line-height`,
30
31
  },
31
32
  {
32
- legend: `${v} message`,
33
+ legend: '',
34
+ element: 'message',
33
35
  colorVariable: `--callout-${v}-text`,
34
36
  familyVariable: `--callout-${v}-text-font-family`,
35
37
  sizeVariable: `--callout-${v}-text-font-size`,
@@ -3,23 +3,29 @@
3
3
 
4
4
  export const component = 'codesnippet';
5
5
 
6
- // Single variant. Default carries every container, code, and icon token;
7
- // hover layers a single icon-color override on top.
6
+ // `element` tags partition the panel into labeled groups (text / frame / icon /
7
+ // scrollbar) via StateBlock; labels drop the prefix the group heading carries.
8
+ // Order is permanent → impermanent. Hover overrides the icon color only.
8
9
  const states: Record<string, Token[]> = {
9
10
  default: [
10
- { label: 'surface', variable: '--codesnippet-surface' },
11
- { label: 'border', variable: '--codesnippet-border' },
12
- { label: 'border width', variable: '--codesnippet-border-width' },
13
- { label: 'corner radius', variable: '--codesnippet-radius' },
14
- { label: 'padding', variable: '--codesnippet-padding' },
15
- { label: 'gap', variable: '--codesnippet-gap' },
16
- { label: 'code text', variable: '--codesnippet-code-text' },
17
- { label: 'code font family', variable: '--codesnippet-code-font-family' },
18
- { label: 'code font size', variable: '--codesnippet-code-font-size' },
19
- { label: 'code font weight', variable: '--codesnippet-code-font-weight' },
20
- { label: 'code line height', variable: '--codesnippet-code-line-height' },
21
- { label: 'icon color', variable: '--codesnippet-icon' },
22
- { label: 'icon size', variable: '--codesnippet-icon-size' },
11
+ { label: 'color', element: 'text', variable: '--codesnippet-code-text' },
12
+ { label: 'font family', element: 'text', variable: '--codesnippet-code-font-family' },
13
+ { label: 'font size', element: 'text', variable: '--codesnippet-code-font-size' },
14
+ { label: 'font weight', element: 'text', variable: '--codesnippet-code-font-weight' },
15
+ { label: 'line height', element: 'text', variable: '--codesnippet-code-line-height' },
16
+
17
+ { label: 'surface', element: 'frame', variable: '--codesnippet-surface' },
18
+ { label: 'border', element: 'frame', variable: '--codesnippet-border' },
19
+ { label: 'border width', element: 'frame', variable: '--codesnippet-border-width' },
20
+ { label: 'corner radius', element: 'frame', variable: '--codesnippet-radius' },
21
+ { label: 'padding', element: 'frame', variable: '--codesnippet-padding' },
22
+ { label: 'gap', element: 'frame', variable: '--codesnippet-gap' },
23
+
24
+ { label: 'color', element: 'icon', variable: '--codesnippet-icon' },
25
+ { label: 'size', element: 'icon', variable: '--codesnippet-icon-size' },
26
+
27
+ { label: 'thumb', element: 'scrollbar', variable: '--codesnippet-scrollbar-thumb' },
28
+ { label: 'width', element: 'scrollbar', variable: '--codesnippet-scrollbar-border-width' },
23
29
  ],
24
30
  hover: [
25
31
  { label: 'icon color', variable: '--codesnippet-hover-icon' },
@@ -33,19 +39,30 @@
33
39
  import CodeSnippet from '../../system/components/CodeSnippet.svelte';
34
40
  import VariantGroup from './scaffolding/VariantGroup.svelte';
35
41
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
42
+
43
+ let previewCode = $state('npx @motion-proto/live-tokens setup-claude');
36
44
  </script>
37
45
 
38
46
  <ComponentEditorBase
39
47
  {component}
40
48
  title="Code Snippet"
41
- description="Single-line code display with a copy button. Shows a brief confirmation popover on copy."
49
+ description="Code display with a copy button. Long lines scroll horizontally; the copy button stays pinned top-right. Shows a brief confirmation popover on copy."
42
50
  tokens={allTokens}
43
51
  >
44
52
  <VariantGroup name="codesnippet" title="Code Snippet" {states} {component}>
45
53
  {#snippet children({ activeState })}
46
54
  <div class="codesnippet-preview">
55
+ <label class="preview-text">
56
+ <span>Preview text</span>
57
+ <textarea
58
+ bind:value={previewCode}
59
+ rows="2"
60
+ spellcheck="false"
61
+ placeholder="Paste a long command or block to preview scrolling"
62
+ ></textarea>
63
+ </label>
47
64
  <CodeSnippet
48
- code="npx @motion-proto/live-tokens setup-claude"
65
+ code={previewCode}
49
66
  class={activeState === 'hover' ? 'force-hover' : ''}
50
67
  />
51
68
  </div>
@@ -56,6 +73,35 @@
56
73
  <style>
57
74
  .codesnippet-preview {
58
75
  display: flex;
76
+ flex-direction: column;
77
+ gap: var(--ui-space-12);
59
78
  padding: var(--ui-space-16);
60
79
  }
80
+
81
+ /* Fixed width so pasted content overflows and demonstrates horizontal scroll. */
82
+ .codesnippet-preview :global(.codesnippet) {
83
+ width: 24rem;
84
+ max-width: 100%;
85
+ }
86
+
87
+ .preview-text {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: var(--ui-space-4);
91
+ font-size: var(--ui-font-size-sm);
92
+ color: var(--ui-text-secondary);
93
+ }
94
+
95
+ .preview-text textarea {
96
+ width: 100%;
97
+ padding: var(--ui-space-8);
98
+ background: var(--ui-surface-lowest);
99
+ color: var(--ui-text-primary);
100
+ border: 1px solid var(--ui-border-low);
101
+ border-radius: var(--ui-radius-sm);
102
+ font-family: var(--ui-font-mono);
103
+ font-size: var(--ui-font-size-sm);
104
+ resize: vertical;
105
+ }
106
+ .preview-text textarea::placeholder { color: var(--ui-text-muted); opacity: 1; }
61
107
  </style>
@@ -12,13 +12,13 @@
12
12
  const variantOptions = buttons.map((b) => ({ value: b, label: b === 'save' ? 'Save button' : 'Cancel button' }));
13
13
  function buttonStateTokens(btn: Button, state: 'default' | 'hover'): Token[] {
14
14
  return [
15
- { label: 'surface color', groupKey: 'surface', variable: `--inlineeditactions-${btn}-${state}-surface` },
16
- { label: 'text color', groupKey: 'text', variable: `--inlineeditactions-${btn}-${state}-text` },
17
- { label: 'border color', groupKey: 'border', variable: `--inlineeditactions-${btn}-${state}-border` },
18
- { label: 'border width', canBeLinked: true, groupKey: `${btn}-border-width`, variable: `--inlineeditactions-${btn}-${state}-border-width` },
19
- { label: 'corner radius', canBeLinked: true, groupKey: `${btn}-radius`, variable: `--inlineeditactions-${btn}-${state}-radius` },
20
- { label: 'padding', canBeLinked: true, groupKey: `${btn}-padding`, variable: `--inlineeditactions-${btn}-${state}-padding` },
21
- { label: 'icon size', canBeLinked: true, groupKey: `${btn}-icon-size`, variable: `--inlineeditactions-${btn}-${state}-icon-size` },
15
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: `--inlineeditactions-${btn}-${state}-surface` },
16
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: `--inlineeditactions-${btn}-${state}-border` },
17
+ { label: 'border width', element: 'frame', canBeLinked: true, groupKey: `${btn}-border-width`, variable: `--inlineeditactions-${btn}-${state}-border-width` },
18
+ { label: 'corner radius', element: 'frame', canBeLinked: true, groupKey: `${btn}-radius`, variable: `--inlineeditactions-${btn}-${state}-radius` },
19
+ { label: 'padding', element: 'frame', canBeLinked: true, groupKey: `${btn}-padding`, variable: `--inlineeditactions-${btn}-${state}-padding` },
20
+ { label: 'color', element: 'text', groupKey: 'text', variable: `--inlineeditactions-${btn}-${state}-text` },
21
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: `${btn}-icon-size`, variable: `--inlineeditactions-${btn}-${state}-icon-size` },
22
22
  ];
23
23
  }
24
24
 
@@ -15,24 +15,24 @@
15
15
  { label: 'message gap', groupKey: 'gap', variable: '--input-gap' },
16
16
  ],
17
17
  default: [
18
- { label: 'surface color', groupKey: 'surface', variable: '--input-default-surface' },
19
- { label: 'border color', groupKey: 'border', variable: '--input-default-border' },
20
- { label: 'placeholder color', groupKey: 'placeholder', variable: '--input-default-placeholder' },
21
- { label: 'icon color', groupKey: 'icon', variable: '--input-default-icon' },
22
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--input-default-icon-size' },
18
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--input-default-surface' },
19
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: '--input-default-border' },
20
+ { label: 'placeholder color', element: 'frame', groupKey: 'placeholder', variable: '--input-default-placeholder' },
21
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--input-default-icon' },
22
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--input-default-icon-size' },
23
23
  ],
24
24
  focused: [
25
- { label: 'surface color', groupKey: 'surface', variable: '--input-focused-surface' },
26
- { label: 'border color', groupKey: 'border', variable: '--input-focused-border' },
27
- { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: '--input-focused-border-width' },
28
- { label: 'icon color', groupKey: 'icon', variable: '--input-focused-icon' },
29
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--input-focused-icon-size' },
25
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--input-focused-surface' },
26
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: '--input-focused-border' },
27
+ { label: 'border width', element: 'frame', canBeLinked: true, groupKey: 'border-width', variable: '--input-focused-border-width' },
28
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--input-focused-icon' },
29
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--input-focused-icon-size' },
30
30
  ],
31
31
  disabled: [
32
- { label: 'surface color', groupKey: 'surface', variable: '--input-disabled-surface' },
33
- { label: 'border color', groupKey: 'border', variable: '--input-disabled-border' },
34
- { label: 'icon color', groupKey: 'icon', variable: '--input-disabled-icon' },
35
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--input-disabled-icon-size' },
32
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--input-disabled-surface' },
33
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: '--input-disabled-border' },
34
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--input-disabled-icon' },
35
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--input-disabled-icon-size' },
36
36
  ],
37
37
  label: [],
38
38
  hint: [],
@@ -46,7 +46,8 @@
46
46
  // Slot-prefixed groupKeys keep label/hint/error from collapsing into each other.
47
47
  const typeGroups: Record<string, TypeGroupConfig[]> = {
48
48
  default: [{
49
- legend: 'input text',
49
+ legend: '',
50
+ element: 'text',
50
51
  colorVariable: '--input-default-text',
51
52
  familyVariable: '--input-default-text-font-family',
52
53
  sizeVariable: '--input-default-text-font-size',
@@ -54,7 +55,8 @@
54
55
  lineHeightVariable: '--input-default-text-line-height',
55
56
  }],
56
57
  focused: [{
57
- legend: 'input text',
58
+ legend: '',
59
+ element: 'text',
58
60
  colorVariable: '--input-focused-text',
59
61
  familyVariable: '--input-focused-text-font-family',
60
62
  sizeVariable: '--input-focused-text-font-size',
@@ -62,7 +64,8 @@
62
64
  lineHeightVariable: '--input-focused-text-line-height',
63
65
  }],
64
66
  disabled: [{
65
- legend: 'input text',
67
+ legend: '',
68
+ element: 'text',
66
69
  colorVariable: '--input-disabled-text',
67
70
  familyVariable: '--input-disabled-text-font-family',
68
71
  sizeVariable: '--input-disabled-text-font-size',
@@ -7,43 +7,44 @@
7
7
  // Non-text tokens per state; text/font lives in typeGroups. Item-shape tokens sit under `menu` so they read as one decision across states.
8
8
  const states: Record<string, Token[]> = {
9
9
  menu: [
10
- { label: 'surface color', groupKey: 'surface', variable: '--menuselect-menu-surface' },
11
- { label: 'border color', groupKey: 'border', variable: '--menuselect-menu-border' },
12
- { label: 'border width', groupKey: 'width', variable: '--menuselect-menu-border-width' },
13
- { label: 'corner radius', groupKey: 'menu-radius', variable: '--menuselect-menu-radius' },
14
- { label: 'padding', variable: '--menuselect-menu-padding', groupKey: 'menu-padding' },
15
- { label: 'item gap', groupKey: 'gap', variable: '--menuselect-menu-gap' },
16
- { label: 'shadow', groupKey: 'shadow', variable: '--menuselect-menu-shadow' },
17
- { label: 'item radius', groupKey: 'item-radius', variable: '--menuselect-item-radius' },
18
- { label: 'item padding', variable: '--menuselect-item-padding', groupKey: 'item-padding' },
10
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--menuselect-menu-surface' },
11
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: '--menuselect-menu-border' },
12
+ { label: 'border width', element: 'frame', groupKey: 'width', variable: '--menuselect-menu-border-width' },
13
+ { label: 'corner radius', element: 'frame', groupKey: 'menu-radius', variable: '--menuselect-menu-radius' },
14
+ { label: 'padding', element: 'frame', variable: '--menuselect-menu-padding', groupKey: 'menu-padding' },
15
+ { label: 'item gap', element: 'frame', groupKey: 'gap', variable: '--menuselect-menu-gap' },
16
+ { label: 'shadow', element: 'frame', groupKey: 'shadow', variable: '--menuselect-menu-shadow' },
17
+ { label: 'radius', element: 'item', groupKey: 'item-radius', variable: '--menuselect-item-radius' },
18
+ { label: 'padding', element: 'item', variable: '--menuselect-item-padding', groupKey: 'item-padding' },
19
19
  ],
20
20
  'default item': [
21
- { label: 'surface color', groupKey: 'surface', variable: '--menuselect-default-surface' },
22
- { label: 'icon color', groupKey: 'icon', variable: '--menuselect-default-icon' },
23
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-default-icon-size' },
21
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--menuselect-default-surface' },
22
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--menuselect-default-icon' },
23
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-default-icon-size' },
24
24
  ],
25
25
  'hover item': [
26
- { label: 'surface color', groupKey: 'surface', variable: '--menuselect-hover-surface' },
27
- { label: 'icon color', groupKey: 'icon', variable: '--menuselect-hover-icon' },
28
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-hover-icon-size' },
26
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--menuselect-hover-surface' },
27
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--menuselect-hover-icon' },
28
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-hover-icon-size' },
29
29
  ],
30
30
  'selected item': [
31
- { label: 'surface color', groupKey: 'surface', variable: '--menuselect-selected-surface' },
32
- { label: 'icon color', groupKey: 'icon', variable: '--menuselect-selected-icon' },
33
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-selected-icon-size' },
34
- { label: 'indicator color', groupKey: 'indicator', variable: '--menuselect-selected-indicator' },
31
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--menuselect-selected-surface' },
32
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--menuselect-selected-icon' },
33
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-selected-icon-size' },
34
+ { label: 'color', element: 'indicator', groupKey: 'indicator', variable: '--menuselect-selected-indicator' },
35
35
  ],
36
36
  'disabled item': [
37
- { label: 'surface color', groupKey: 'surface', variable: '--menuselect-disabled-surface' },
38
- { label: 'icon color', groupKey: 'icon', variable: '--menuselect-disabled-icon' },
39
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-disabled-icon-size' },
37
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--menuselect-disabled-surface' },
38
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--menuselect-disabled-icon' },
39
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: '--menuselect-disabled-icon-size' },
40
40
  ],
41
41
  };
42
42
 
43
43
  // Per-state item-label typography; linkable groupKeys let users collapse or diverge across states.
44
44
  const typeGroups: Record<string, TypeGroupConfig[]> = {
45
45
  'default item': [{
46
- legend: 'item label',
46
+ legend: '',
47
+ element: 'text',
47
48
  colorVariable: '--menuselect-default-text',
48
49
  familyVariable: '--menuselect-default-text-font-family',
49
50
  sizeVariable: '--menuselect-default-text-font-size',
@@ -51,7 +52,8 @@
51
52
  lineHeightVariable: '--menuselect-default-text-line-height',
52
53
  }],
53
54
  'hover item': [{
54
- legend: 'item label',
55
+ legend: '',
56
+ element: 'text',
55
57
  colorVariable: '--menuselect-hover-text',
56
58
  familyVariable: '--menuselect-hover-text-font-family',
57
59
  sizeVariable: '--menuselect-hover-text-font-size',
@@ -59,7 +61,8 @@
59
61
  lineHeightVariable: '--menuselect-hover-text-line-height',
60
62
  }],
61
63
  'selected item': [{
62
- legend: 'item label',
64
+ legend: '',
65
+ element: 'text',
63
66
  colorVariable: '--menuselect-selected-text',
64
67
  familyVariable: '--menuselect-selected-text-font-family',
65
68
  sizeVariable: '--menuselect-selected-text-font-size',
@@ -67,7 +70,8 @@
67
70
  lineHeightVariable: '--menuselect-selected-text-line-height',
68
71
  }],
69
72
  'disabled item': [{
70
- legend: 'item label',
73
+ legend: '',
74
+ element: 'text',
71
75
  colorVariable: '--menuselect-disabled-text',
72
76
  familyVariable: '--menuselect-disabled-text-font-family',
73
77
  sizeVariable: '--menuselect-disabled-text-font-size',
@@ -8,20 +8,21 @@
8
8
  // not a per-variant token namespace.
9
9
  const states: Record<string, Token[]> = {
10
10
  default: [
11
- { label: 'fill color', groupKey: 'fill', variable: '--progressbar-fill' },
12
- { label: 'track surface color', groupKey: 'surface', variable: '--progressbar-track-surface' },
13
- { label: 'track border color', groupKey: 'border', variable: '--progressbar-track-border' },
14
- { label: 'track border width', canBeLinked: true, groupKey: 'track-border-width', variable: '--progressbar-track-border-width' },
15
- { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: '--progressbar-radius' },
16
- { label: 'track height', canBeLinked: true, groupKey: 'track-height', variable: '--progressbar-track-height' },
17
- { label: 'label gap', groupKey: 'label-gap', variable: '--progressbar-label-gap' },
11
+ { label: 'color', element: 'fill', groupKey: 'fill', variable: '--progressbar-fill' },
12
+ { label: 'surface', element: 'frame', groupKey: 'surface', variable: '--progressbar-track-surface' },
13
+ { label: 'border', element: 'frame', groupKey: 'border', variable: '--progressbar-track-border' },
14
+ { label: 'border width', element: 'frame', canBeLinked: true, groupKey: 'track-border-width', variable: '--progressbar-track-border-width' },
15
+ { label: 'corner radius', element: 'frame', canBeLinked: true, groupKey: 'radius', variable: '--progressbar-radius' },
16
+ { label: 'height', element: 'frame', canBeLinked: true, groupKey: 'track-height', variable: '--progressbar-track-height' },
17
+ { label: 'label gap', element: 'frame', groupKey: 'label-gap', variable: '--progressbar-label-gap' },
18
18
  ],
19
19
  };
20
20
 
21
21
  const typeGroups: Record<string, TypeGroupConfig[]> = {
22
22
  default: [
23
23
  {
24
- legend: 'label',
24
+ legend: '',
25
+ element: 'label',
25
26
  colorVariable: '--progressbar-label',
26
27
  familyVariable: '--progressbar-label-font-family',
27
28
  sizeVariable: '--progressbar-label-font-size',
@@ -29,7 +30,8 @@
29
30
  lineHeightVariable: '--progressbar-label-line-height',
30
31
  },
31
32
  {
32
- legend: 'value',
33
+ legend: '',
34
+ element: 'value',
33
35
  colorVariable: '--progressbar-value',
34
36
  familyVariable: '--progressbar-value-font-family',
35
37
  sizeVariable: '--progressbar-value-font-size',
@@ -1,6 +1,6 @@
1
1
  <script module lang="ts">
2
2
  import { buildSiblings } from './scaffolding/siblings';
3
- import type { Token, TypeGroupConfig } from './scaffolding/types';
3
+ import type { Token, TypeGroupConfig, IntrinsicSpec } from './scaffolding/types';
4
4
 
5
5
  export const component = 'sectiondivider';
6
6
 
@@ -149,6 +149,57 @@
149
149
  | 'above-description'
150
150
  | 'through-description'
151
151
  | 'below-description';
152
+
153
+ // Structural/display properties driven by the bespoke selects + element
154
+ // toggles below, not the token grid. Each `default` mirrors the runtime
155
+ // SectionDivider's `:global(:root)`; the read-back getters fall back to it
156
+ // when a variant is unedited, and intrinsicsContract.test pins the two
157
+ // copies together. Defaults are per-variant: lg ships eyebrow + description
158
+ // + a description-anchored hairline; md/sm ship the title alone.
159
+ const HAIRLINE_POSITIONS = [
160
+ 'above-label', 'through-label', 'below-label', 'through-description', 'below-description',
161
+ ] as const;
162
+ export const intrinsics: IntrinsicSpec[] = [
163
+ {
164
+ key: 'align',
165
+ variants: ['lg', 'md', 'sm'],
166
+ variable: (v) => `--sectiondivider-${v}-align`,
167
+ values: ['start', 'center'],
168
+ default: { lg: 'start', md: 'start', sm: 'start' },
169
+ },
170
+ {
171
+ key: 'eyebrow-display',
172
+ variants: ['lg', 'md', 'sm'],
173
+ variable: (v) => `--sectiondivider-${v}-eyebrow-display`,
174
+ values: ['block', 'none'],
175
+ default: { lg: 'block', md: 'none', sm: 'none' },
176
+ },
177
+ {
178
+ key: 'description-display',
179
+ variants: ['lg', 'md', 'sm'],
180
+ variable: (v) => `--sectiondivider-${v}-description-display`,
181
+ values: ['flex', 'none'],
182
+ default: { lg: 'flex', md: 'none', sm: 'none' },
183
+ },
184
+ {
185
+ key: 'eyebrow-text-transform',
186
+ variants: ['lg', 'md', 'sm'],
187
+ variable: (v) => `--sectiondivider-${v}-eyebrow-text-transform`,
188
+ values: ['uppercase', 'none'],
189
+ default: { lg: 'none', md: 'none', sm: 'none' },
190
+ },
191
+ {
192
+ key: 'hairline',
193
+ variants: ['lg', 'md', 'sm'],
194
+ variable: (v) => `--sectiondivider-${v}-hairline`,
195
+ values: ['none', ...HAIRLINE_POSITIONS],
196
+ default: { lg: 'below-description', md: 'below-label', sm: 'below-label' },
197
+ // 'above-description' renders identically to 'below-label'; the position
198
+ // dropdown omits it, so coerce on read to keep the control's value valid.
199
+ normalize: (raw) => (raw === 'above-description' ? 'below-label' : raw),
200
+ },
201
+ ];
202
+ const INTRINSIC_BY_KEY = new Map(intrinsics.map((i) => [i.key, i]));
152
203
  </script>
153
204
 
154
205
  <script lang="ts">
@@ -191,8 +242,19 @@
191
242
  return ref.value;
192
243
  }
193
244
 
245
+ // Resolved raw intrinsic value: the stored override, else the spec's
246
+ // per-variant default (which mirrors the runtime :root). Normalized so the
247
+ // getters below branch on a canonical value. Falling back to the spec
248
+ // default — not a hard-coded constant — is what keeps each control's
249
+ // displayed default in step with what the unedited divider actually renders.
250
+ function readIntrinsic(key: string, v: Variant): string {
251
+ const spec = INTRINSIC_BY_KEY.get(key)!;
252
+ const raw = readLiteral(spec.variable(v)) ?? spec.default[v];
253
+ return spec.normalize ? spec.normalize(raw) : raw;
254
+ }
255
+
194
256
  function getAlign(v: Variant): Align {
195
- return readLiteral(`--sectiondivider-${v}-align`) === 'start' ? 'start' : 'center';
257
+ return readIntrinsic('align', v) === 'center' ? 'center' : 'start';
196
258
  }
197
259
  function getColorFamily(v: Variant): string {
198
260
  const raw = cfg[`--sectiondivider-${v}-color-family`];
@@ -201,13 +263,9 @@
201
263
  }
202
264
  /** Active hairline position OR `'none'` (= hidden). */
203
265
  function getHairlineValue(v: Variant): HairlinePosition | 'none' {
204
- const raw = readLiteral(`--sectiondivider-${v}-hairline`);
205
- if (raw === undefined || raw === 'none') return 'none';
206
- // 'above-description' renders identically to 'below-label' coerce on read
207
- // so the dropdown's option list (which omits 'above-description') stays valid.
208
- if (raw === 'above-description') return 'below-label';
209
- const positions: HairlinePosition[] = ['above-label', 'through-label', 'below-label', 'through-description', 'below-description'];
210
- return (positions as string[]).includes(raw) ? (raw as HairlinePosition) : 'none';
266
+ const raw = readIntrinsic('hairline', v);
267
+ if (raw === 'none') return 'none';
268
+ return (HAIRLINE_POSITIONS as readonly string[]).includes(raw) ? (raw as HairlinePosition) : 'none';
211
269
  }
212
270
  function getShowHairline(v: Variant): boolean {
213
271
  return getHairlineValue(v) !== 'none';
@@ -220,17 +278,13 @@
220
278
  return val === 'none' ? 'above-label' : val;
221
279
  }
222
280
  function getShowEyebrow(v: Variant): boolean {
223
- return readLiteral(`--sectiondivider-${v}-eyebrow-display`) === 'block';
281
+ return readIntrinsic('eyebrow-display', v) === 'block';
224
282
  }
225
283
  function getEyebrowUppercase(v: Variant): boolean {
226
- return readLiteral(`--sectiondivider-${v}-eyebrow-text-transform`) === 'uppercase';
284
+ return readIntrinsic('eyebrow-text-transform', v) === 'uppercase';
227
285
  }
228
286
  function getShowDescription(v: Variant): boolean {
229
- const raw = readLiteral(`--sectiondivider-${v}-description-display`);
230
- // Default: shown (flex) when unset. Matches the :root default and the
231
- // legacy `getShowDescription` semantics for files that never set the key.
232
- if (raw === undefined) return true;
233
- return raw === 'flex';
287
+ return readIntrinsic('description-display', v) !== 'none';
234
288
  }
235
289
  /** Write an intrinsic to the aliases bucket as a literal so it cascades
236
290
  * through cssVarSync to `:root` on both the editor iframe and host page. */
@@ -391,8 +445,8 @@
391
445
  value={getAlign(v.key)}
392
446
  onchange={(e) => setIntrinsic(v.key, 'align', (e.currentTarget as HTMLSelectElement).value)}
393
447
  >
394
- <option value="center">Center</option>
395
448
  <option value="start">Start</option>
449
+ <option value="center">Center</option>
396
450
  </select>
397
451
  </div>
398
452
  <div class="property-row sd-intrinsic-row">
@@ -10,15 +10,15 @@
10
10
  // they don't vary per state in the runtime.
11
11
  const defaultStates: Record<string, Token[]> = {
12
12
  'control bar': [
13
- { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-bar-surface' },
14
- { label: 'border color', groupKey: 'border', variable: '--segmentedcontrol-bar-border' },
15
- { label: 'border width', groupKey: 'width', variable: '--segmentedcontrol-bar-border-width' },
16
- { label: 'divider color', groupKey: 'color', variable: '--segmentedcontrol-divider-color' },
17
- { label: 'divider width', groupKey: 'thickness', variable: '--segmentedcontrol-divider-thickness' },
18
- { label: 'divider inset', groupKey: 'divider-inset', variable: '--segmentedcontrol-divider-inset' },
19
- { label: 'corner radius', groupKey: 'radius', variable: '--segmentedcontrol-bar-radius' },
20
- { label: 'option gap', groupKey: 'gap', variable: '--segmentedcontrol-bar-gap' },
21
- { label: 'padding', variable: '--segmentedcontrol-bar-padding', groupKey: 'bar-padding' },
13
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--segmentedcontrol-bar-surface' },
14
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: '--segmentedcontrol-bar-border' },
15
+ { label: 'border width', element: 'frame', groupKey: 'width', variable: '--segmentedcontrol-bar-border-width' },
16
+ { label: 'color', element: 'divider', groupKey: 'color', variable: '--segmentedcontrol-divider-color' },
17
+ { label: 'width', element: 'divider', groupKey: 'thickness', variable: '--segmentedcontrol-divider-thickness' },
18
+ { label: 'inset', element: 'divider', groupKey: 'divider-inset', variable: '--segmentedcontrol-divider-inset' },
19
+ { label: 'corner radius', element: 'frame', groupKey: 'radius', variable: '--segmentedcontrol-bar-radius' },
20
+ { label: 'option gap', element: 'frame', groupKey: 'gap', variable: '--segmentedcontrol-bar-gap' },
21
+ { label: 'padding', element: 'frame', variable: '--segmentedcontrol-bar-padding', groupKey: 'bar-padding' },
22
22
  ],
23
23
  'option base': [
24
24
  { label: 'padding', variable: '--segmentedcontrol-option-padding', groupKey: 'option-padding' },
@@ -29,11 +29,11 @@
29
29
  { label: 'icon color', groupKey: 'icon', variable: '--segmentedcontrol-option-icon' },
30
30
  ],
31
31
  'selected option': [
32
- { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-selected-surface' },
33
- { label: 'icon color', groupKey: 'icon', variable: '--segmentedcontrol-selected-icon' },
34
- { label: 'border color', groupKey: 'border', variable: '--segmentedcontrol-selected-border' },
35
- { label: 'border width', groupKey: 'width', variable: '--segmentedcontrol-selected-border-width' },
36
- { label: 'corner radius', groupKey: 'radius', variable: '--segmentedcontrol-selected-radius' },
32
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: '--segmentedcontrol-selected-surface' },
33
+ { label: 'color', element: 'icon', groupKey: 'icon', variable: '--segmentedcontrol-selected-icon' },
34
+ { label: 'border color', element: 'frame', groupKey: 'border', variable: '--segmentedcontrol-selected-border' },
35
+ { label: 'border width', element: 'frame', groupKey: 'width', variable: '--segmentedcontrol-selected-border-width' },
36
+ { label: 'corner radius', element: 'frame', groupKey: 'radius', variable: '--segmentedcontrol-selected-radius' },
37
37
  ],
38
38
  'hover option': [
39
39
  { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-option-hover-surface' },
@@ -52,10 +52,10 @@
52
52
  // since family/weight/color don't differ.
53
53
  const smallStates: Record<string, Token[]> = {
54
54
  'control bar': [
55
- { label: 'divider inset', groupKey: 'small-divider-inset', variable: '--segmentedcontrol-small-divider-inset' },
56
- { label: 'divider width', groupKey: 'small-divider-thickness', variable: '--segmentedcontrol-small-divider-thickness' },
57
- { label: 'corner radius', groupKey: 'small-radius', variable: '--segmentedcontrol-bar-small-radius' },
58
- { label: 'padding', variable: '--segmentedcontrol-bar-small-padding', groupKey: 'bar-small-padding' },
55
+ { label: 'corner radius', element: 'frame', groupKey: 'small-radius', variable: '--segmentedcontrol-bar-small-radius' },
56
+ { label: 'padding', element: 'frame', variable: '--segmentedcontrol-bar-small-padding', groupKey: 'bar-small-padding' },
57
+ { label: 'inset', element: 'divider', groupKey: 'small-divider-inset', variable: '--segmentedcontrol-small-divider-inset' },
58
+ { label: 'width', element: 'divider', groupKey: 'small-divider-thickness', variable: '--segmentedcontrol-small-divider-thickness' },
59
59
  ],
60
60
  'option base': [
61
61
  { label: 'icon size', groupKey: 'small-icon-size', variable: '--segmentedcontrol-option-small-icon-size' },
@@ -80,7 +80,8 @@
80
80
  lineHeightVariable: '--segmentedcontrol-option-text-line-height',
81
81
  }],
82
82
  'selected option': [{
83
- legend: 'option text',
83
+ legend: '',
84
+ element: 'text',
84
85
  colorVariable: '--segmentedcontrol-selected-text',
85
86
  familyVariable: '--segmentedcontrol-selected-text-font-family',
86
87
  sizeVariable: '--segmentedcontrol-selected-text-font-size',
@@ -7,21 +7,25 @@
7
7
  // The tab object — four states (default/hover/active/disabled) of the same tab button.
8
8
  const tabStateNames = ['default', 'hover', 'active', 'disabled'] as const;
9
9
  type TabState = typeof tabStateNames[number];
10
+ // `element` tags split each tab state into frame / icon / indicator / text.
11
+ // No elementOrder: it would force the flat `bar` state into grouped mode.
12
+ // Frame tokens lead so sections read frame → icon → indicator → text (typeGroup last).
10
13
  function tabStateTokens(s: TabState): Token[] {
11
14
  return [
12
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: `--tabbar-${s}-icon-size` },
13
- { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--tabbar-${s}-padding` },
14
- { label: 'surface color', groupKey: 'surface', variable: `--tabbar-${s}-surface` },
15
- { label: 'top radius', canBeLinked: true, groupKey: 'tab-top-radius', variable: `--tabbar-${s}-tab-top-radius` },
16
- { label: 'bottom radius', canBeLinked: true, groupKey: 'tab-bottom-radius', variable: `--tabbar-${s}-tab-bottom-radius` },
17
- { label: 'border color', canBeLinked: true, groupKey: 'tab-border-color', variable: `--tabbar-${s}-tab-border-color` },
18
- { label: 'border width', canBeLinked: true, groupKey: 'tab-border-width', variable: `--tabbar-${s}-tab-border-width` },
19
- { label: 'indicator width', canBeLinked: true, groupKey: 'indicator-border-width', variable: `--tabbar-${s}-indicator-border-width` },
15
+ { label: 'surface color', element: 'frame', groupKey: 'surface', variable: `--tabbar-${s}-surface` },
16
+ { label: 'top radius', element: 'frame', canBeLinked: true, groupKey: 'tab-top-radius', variable: `--tabbar-${s}-tab-top-radius` },
17
+ { label: 'bottom radius', element: 'frame', canBeLinked: true, groupKey: 'tab-bottom-radius', variable: `--tabbar-${s}-tab-bottom-radius` },
18
+ { label: 'border color', element: 'frame', canBeLinked: true, groupKey: 'tab-border-color', variable: `--tabbar-${s}-tab-border-color` },
19
+ { label: 'border width', element: 'frame', canBeLinked: true, groupKey: 'tab-border-width', variable: `--tabbar-${s}-tab-border-width` },
20
+ { label: 'padding', element: 'frame', canBeLinked: true, groupKey: 'padding', variable: `--tabbar-${s}-padding` },
21
+ { label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: `--tabbar-${s}-icon-size` },
22
+ { label: 'width', element: 'indicator', canBeLinked: true, groupKey: 'indicator-border-width', variable: `--tabbar-${s}-indicator-border-width` },
20
23
  ];
21
24
  }
22
25
  function tabStateTypeGroups(s: TabState): TypeGroupConfig[] {
23
26
  return [{
24
- legend: 'tab text',
27
+ legend: '',
28
+ element: 'text',
25
29
  colorVariable: `--tabbar-${s}-text`,
26
30
  familyVariable: `--tabbar-${s}-text-font-family`,
27
31
  sizeVariable: `--tabbar-${s}-text-font-size`,
@@ -48,7 +52,7 @@
48
52
  };
49
53
  states['active tab'] = [
50
54
  ...states['active tab'],
51
- { label: 'indicator color', groupKey: 'indicator-color', variable: '--tabbar-active-border' },
55
+ { label: 'color', element: 'indicator', groupKey: 'indicator-color', variable: '--tabbar-active-border' },
52
56
  ];
53
57
  const typeGroups: Record<string, TypeGroupConfig[]> = Object.fromEntries(
54
58
  tabStateNames.map((s) => [`${s} tab`, tabStateTypeGroups(s)]),
@@ -8,29 +8,29 @@
8
8
  // on top of that baseline.
9
9
  const states: Record<string, Token[]> = {
10
10
  default: [
11
- { label: 'thumb size', variable: '--toggle-thumb-size' },
12
- { label: 'track padding', variable: '--toggle-track-padding' },
13
- { label: 'track radius', variable: '--toggle-track-radius' },
14
- { label: 'track border width',variable: '--toggle-track-border-width' },
15
- { label: 'track border', variable: '--toggle-track-border' },
16
- { label: 'track surface', variable: '--toggle-track-surface' },
17
- { label: 'thumb border', variable: '--toggle-thumb-border' },
18
- { label: 'thumb surface', variable: '--toggle-thumb-surface' },
19
- { label: 'label text', variable: '--toggle-label-text' },
20
- { label: 'label font family', variable: '--toggle-label-font-family' },
21
- { label: 'label font size', variable: '--toggle-label-font-size' },
22
- { label: 'label font weight', variable: '--toggle-label-font-weight' },
23
- { label: 'label gap', variable: '--toggle-gap' },
11
+ { label: 'surface', element: 'track', variable: '--toggle-track-surface' },
12
+ { label: 'border', element: 'track', variable: '--toggle-track-border' },
13
+ { label: 'border width', element: 'track', variable: '--toggle-track-border-width' },
14
+ { label: 'radius', element: 'track', variable: '--toggle-track-radius' },
15
+ { label: 'padding', element: 'track', variable: '--toggle-track-padding' },
16
+ { label: 'surface', element: 'thumb', variable: '--toggle-thumb-surface' },
17
+ { label: 'border', element: 'thumb', variable: '--toggle-thumb-border' },
18
+ { label: 'size', element: 'thumb', variable: '--toggle-thumb-size' },
19
+ { label: 'text', element: 'label', variable: '--toggle-label-text' },
20
+ { label: 'font family', element: 'label', variable: '--toggle-label-font-family' },
21
+ { label: 'font size', element: 'label', variable: '--toggle-label-font-size' },
22
+ { label: 'font weight', element: 'label', variable: '--toggle-label-font-weight' },
23
+ { label: 'gap', element: 'label', variable: '--toggle-gap' },
24
24
  ],
25
25
  hover: [
26
26
  { label: 'track surface', variable: '--toggle-hover-track-surface' },
27
27
  { label: 'thumb surface', variable: '--toggle-hover-thumb-surface' },
28
28
  ],
29
29
  on: [
30
- { label: 'track surface', variable: '--toggle-on-track-surface' },
31
- { label: 'track border', variable: '--toggle-on-track-border' },
32
- { label: 'thumb surface', variable: '--toggle-on-thumb-surface' },
33
- { label: 'thumb border', variable: '--toggle-on-thumb-border' },
30
+ { label: 'surface', element: 'track', variable: '--toggle-on-track-surface' },
31
+ { label: 'border', element: 'track', variable: '--toggle-on-track-border' },
32
+ { label: 'surface', element: 'thumb', variable: '--toggle-on-thumb-surface' },
33
+ { label: 'border', element: 'thumb', variable: '--toggle-on-thumb-border' },
34
34
  ],
35
35
  'on hover': [
36
36
  { label: 'track surface', variable: '--toggle-on-hover-track-surface' },
@@ -4,21 +4,22 @@
4
4
 
5
5
  export const component = 'tooltip';
6
6
 
7
- // Tooltip is a single object surface/border/padding/radius/shadow live together.
7
+ // Element tags split the panel into frame + text via StateBlock.
8
8
  const states: Record<string, Token[]> = {
9
9
  tooltip: [
10
- { label: 'surface color', variable: '--tooltip-surface' },
11
- { label: 'border color', variable: '--tooltip-border' },
12
- { label: 'border width', groupKey: 'width', variable: '--tooltip-border-width' },
13
- { label: 'corner radius', variable: '--tooltip-radius' },
14
- { label: 'padding', variable: '--tooltip-padding' },
15
- { label: 'tooltip shadow', variable: '--tooltip-shadow' },
10
+ { label: 'surface color', element: 'frame', variable: '--tooltip-surface' },
11
+ { label: 'border color', element: 'frame', variable: '--tooltip-border' },
12
+ { label: 'border width', element: 'frame', groupKey: 'width', variable: '--tooltip-border-width' },
13
+ { label: 'corner radius', element: 'frame', variable: '--tooltip-radius' },
14
+ { label: 'padding', element: 'frame', variable: '--tooltip-padding' },
15
+ { label: 'shadow', element: 'frame', variable: '--tooltip-shadow' },
16
16
  ],
17
17
  };
18
18
 
19
19
  const typeGroups: Record<string, TypeGroupConfig[]> = {
20
20
  tooltip: [{
21
- legend: 'tooltip text',
21
+ legend: '',
22
+ element: 'text',
22
23
  colorVariable: '--tooltip-text',
23
24
  familyVariable: '--tooltip-text-font-family',
24
25
  sizeVariable: '--tooltip-text-font-size',
@@ -18,3 +18,7 @@ export { buildTypeGroupTokens } from './scaffolding/buildTypeGroupTokens';
18
18
 
19
19
  // Token schema type — the shape of an entry in an editor's `allTokens` array.
20
20
  export type { Token } from './scaffolding/types';
21
+
22
+ // Intrinsic spec — structural/display props an editor drives outside the token
23
+ // grid (alignment, visibility). Pass an array as `registerComponent({ intrinsics })`.
24
+ export type { IntrinsicSpec } from './scaffolding/types';
@@ -1,5 +1,5 @@
1
1
  import type { Component } from 'svelte';
2
- import type { Token } from './scaffolding/types';
2
+ import type { Token, IntrinsicSpec } from './scaffolding/types';
3
3
  import { registerComponentSchema } from '../core/store/editorStore';
4
4
 
5
5
  import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
@@ -18,7 +18,7 @@ import MenuSelectEditor, { allTokens as menuSelectTokens } from './MenuSelectEdi
18
18
  import NotificationEditor, { allTokens as notificationTokens } from './NotificationEditor.svelte';
19
19
  import ProgressBarEditor, { allTokens as progressBarTokens } from './ProgressBarEditor.svelte';
20
20
  import RadioButtonEditor, { allTokens as radioButtonTokens } from './RadioButtonEditor.svelte';
21
- import SectionDividerEditor, { allTokens as sectionDividerTokens } from './SectionDividerEditor.svelte';
21
+ import SectionDividerEditor, { allTokens as sectionDividerTokens, intrinsics as sectionDividerIntrinsics } from './SectionDividerEditor.svelte';
22
22
  import SegmentedControlEditor, { allTokens as segmentedControlTokens } from './SegmentedControlEditor.svelte';
23
23
  import SideNavigationEditor, { allTokens as sideNavigationTokens } from './SideNavigationEditor.svelte';
24
24
  import TableEditor, { allTokens as tableTokens } from './TableEditor.svelte';
@@ -72,6 +72,10 @@ export interface RegistryEntry {
72
72
  editorComponent: Component<any, any, any>;
73
73
  /** Flat token list — the editor's declarative description of its token surface. */
74
74
  schema: Token[];
75
+ /** Structural/display properties controlled outside the token grid. Optional —
76
+ most components have none. When present, each spec's per-variant default is
77
+ pinned to the runtime `:global(:root)` by intrinsicsContract.test. */
78
+ intrinsics?: IntrinsicSpec[];
75
79
  /** `'system'` for first-party entries; `'custom'` for entries added via `registerComponent()`. */
76
80
  origin: 'system' | 'custom';
77
81
  }
@@ -226,6 +230,7 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
226
230
  sourceFile: 'src/system/components/SectionDivider.svelte',
227
231
  editorComponent: SectionDividerEditor,
228
232
  schema: sectionDividerTokens,
233
+ intrinsics: sectionDividerIntrinsics,
229
234
  origin: 'system',
230
235
  },
231
236
  collapsiblesection: {
@@ -36,6 +36,31 @@ export type Token = {
36
36
  family?: string;
37
37
  };
38
38
 
39
+ /** An intrinsic: a structural/display property (alignment, hairline position,
40
+ element visibility) driven by a bespoke editor control rather than the
41
+ generic token grid. Unlike a Token it carries the default twice — once in
42
+ the runtime component's `:global(:root)` and once in the editor's read-back
43
+ (`default` below, which the getters fall back to when unset). Those two
44
+ copies must agree or the control displays a state the page doesn't render
45
+ (the SectionDivider align bug). The intrinsics contract test pins them. */
46
+ export type IntrinsicSpec = {
47
+ /** Stable id, used in diagnostics and to key getters off the spec. */
48
+ key: string;
49
+ /** Variant keys this intrinsic spans (e.g. ['lg','md','sm']). */
50
+ variants: string[];
51
+ /** Per-variant CSS custom property. */
52
+ variable: (variant: string) => string;
53
+ /** Allowed raw values; the runtime `:root` default must be one of these. */
54
+ values: string[];
55
+ /** Editor's unset-default raw value per variant — the read-back getters
56
+ source their default from here. Pinned to the runtime `:global(:root)`
57
+ default by the intrinsics contract test. */
58
+ default: Record<string, string>;
59
+ /** Folds render-equivalent raw values to one canonical form before
60
+ comparison (e.g. SectionDivider's 'above-description' ≡ 'below-label'). */
61
+ normalize?: (raw: string) => string;
62
+ };
63
+
39
64
  /** Editor type-group: a fieldset containing a coordinated set of typography tokens
40
65
  (text color + font-family/size/weight/line-height) for a piece of content
41
66
  (e.g. a card title, notification body). Optional outline rows let
@@ -1,6 +1,6 @@
1
1
  <!--
2
- CodeSnippet.svelte — single-line code display with a copy button. Click the
3
- button to copy `code` to the clipboard; a Tooltip-rendered confirmation
2
+ CodeSnippet.svelte — code display with a copy button pinned top-right. Click
3
+ the button to copy `code` to the clipboard; a Tooltip-rendered confirmation
4
4
  ("Copied") flashes briefly above the button.
5
5
  -->
6
6
  <script lang="ts">
@@ -71,11 +71,15 @@
71
71
  --codesnippet-icon: var(--text-secondary);
72
72
  --codesnippet-icon-size: var(--font-size-md);
73
73
  --codesnippet-hover-icon: var(--text-primary);
74
+
75
+ /* Horizontal-scroll scrollbar. */
76
+ --codesnippet-scrollbar-border-width: var(--border-width-6);
77
+ --codesnippet-scrollbar-thumb: var(--text-tertiary);
74
78
  }
75
79
 
76
80
  .codesnippet {
77
81
  display: inline-flex;
78
- align-items: center;
82
+ align-items: flex-start;
79
83
  gap: var(--codesnippet-gap);
80
84
  max-width: 100%;
81
85
  padding: var(--codesnippet-padding);
@@ -93,8 +97,22 @@
93
97
  font-weight: var(--codesnippet-code-font-weight);
94
98
  line-height: var(--codesnippet-code-line-height);
95
99
  white-space: pre;
96
- overflow: hidden;
97
- text-overflow: ellipsis;
100
+ overflow-x: auto;
101
+ scrollbar-width: thin;
102
+ scrollbar-color: var(--codesnippet-scrollbar-thumb) transparent;
103
+ }
104
+
105
+ .code::-webkit-scrollbar {
106
+ height: var(--codesnippet-scrollbar-border-width);
107
+ }
108
+
109
+ .code::-webkit-scrollbar-track {
110
+ background: transparent;
111
+ }
112
+
113
+ .code::-webkit-scrollbar-thumb {
114
+ background: var(--codesnippet-scrollbar-thumb);
115
+ border-radius: var(--radius-full);
98
116
  }
99
117
 
100
118
  .copy {