@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
@@ -16,20 +16,69 @@ export const TYPE_FONT_PROPS = [
16
16
 
17
17
  export type TypeFontProp = typeof TYPE_FONT_PROPS[number];
18
18
 
19
- export type BuildTypeGroupTokensOptions = {
20
- /** Override the default groupKey per property. Receives the prop descriptor and the
19
+ /** Declarative inputs that let the helpers derive a slot-scoped groupKey from a
20
+ variable name instead of guessing from its last dash. Pass `component` (the id,
21
+ so the `--<id>-` prefix is stripped) and `variants` (the variant/state segment
22
+ strings exactly as they appear in variable names, e.g. `['default','hover']`).
23
+ The derived key is everything left after removing the prefix and those segments,
24
+ so two parts that share a suffix (`section-text`, `item-text`) stay distinct
25
+ while the same slot across variants collapses to one key. */
26
+ export type GroupKeyDerivation = {
27
+ component?: string;
28
+ variants?: readonly string[];
29
+ };
30
+
31
+ export type BuildTypeGroupTokensOptions = GroupKeyDerivation & {
32
+ /** Override the derived groupKey per property. Receives the prop descriptor and the
21
33
  type-group config it came from. Return a stable string — siblings sharing the same
22
- groupKey are linked in the linked block. */
34
+ groupKey are linked in the linked block. Wins over structural derivation. */
23
35
  groupKeyFor?: (prop: TypeFontProp, group: TypeGroupConfig) => string;
24
36
  };
25
37
 
38
+ /** Color-token derivation: either the legacy single-arg callback form
39
+ `(group) => string`, or the declarative options object. */
40
+ export type ColorGroupKeyOption =
41
+ | ((group: TypeGroupConfig) => string)
42
+ | (GroupKeyDerivation & { groupKeyFor?: (group: TypeGroupConfig) => string });
43
+
44
+ /** Remove the first contiguous run matching each `segment` (a segment may itself
45
+ be multi-part, e.g. `'on-hover'`) from `segs`. */
46
+ function stripSegments(segs: string[], remove: readonly string[]): string[] {
47
+ let out = segs;
48
+ for (const r of remove) {
49
+ const rs = r.split('-');
50
+ for (let i = 0; i + rs.length <= out.length; i++) {
51
+ if (rs.every((s, k) => out[i + k] === s)) {
52
+ out = [...out.slice(0, i), ...out.slice(i + rs.length)];
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /** Slot-scoped key = the variable minus the `--<component>-` prefix and any
61
+ variant/state segments. Falls back to the whole variable if nothing remains. */
62
+ export function structuralGroupKey(
63
+ variable: string,
64
+ { component, variants }: GroupKeyDerivation,
65
+ ): string {
66
+ let body = variable.startsWith('--') ? variable.slice(2) : variable;
67
+ if (component && body.startsWith(component + '-')) body = body.slice(component.length + 1);
68
+ const segs = stripSegments(body.split('-'), variants ?? []);
69
+ return segs.join('-') || variable;
70
+ }
71
+
26
72
  /** Derive the Token[] schema entries for every TypeGroupConfig in `typeGroups`. Each
27
73
  group emits one `colorVariable` token plus up to 4 font-shape tokens (family/size/
28
74
  weight/line-height) for whichever of those are declared on the group. Font-shape
29
75
  tokens carry `canBeLinked: true` and a stable `groupKey` so the linked-block linkage
30
- sees them; the color token is emitted plain (no groupKey, not shareable) so it stays
31
- out of the linked block while still appearing in the editor's full token surface
32
- (used by the reset-button and the design-token resolution test).
76
+ sees them; the color token is emitted plain (no groupKey, not shareable) unless a
77
+ derivation is supplied, in which case it too gets a slot-scoped key.
78
+
79
+ Pass `{ component, variants }` so font keys are slot-scoped
80
+ (`--card-default-title-font-family` → `title-font-family`) instead of the bare
81
+ `font-family` default, which silently merges multiple slots into one link tree.
33
82
 
34
83
  Mirrors the `flatMap`/loop pattern in ButtonEditor and RadioButtonEditor so
35
84
  editors don't have to hand-list 16+ near-identical Token entries. */
@@ -37,15 +86,23 @@ export function buildTypeGroupTokens(
37
86
  typeGroups: Record<string, TypeGroupConfig[]>,
38
87
  options: BuildTypeGroupTokensOptions = {},
39
88
  ): Token[] {
40
- const { groupKeyFor } = options;
89
+ const { groupKeyFor, component, variants } = options;
90
+ const derive = component !== undefined || variants !== undefined;
41
91
  const tokens: Token[] = [];
42
92
  for (const groups of Object.values(typeGroups)) {
43
93
  for (const group of groups) {
44
- tokens.push({ label: group.colorLabel ?? 'color', variable: group.colorVariable });
94
+ const colorToken: Token = { label: group.colorLabel ?? 'color', variable: group.colorVariable };
95
+ if (group.colorGroupKey) colorToken.groupKey = group.colorGroupKey;
96
+ else if (derive) colorToken.groupKey = structuralGroupKey(group.colorVariable, { component, variants });
97
+ tokens.push(colorToken);
45
98
  for (const prop of TYPE_FONT_PROPS) {
46
99
  const variable = group[prop.key];
47
100
  if (!variable) continue;
48
- const groupKey = groupKeyFor ? groupKeyFor(prop, group) : prop.defaultGroupKey;
101
+ const groupKey = groupKeyFor
102
+ ? groupKeyFor(prop, group)
103
+ : derive
104
+ ? structuralGroupKey(variable, { component, variants })
105
+ : prop.defaultGroupKey;
49
106
  tokens.push({ label: prop.label, canBeLinked: true, groupKey, variable });
50
107
  }
51
108
  }
@@ -60,24 +117,70 @@ export function buildTypeGroupTokens(
60
117
  cleanly into both `Object.values(typeGroups).flat()` chains and per-variant
61
118
  `flatMap` constructions.
62
119
 
63
- Each color token gets a groupKey derived from the colorVariable's last-dash
64
- suffix (e.g. `--badge-primary-text` → `text`) so that all variants/states of
65
- the same slot are siblings and can be linked in the editor. They start out
66
- divergent (one value per variant) and the user can opt in to a single shared
67
- value via the link UI. */
120
+ groupKey precedence per group: explicit `group.colorGroupKey` > the second
121
+ argument (`groupKeyFor` callback) > structural derivation (`{ component, variants }`).
122
+ With none of these the color token is emitted without a groupKey (solo, no
123
+ siblings) the contract is explicit, there is no name-based inference. Pass
124
+ `{ component, variants }` to group a slot's color across variants; structural
125
+ derivation keeps several slots ending in the same word (SideNavigation's
126
+ section / item / footer) distinct, so `editor color/font link-group parity`
127
+ stays satisfied. */
68
128
  export function buildTypeGroupColorTokens(
69
129
  typeGroups: Record<string, TypeGroupConfig[]> | TypeGroupConfig[],
130
+ option?: ColorGroupKeyOption,
70
131
  ): Token[] {
71
132
  const groups: TypeGroupConfig[] = Array.isArray(typeGroups)
72
133
  ? typeGroups
73
134
  : Object.values(typeGroups).flat();
135
+ const legacyFn = typeof option === 'function' ? option : undefined;
136
+ const opts = typeof option === 'object' ? option : undefined;
137
+ const groupKeyFor = legacyFn ?? opts?.groupKeyFor;
138
+ const derive = opts && (opts.component !== undefined || opts.variants !== undefined);
74
139
  return groups.map((g) => {
75
- const lastDash = g.colorVariable.lastIndexOf('-');
76
- const groupKey = lastDash >= 0 ? g.colorVariable.slice(lastDash + 1) : g.colorVariable;
77
- return { label: g.colorLabel ?? 'color', variable: g.colorVariable, groupKey };
140
+ const groupKey = g.colorGroupKey
141
+ ? g.colorGroupKey
142
+ : groupKeyFor
143
+ ? groupKeyFor(g)
144
+ : derive
145
+ ? structuralGroupKey(g.colorVariable, opts)
146
+ : undefined;
147
+ const token: Token = { label: g.colorLabel ?? 'color', variable: g.colorVariable };
148
+ if (groupKey) token.groupKey = groupKey;
149
+ return token;
78
150
  });
79
151
  }
80
152
 
153
+ /** Font-only counterpart: the family/size/weight/line-height tokens for each group
154
+ with slot-scoped, linkable groupKeys, but no color token. Use alongside
155
+ `buildTypeGroupColorTokens` when a component wants its type-group colors and fonts
156
+ emitted separately (e.g. Table keeps per-slot colors but a custom layout). Keys
157
+ derive structurally from `{ component, variants }`; without a derivation they fall
158
+ back to the bare `font-family`/`font-size`/… defaults. */
159
+ export function buildTypeGroupFontTokens(
160
+ typeGroups: Record<string, TypeGroupConfig[]> | TypeGroupConfig[],
161
+ options: BuildTypeGroupTokensOptions = {},
162
+ ): Token[] {
163
+ const { groupKeyFor, component, variants } = options;
164
+ const derive = component !== undefined || variants !== undefined;
165
+ const groups: TypeGroupConfig[] = Array.isArray(typeGroups)
166
+ ? typeGroups
167
+ : Object.values(typeGroups).flat();
168
+ const tokens: Token[] = [];
169
+ for (const group of groups) {
170
+ for (const prop of TYPE_FONT_PROPS) {
171
+ const variable = group[prop.key];
172
+ if (!variable) continue;
173
+ const groupKey = groupKeyFor
174
+ ? groupKeyFor(prop, group)
175
+ : derive
176
+ ? structuralGroupKey(variable, { component, variants })
177
+ : prop.defaultGroupKey;
178
+ tokens.push({ label: prop.label, canBeLinked: true, groupKey, variable });
179
+ }
180
+ }
181
+ return tokens;
182
+ }
183
+
81
184
  /** Companion helper: derive a `linkableContexts` map mapping every typography variable in
82
185
  `typeGroups` to its state name. Use to build the linked-block context map without
83
186
  spelling out 16+ entries; merge with non-typography entries via spread. */
@@ -84,6 +84,10 @@ export type TypeGroupConfig = {
84
84
  outlineWidthLabel?: string;
85
85
  outlineColorVariable?: string;
86
86
  outlineColorLabel?: string;
87
+ /** Explicit groupKey for this group's color token. Wins over every derivation
88
+ (structural or `groupKeyFor`) and is never recomputed — the durable, one-line
89
+ fix when the derived key is wrong. */
90
+ colorGroupKey?: string;
87
91
  /** See `Token.element` — when present, StateBlock groups this fieldset under
88
92
  the matching element subsection. */
89
93
  element?: string;
@@ -146,15 +146,61 @@
146
146
  let floating = $state({ ...initial.floating });
147
147
 
148
148
  // Collapsed-pill size; slight overshoot is fine (overflow:hidden).
149
- const COLLAPSED_WIDTH = 200;
149
+ const COLLAPSED_WIDTH = 232;
150
150
  const COLLAPSED_HEIGHT = 44;
151
151
 
152
152
  // Fade for open-only buttons (bar timing lives in CSS vars below).
153
153
  const BTN_FADE = { duration: 130, easing: cubicInOut };
154
154
 
155
+ // Mirror the CSS bar timings; used for the fallback that clears the mask if
156
+ // no transitionend fires (reduced motion, suppressed transitions, no delta).
157
+ const OPEN_DUR = 240;
158
+ const CLOSE_DUR = 240;
159
+ const CLOSE_DELAY = 120; // matches --bar-close-delay: collapse waits for the fade-out
160
+
155
161
  // Suppress CSS transitions during gestures + mode swaps.
156
162
  let suppressTransition = $state(false);
157
163
 
164
+ // Mask the iframe during the pill↔full size change so the editor route's
165
+ // reflow (side-nav scrollbar, white body) never shows. On open, content fades
166
+ // in once the panel settles; on close, content fades out first, then the
167
+ // panel collapses (the shrink is delayed by --bar-close-delay to wait for it).
168
+ let opening = $state(false);
169
+ let closing = $state(false);
170
+ let prevOpen = open;
171
+ let maskTimer: ReturnType<typeof setTimeout> | undefined;
172
+ run(() => {
173
+ if (open === prevOpen) return;
174
+ prevOpen = open;
175
+ clearTimeout(maskTimer);
176
+ if (open) {
177
+ closing = false;
178
+ opening = true;
179
+ if (gesturing || suppressTransition) {
180
+ // .no-transition path: no transitionend will fire, so reveal next frame.
181
+ requestAnimationFrame(() => { opening = false; });
182
+ } else {
183
+ maskTimer = setTimeout(() => { opening = false; }, OPEN_DUR + 60);
184
+ }
185
+ } else {
186
+ opening = false;
187
+ closing = true;
188
+ if (gesturing || suppressTransition) {
189
+ requestAnimationFrame(() => { closing = false; });
190
+ } else {
191
+ maskTimer = setTimeout(() => { closing = false; }, CLOSE_DELAY + CLOSE_DUR + 60);
192
+ }
193
+ }
194
+ });
195
+
196
+ function onPanelTransitionEnd(e: TransitionEvent) {
197
+ if (e.target !== e.currentTarget) return;
198
+ if (e.propertyName !== 'width' && e.propertyName !== 'height') return;
199
+ clearTimeout(maskTimer);
200
+ opening = false;
201
+ closing = false;
202
+ }
203
+
158
204
  // Scrim catches pointer events during gestures so they hit the panel, not the iframe.
159
205
  let gesturing: 'drag' | 'resize-left' | 'resize-se' | null = $state(null);
160
206
 
@@ -268,6 +314,7 @@
268
314
  });
269
315
  onDestroy(() => {
270
316
  window.removeEventListener('lt-overlay-toggle', handleToggleRequest);
317
+ clearTimeout(maskTimer);
271
318
  });
272
319
 
273
320
  let panelStyle = $derived(!open
@@ -286,7 +333,10 @@
286
333
  class:hidden={!open}
287
334
  class:docked={open && mode === 'docked'}
288
335
  class:floating={open && mode === 'floating'}
336
+ class:opening={open && opening}
337
+ class:closing={!open && closing}
289
338
  class:no-transition={!!gesturing || suppressTransition}
339
+ ontransitionend={onPanelTransitionEnd}
290
340
  >
291
341
  <div
292
342
  class="header"
@@ -394,8 +444,8 @@
394
444
  --bar-open-ease: cubic-bezier(0.65, 0, 0.35, 1);
395
445
  --bar-open-delay: 0ms;
396
446
  --bar-close-dur: 240ms;
397
- --bar-close-ease: cubic-bezier(0.65, 0, 0.35, 1);
398
- --bar-close-delay: 70ms;
447
+ --bar-close-ease: cubic-bezier(0, 0, 0.2, 1); /* ease-out */
448
+ --bar-close-delay: 120ms; /* let the iframe fade out before the panel shrinks */
399
449
 
400
450
  display: flex;
401
451
  flex-direction: column;
@@ -426,6 +476,7 @@
426
476
  /* Collapsed state: pinned top-right; iframe stays mounted, clipped by overflow:hidden. */
427
477
  .lt-overlay.hidden {
428
478
  border-radius: var(--ui-radius-lg, 6px);
479
+ border-color: rgba(255, 255, 255, 0.32);
429
480
  transition:
430
481
  width var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay),
431
482
  height var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay),
@@ -616,6 +667,23 @@
616
667
  height: 100%;
617
668
  border: 0;
618
669
  display: block;
670
+ transition: opacity 200ms ease-in;
671
+ }
672
+
673
+ /* While the panel grows pill → full, hide the iframe over the #000 frame-wrap
674
+ so the editor route's intermediate reflow (side-nav scrollbar, white body)
675
+ never shows; it fades in once the size transition settles. */
676
+ .lt-overlay.opening .editor-frame {
677
+ opacity: 0;
678
+ pointer-events: none;
679
+ }
680
+
681
+ /* On collapse, fade the iframe out quickly first (the panel shrink is held
682
+ back by --bar-close-delay), so the reflow during shrink stays masked. */
683
+ .lt-overlay.closing .editor-frame {
684
+ opacity: 0;
685
+ pointer-events: none;
686
+ transition: opacity 120ms ease-in;
619
687
  }
620
688
 
621
689
  .gesture-scrim {
@@ -654,4 +722,12 @@
654
722
  transparent 55%
655
723
  );
656
724
  }
725
+
726
+ @media (prefers-reduced-motion: reduce) {
727
+ .lt-overlay,
728
+ .lt-overlay.hidden,
729
+ .editor-frame {
730
+ transition: none;
731
+ }
732
+ }
657
733
  </style>
@@ -7,8 +7,14 @@
7
7
  font-family: var(--ui-font-sans);
8
8
  }
9
9
 
10
- /* Override tokens.css's global :where(*) font-family. */
11
- .editor-page *:not([class*="fa-"]) {
10
+ /* Pull editor chrome onto --ui-font-sans, overriding tokens.css's global
11
+ :where(*) theme-font baseline. The element matcher stays inside :where() so
12
+ this override computes to (0,1,0) — above the zero-specificity baseline but
13
+ below a component's single-class scoped font rule (`.sn-item.svelte-HASH` =
14
+ (0,2,0)). Without :where() it would tie those component rules and, winning on
15
+ source order, freeze every preview on --ui-font-sans (font-token edits write
16
+ the variable but the preview never repaints). */
17
+ .editor-page :where(*:not([class*="fa-"])) {
12
18
  font-family: inherit;
13
19
  }
14
20
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#ffffff" d="M10.226 17.284c-2.965-.36-5.054-2.493-5.054-5.256 0-1.123.404-2.336 1.078-3.144-.292-.741-.247-2.314.09-2.965.898-.112 2.111.36 2.83 1.01.853-.269 1.752-.404 2.853-.404 1.1 0 1.999.135 2.807.382.696-.629 1.932-1.1 2.83-.988.315.606.36 2.179.067 2.942.72.854 1.101 2 1.101 3.167 0 2.763-2.089 4.852-5.098 5.234.763.494 1.28 1.572 1.28 2.807v2.336c0 .674.561 1.056 1.235.786 4.066-1.55 7.255-5.615 7.255-10.646C23.5 6.188 18.334 1 11.978 1 5.62 1 .5 6.188.5 12.545c0 4.986 3.167 9.12 7.435 10.669.606.225 1.19-.18 1.19-.786V20.63a2.9 2.9 0 0 1-1.078.224c-1.483 0-2.359-.808-2.987-2.313-.247-.607-.517-.966-1.034-1.033-.27-.023-.359-.135-.359-.27 0-.27.45-.471.898-.471.652 0 1.213.404 1.797 1.235.45.651.921.943 1.483.943.561 0 .92-.202 1.437-.719.382-.381.674-.718.944-.943"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#ffffff" fill-rule="evenodd" d="M0,16V0H16V16ZM3,3V13H8V5h3v8h2V3Z"/></svg>
Binary file
@@ -77,7 +77,7 @@
77
77
  --button-primary-surface: var(--surface-brand-high);
78
78
  --button-primary-text: var(--text-primary);
79
79
  --button-primary-text-font-family: var(--font-sans);
80
- --button-primary-text-font-size: var(--font-size-md);
80
+ --button-primary-text-font-size: var(--font-size-lg);
81
81
  --button-primary-text-font-weight: var(--font-weight-semibold);
82
82
  --button-primary-text-line-height: var(--line-height-sm);
83
83
  --button-primary-border: var(--border-brand);
@@ -90,13 +90,13 @@
90
90
  --button-primary-disabled-surface: var(--color-neutral-700);
91
91
  --button-primary-disabled-text: var(--text-tertiary);
92
92
  --button-primary-disabled-border: var(--border-neutral-faint);
93
- --button-primary-icon-size: var(--icon-size-sm);
93
+ --button-primary-icon-size: var(--icon-size-md);
94
94
 
95
95
  /* Secondary */
96
96
  --button-secondary-surface: var(--surface-neutral-high);
97
97
  --button-secondary-text: var(--text-primary);
98
98
  --button-secondary-text-font-family: var(--font-sans);
99
- --button-secondary-text-font-size: var(--font-size-md);
99
+ --button-secondary-text-font-size: var(--font-size-lg);
100
100
  --button-secondary-text-font-weight: var(--font-weight-semibold);
101
101
  --button-secondary-text-line-height: var(--line-height-sm);
102
102
  --button-secondary-border: var(--border-neutral);
@@ -109,13 +109,13 @@
109
109
  --button-secondary-disabled-surface: var(--color-neutral-700);
110
110
  --button-secondary-disabled-text: var(--text-tertiary);
111
111
  --button-secondary-disabled-border: var(--border-neutral-faint);
112
- --button-secondary-icon-size: var(--icon-size-sm);
112
+ --button-secondary-icon-size: var(--icon-size-md);
113
113
 
114
114
  /* Outline */
115
115
  --button-outline-surface: var(--color-transparent);
116
116
  --button-outline-text: var(--text-primary);
117
117
  --button-outline-text-font-family: var(--font-sans);
118
- --button-outline-text-font-size: var(--font-size-md);
118
+ --button-outline-text-font-size: var(--font-size-lg);
119
119
  --button-outline-text-font-weight: var(--font-weight-semibold);
120
120
  --button-outline-text-line-height: var(--line-height-sm);
121
121
  --button-outline-border: var(--border-neutral);
@@ -129,13 +129,13 @@
129
129
  --button-outline-disabled-surface: var(--color-transparent);
130
130
  --button-outline-disabled-text: var(--text-tertiary);
131
131
  --button-outline-disabled-border: var(--border-neutral-faint);
132
- --button-outline-icon-size: var(--icon-size-sm);
132
+ --button-outline-icon-size: var(--icon-size-md);
133
133
 
134
134
  /* Success */
135
135
  --button-success-surface: var(--surface-success-low);
136
136
  --button-success-text: var(--text-success);
137
137
  --button-success-text-font-family: var(--font-sans);
138
- --button-success-text-font-size: var(--font-size-md);
138
+ --button-success-text-font-size: var(--font-size-lg);
139
139
  --button-success-text-font-weight: var(--font-weight-semibold);
140
140
  --button-success-text-line-height: var(--line-height-sm);
141
141
  --button-success-border: var(--border-success);
@@ -148,13 +148,13 @@
148
148
  --button-success-disabled-surface: var(--color-neutral-700);
149
149
  --button-success-disabled-text: var(--text-tertiary);
150
150
  --button-success-disabled-border: var(--border-neutral-faint);
151
- --button-success-icon-size: var(--icon-size-sm);
151
+ --button-success-icon-size: var(--icon-size-md);
152
152
 
153
153
  /* Danger */
154
154
  --button-danger-surface: var(--surface-danger-low);
155
155
  --button-danger-text: var(--text-danger);
156
156
  --button-danger-text-font-family: var(--font-sans);
157
- --button-danger-text-font-size: var(--font-size-md);
157
+ --button-danger-text-font-size: var(--font-size-lg);
158
158
  --button-danger-text-font-weight: var(--font-weight-semibold);
159
159
  --button-danger-text-line-height: var(--line-height-sm);
160
160
  --button-danger-border: var(--border-danger);
@@ -167,13 +167,13 @@
167
167
  --button-danger-disabled-surface: var(--color-neutral-700);
168
168
  --button-danger-disabled-text: var(--text-tertiary);
169
169
  --button-danger-disabled-border: var(--border-neutral-faint);
170
- --button-danger-icon-size: var(--icon-size-sm);
170
+ --button-danger-icon-size: var(--icon-size-md);
171
171
 
172
172
  /* Warning */
173
173
  --button-warning-surface: var(--surface-warning-low);
174
174
  --button-warning-text: var(--text-warning);
175
175
  --button-warning-text-font-family: var(--font-sans);
176
- --button-warning-text-font-size: var(--font-size-md);
176
+ --button-warning-text-font-size: var(--font-size-lg);
177
177
  --button-warning-text-font-weight: var(--font-weight-semibold);
178
178
  --button-warning-text-line-height: var(--line-height-sm);
179
179
  --button-warning-border: var(--border-warning);
@@ -186,7 +186,7 @@
186
186
  --button-warning-disabled-surface: var(--color-neutral-700);
187
187
  --button-warning-disabled-text: var(--text-tertiary);
188
188
  --button-warning-disabled-border: var(--border-neutral-faint);
189
- --button-warning-icon-size: var(--icon-size-sm);
189
+ --button-warning-icon-size: var(--icon-size-md);
190
190
 
191
191
  /* Small size — shared across all variants. The `.small` rule below reads
192
192
  these tokens directly (no per-variant rebind needed, since small
@@ -61,7 +61,7 @@
61
61
  --codesnippet-gap: var(--space-16);
62
62
 
63
63
  /* Code text. */
64
- --codesnippet-code-text: var(--text-brand-secondary);
64
+ --codesnippet-code-text: var(--text-brand);
65
65
  --codesnippet-code-font-family: var(--font-mono);
66
66
  --codesnippet-code-font-size: var(--font-size-md);
67
67
  --codesnippet-code-font-weight: var(--font-weight-normal);
@@ -78,6 +78,9 @@
78
78
  }
79
79
 
80
80
  .codesnippet {
81
+ /* Without a global border-box reset, content-box would let the snippet's own
82
+ padding + border overflow max-width: 100%, tripping a spurious side scroll. */
83
+ box-sizing: border-box;
81
84
  display: inline-flex;
82
85
  align-items: flex-start;
83
86
  gap: var(--codesnippet-gap);
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ /** Forwarded to the stage so callers can inject token overrides. */
6
+ style?: string;
7
+ /** Fixes the stage height so it never reflows when its content resizes. */
8
+ minHeight?: string;
9
+ children: Snippet;
10
+ }
11
+
12
+ let { style = '', minHeight, children }: Props = $props();
13
+ </script>
14
+
15
+ <div class="panel" {style} style:min-height={minHeight}>
16
+ {@render children()}
17
+ </div>
18
+
19
+ <style>
20
+ :global(:root) {
21
+ --panel-frame-border: var(--border-neutral);
22
+ --panel-frame-border-width: var(--border-width-1);
23
+ --panel-frame-radius: var(--radius-2xl);
24
+
25
+ --panel-stage-surface: linear-gradient(0deg, color-mix(in srgb, var(--surface-neutral-low) 40%, transparent) 0.5%, color-mix(in srgb, var(--surface-neutral-lowest) 75%, transparent) 100%);
26
+ --panel-stage-padding: var(--space-16);
27
+ --panel-stage-inline-padding: var(--space-32);
28
+ --panel-stage-gap: var(--space-16);
29
+ }
30
+
31
+ .panel {
32
+ box-sizing: border-box;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ gap: var(--panel-stage-gap);
37
+ width: 100%;
38
+ border: var(--panel-frame-border-width) solid var(--panel-frame-border);
39
+ border-radius: var(--panel-frame-radius);
40
+ background: var(--panel-stage-surface);
41
+ padding: var(--panel-stage-padding) var(--panel-stage-inline-padding);
42
+ overflow: hidden;
43
+ }
44
+ </style>
@@ -213,7 +213,7 @@
213
213
  --sectiondivider-lg-eyebrow-font-weight: var(--font-weight-medium);
214
214
  --sectiondivider-lg-eyebrow-font-size: var(--font-size-md);
215
215
  --sectiondivider-lg-eyebrow-letter-spacing: var(--letter-spacing-wide);
216
- --sectiondivider-lg-padding: var(--space-4);
216
+ --sectiondivider-lg-padding: var(--space-0);
217
217
  --sectiondivider-lg-title-padding: var(--space-2);
218
218
  --sectiondivider-lg-description-padding: var(--space-0);
219
219
  --sectiondivider-lg-eyebrow-padding: var(--space-0);
@@ -244,7 +244,7 @@
244
244
  --sectiondivider-md-eyebrow-font-weight: var(--font-weight-medium);
245
245
  --sectiondivider-md-eyebrow-font-size: var(--font-size-sm);
246
246
  --sectiondivider-md-eyebrow-letter-spacing: var(--letter-spacing-wide);
247
- --sectiondivider-md-padding: var(--space-4);
247
+ --sectiondivider-md-padding: var(--space-0);
248
248
  --sectiondivider-md-title-padding: var(--space-2);
249
249
  --sectiondivider-md-description-padding: var(--space-0);
250
250
  --sectiondivider-md-eyebrow-padding: var(--space-0);
@@ -262,8 +262,8 @@
262
262
 
263
263
  /* Small */
264
264
  --sectiondivider-sm-title-font-family: var(--font-display);
265
- --sectiondivider-sm-title-font-weight: var(--font-weight-bold);
266
- --sectiondivider-sm-title-font-size: var(--font-size-2xl);
265
+ --sectiondivider-sm-title-font-weight: var(--font-weight-medium);
266
+ --sectiondivider-sm-title-font-size: var(--font-size-3xl);
267
267
  --sectiondivider-sm-title-line-height: var(--line-height-md);
268
268
  --sectiondivider-sm-title-letter-spacing: var(--letter-spacing-normal);
269
269
  --sectiondivider-sm-title-outline-width: var(--border-width-4);
@@ -275,7 +275,7 @@
275
275
  --sectiondivider-sm-eyebrow-font-weight: var(--font-weight-medium);
276
276
  --sectiondivider-sm-eyebrow-font-size: var(--font-size-xs);
277
277
  --sectiondivider-sm-eyebrow-letter-spacing: var(--letter-spacing-wide);
278
- --sectiondivider-sm-padding: var(--space-4);
278
+ --sectiondivider-sm-padding: var(--space-0);
279
279
  --sectiondivider-sm-title-padding: var(--space-2);
280
280
  --sectiondivider-sm-description-padding: var(--space-0);
281
281
  --sectiondivider-sm-eyebrow-padding: var(--space-0);
@@ -284,12 +284,12 @@
284
284
  --sectiondivider-sm-shadow: var(--shadow-none);
285
285
  --sectiondivider-sm-hairline-thickness: var(--border-width-1);
286
286
  --sectiondivider-sm-background: var(--color-transparent);
287
- --sectiondivider-sm-title: var(--text-primary);
287
+ --sectiondivider-sm-title: var(--text-brand);
288
288
  --sectiondivider-sm-description: var(--text-secondary);
289
289
  --sectiondivider-sm-eyebrow: var(--text-tertiary);
290
290
  --sectiondivider-sm-border: var(--color-transparent);
291
- --sectiondivider-sm-title-outline-color: var(--surface-canvas-lowest);
292
- --sectiondivider-sm-hairline-color: var(--border-brand-medium);
291
+ --sectiondivider-sm-title-outline-color: color-mix(in srgb, var(--surface-neutral-lowest) 25%, transparent);
292
+ --sectiondivider-sm-hairline-color: color-mix(in srgb, var(--border-brand-medium) 50%, transparent);
293
293
 
294
294
  /* Intrinsic defaults. These keys cascade to `:root` via the editor's
295
295
  alias bucket; un-edited variants fall back to these. The defaults
@@ -297,7 +297,7 @@
297
297
  eyebrow, visible description, hidden hairline, normal-case eyebrow. */
298
298
  --sectiondivider-lg-align: start;
299
299
  --sectiondivider-md-align: start;
300
- --sectiondivider-sm-align: start;
300
+ --sectiondivider-sm-align: center;
301
301
  --sectiondivider-lg-eyebrow-display: block;
302
302
  --sectiondivider-md-eyebrow-display: none;
303
303
  --sectiondivider-sm-eyebrow-display: none;
@@ -306,7 +306,7 @@
306
306
  --sectiondivider-sm-description-display: none;
307
307
  --sectiondivider-lg-hairline: below-description;
308
308
  --sectiondivider-md-hairline: below-label;
309
- --sectiondivider-sm-hairline: below-label;
309
+ --sectiondivider-sm-hairline: none;
310
310
  --sectiondivider-lg-eyebrow-text-transform: none;
311
311
  --sectiondivider-md-eyebrow-text-transform: none;
312
312
  --sectiondivider-sm-eyebrow-text-transform: none;
@@ -41,6 +41,13 @@
41
41
  class?: string;
42
42
  /** Toggle callback. Preferred over `on:toggle` from 0.5.0 onward. */
43
43
  ontoggle?: () => void;
44
+ /** Optional content rendered at the top of the rail, above the title
45
+ (e.g. a logo or company name). Hidden while collapsed. No spacing is
46
+ imposed — the consumer controls it. */
47
+ lead?: import('svelte').Snippet;
48
+ /** Optional content rendered at the foot of the nav list, inside the panel.
49
+ Hidden while collapsed. No spacing is imposed — the consumer controls it. */
50
+ actions?: import('svelte').Snippet;
44
51
  }
45
52
 
46
53
  let {
@@ -55,6 +62,8 @@
55
62
  forceActivePart = null,
56
63
  class: className = '',
57
64
  ontoggle,
65
+ lead,
66
+ actions,
58
67
  }: Props = $props();
59
68
 
60
69
  // Dual-fire bridge — see Button.svelte for the deprecation timeline.
@@ -152,6 +161,8 @@
152
161
  class:force-footer-hover={forceHoverPart === 'footer'}
153
162
  class:force-footer-active={forceActivePart === 'footer'}
154
163
  >
164
+ {#if open && lead}{@render lead()}{/if}
165
+
155
166
  <!-- Header is always rendered so the toggle (a child here) survives the
156
167
  collapsed state. The label is the only conditional child — when the
157
168
  rail is closed, the header reduces to just the toggle, centred via the
@@ -220,6 +231,8 @@
220
231
  <span>{footer.title}</span>
221
232
  </a>
222
233
  {/if}
234
+
235
+ {#if actions}{@render actions()}{/if}
223
236
  </div>
224
237
  {/if}
225
238
  </aside>