@motion-proto/live-tokens 0.3.9 → 0.5.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 (103) hide show
  1. package/package.json +9 -8
  2. package/src/component-editor/BadgeEditor.svelte +24 -22
  3. package/src/component-editor/CalloutEditor.svelte +3 -3
  4. package/src/component-editor/CardEditor.svelte +25 -21
  5. package/src/component-editor/CollapsibleSectionEditor.svelte +27 -25
  6. package/src/component-editor/CornerBadgeEditor.svelte +37 -35
  7. package/src/component-editor/DialogEditor.svelte +26 -24
  8. package/src/component-editor/ImageEditor.svelte +11 -9
  9. package/src/component-editor/InlineEditActionsEditor.svelte +17 -15
  10. package/src/component-editor/NotificationEditor.svelte +32 -30
  11. package/src/component-editor/ProgressBarEditor.svelte +3 -3
  12. package/src/component-editor/RadioButtonEditor.svelte +31 -29
  13. package/src/component-editor/SectionDividerEditor.svelte +30 -28
  14. package/src/component-editor/SegmentedControlEditor.svelte +29 -25
  15. package/src/component-editor/StandardButtonsEditor.svelte +42 -38
  16. package/src/component-editor/TabBarEditor.svelte +20 -18
  17. package/src/component-editor/TableEditor.svelte +4 -4
  18. package/src/component-editor/TooltipEditor.svelte +11 -9
  19. package/src/component-editor/registry.ts +2 -2
  20. package/src/component-editor/scaffolding/AngleDial.svelte +20 -19
  21. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +44 -20
  22. package/src/component-editor/scaffolding/ComponentFileManager.svelte +260 -37
  23. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +41 -29
  24. package/src/component-editor/scaffolding/ComponentsTab.svelte +7 -3
  25. package/src/component-editor/scaffolding/CopyFromMenu.svelte +21 -12
  26. package/src/component-editor/scaffolding/DemoHeader.svelte +13 -4
  27. package/src/component-editor/scaffolding/DividerEditor.svelte +27 -14
  28. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +10 -4
  29. package/src/component-editor/scaffolding/GradientCard.svelte +25 -20
  30. package/src/component-editor/scaffolding/LinkageChart.svelte +43 -34
  31. package/src/component-editor/scaffolding/LinkedBlock.svelte +24 -21
  32. package/src/component-editor/scaffolding/NonStylableConfig.svelte +6 -1
  33. package/src/component-editor/scaffolding/SaveAsDialog.svelte +39 -35
  34. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +21 -9
  35. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +8 -3
  36. package/src/component-editor/scaffolding/StateBlock.svelte +30 -13
  37. package/src/component-editor/scaffolding/TokenLayout.svelte +46 -30
  38. package/src/component-editor/scaffolding/TypeEditor.svelte +52 -26
  39. package/src/component-editor/scaffolding/VariantGroup.svelte +81 -48
  40. package/src/component-editor/scaffolding/componentSectionType.ts +2 -2
  41. package/src/components/Badge.svelte +45 -26
  42. package/src/components/Button.svelte +44 -21
  43. package/src/components/Callout.svelte +17 -12
  44. package/src/components/Card.svelte +23 -11
  45. package/src/components/CollapsibleSection.svelte +56 -27
  46. package/src/components/CornerBadge.svelte +32 -18
  47. package/src/components/Dialog.svelte +55 -31
  48. package/src/components/Image.svelte +14 -5
  49. package/src/components/InlineEditActions.svelte +22 -10
  50. package/src/components/Notification.svelte +39 -19
  51. package/src/components/ProgressBar.svelte +27 -17
  52. package/src/components/RadioButton.svelte +27 -10
  53. package/src/components/SectionDivider.svelte +34 -26
  54. package/src/components/SegmentedControl.svelte +23 -9
  55. package/src/components/TabBar.svelte +23 -10
  56. package/src/components/Table.svelte +8 -3
  57. package/src/components/Tooltip.svelte +15 -5
  58. package/src/lib/ColumnsOverlay.svelte +3 -3
  59. package/src/lib/LiveEditorOverlay.svelte +57 -36
  60. package/src/pages/ComponentEditorPage.svelte +17 -13
  61. package/src/pages/EditorShell.svelte +24 -20
  62. package/src/styles/form-controls.css +2 -2
  63. package/src/styles/tokens.css +59 -81
  64. package/src/ui/BezierCurveEditor.svelte +59 -43
  65. package/src/ui/ColorEditPanel.svelte +71 -44
  66. package/src/ui/EditorViewSwitcher.svelte +9 -5
  67. package/src/ui/FontStackEditor.svelte +16 -15
  68. package/src/ui/GradientEditor.svelte +42 -33
  69. package/src/ui/GradientStopPicker.svelte +18 -29
  70. package/src/ui/PaletteEditor.svelte +238 -212
  71. package/src/ui/PresetFileManager.svelte +20 -18
  72. package/src/ui/ProjectFontsSection.svelte +30 -30
  73. package/src/ui/SurfacesTab.svelte +3 -3
  74. package/src/ui/TextTab.svelte +2 -2
  75. package/src/ui/ThemeFileManager.svelte +38 -35
  76. package/src/ui/Toggle.svelte +11 -9
  77. package/src/ui/UICopyPopover.svelte +19 -15
  78. package/src/ui/UIDialog.svelte +48 -30
  79. package/src/ui/UIFontFamilySelector.svelte +104 -78
  80. package/src/ui/UIFontSizeSelector.svelte +38 -20
  81. package/src/ui/UIFontWeightSelector.svelte +33 -13
  82. package/src/ui/UILineHeightSelector.svelte +33 -13
  83. package/src/ui/UILinkToggle.svelte +7 -6
  84. package/src/ui/UIOptionItem.svelte +21 -7
  85. package/src/ui/UIOptionList.svelte +9 -3
  86. package/src/ui/UIPaddingSelector.svelte +108 -82
  87. package/src/ui/UIPaletteSelector.svelte +186 -161
  88. package/src/ui/UIRadio.svelte +23 -8
  89. package/src/ui/UIRadioGroup.svelte +9 -8
  90. package/src/ui/UIRelinkConfirmPopover.svelte +26 -16
  91. package/src/ui/UITokenSelector.svelte +112 -68
  92. package/src/ui/UIVariantSelector.svelte +79 -57
  93. package/src/ui/VariablesTab.svelte +15 -15
  94. package/src/ui/palette/GradientStopEditor.svelte +45 -26
  95. package/src/ui/palette/OverridesPanel.svelte +85 -49
  96. package/src/ui/palette/PaletteBase.svelte +60 -32
  97. package/src/ui/palette/ScaleCurveEditor.svelte +25 -10
  98. package/src/ui/sections/ColumnsSection.svelte +13 -13
  99. package/src/ui/sections/GradientsSection.svelte +12 -9
  100. package/src/ui/sections/OverlaysSection.svelte +50 -47
  101. package/src/ui/sections/ShadowsSection.svelte +110 -104
  102. package/src/ui/sections/TokenScaleTable.svelte +38 -22
  103. package/src/ui/sections/tokenScales.ts +2 -2
@@ -1,17 +1,24 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, onMount, onDestroy } from 'svelte';
2
+ import { run, createBubbler, stopPropagation } from 'svelte/legacy';
3
+
4
+ const bubble = createBubbler();
5
+ import { onMount, onDestroy } from 'svelte';
3
6
  import UIRadio from './UIRadio.svelte';
4
7
  import { keepInViewport } from './keepInViewport';
5
8
 
6
9
  type Candidate = { variable: string; alias: string };
7
10
 
8
- export let candidates: Candidate[];
9
- /** Variable whose lock was clicked — its alias is the default selection. */
10
- export let initialVariable: string;
11
- /** Component prefix to strip when rendering source labels. */
12
- export let prefixToStrip: string = '';
11
+ interface Props {
12
+ candidates: Candidate[];
13
+ /** Variable whose lock was clicked — its alias is the default selection. */
14
+ initialVariable: string;
15
+ /** Component prefix to strip when rendering source labels. */
16
+ prefixToStrip?: string;
17
+ onconfirm?: (payload: { alias: string }) => void;
18
+ oncancel?: () => void;
19
+ }
13
20
 
14
- const dispatch = createEventDispatcher<{ confirm: { alias: string }; cancel: void }>();
21
+ let { candidates, initialVariable, prefixToStrip = '', onconfirm, oncancel }: Props = $props();
15
22
 
16
23
  type Option = { alias: string; sources: string[] };
17
24
 
@@ -31,17 +38,19 @@
31
38
  return [...seen.values()];
32
39
  }
33
40
 
34
- $: options = distinctOptions(candidates, initialVariable);
35
- let selected = '';
36
- $: if (selected === '' && options.length > 0) selected = options[0].alias;
41
+ let options = $derived(distinctOptions(candidates, initialVariable));
42
+ let selected = $state('');
43
+ run(() => {
44
+ if (selected === '' && options.length > 0) selected = options[0].alias;
45
+ });
37
46
 
38
47
  function handleConfirm() {
39
48
  if (!selected) return;
40
- dispatch('confirm', { alias: selected });
49
+ onconfirm?.({ alias: selected });
41
50
  }
42
51
 
43
52
  function handleCancel() {
44
- dispatch('cancel');
53
+ oncancel?.();
45
54
  }
46
55
 
47
56
  function handleKeydown(e: KeyboardEvent) {
@@ -55,7 +64,7 @@
55
64
  }
56
65
  }
57
66
 
58
- let popoverEl: HTMLDivElement;
67
+ let popoverEl: HTMLDivElement | undefined = $state();
59
68
 
60
69
  onMount(() => {
61
70
  popoverEl?.focus();
@@ -67,6 +76,7 @@
67
76
  });
68
77
  </script>
69
78
 
79
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
70
80
  <div
71
81
  class="ui-relink-popover"
72
82
  role="dialog"
@@ -74,7 +84,7 @@
74
84
  tabindex="-1"
75
85
  bind:this={popoverEl}
76
86
  use:keepInViewport
77
- on:click|stopPropagation
87
+ onclick={stopPropagation(bubble('click'))}
78
88
  >
79
89
  <div class="ui-relink-header">
80
90
  {#if options.length > 1}
@@ -99,8 +109,8 @@
99
109
  </div>
100
110
 
101
111
  <div class="ui-relink-footer">
102
- <button type="button" class="ui-relink-btn ui-relink-btn-cancel" on:click={handleCancel}>Cancel</button>
103
- <button type="button" class="ui-relink-btn ui-relink-btn-confirm" on:click={handleConfirm}>Link</button>
112
+ <button type="button" class="ui-relink-btn ui-relink-btn-cancel" onclick={handleCancel}>Cancel</button>
113
+ <button type="button" class="ui-relink-btn ui-relink-btn-confirm" onclick={handleConfirm}>Link</button>
104
114
  </div>
105
115
  </div>
106
116
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { onMount, onDestroy, createEventDispatcher } from 'svelte';
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import type { Snippet } from 'svelte';
3
4
  import { setCssVar, removeCssVar, CSS_VAR_CHANGE_EVENT } from '../lib/cssVarSync';
4
5
  import type { CssVarRef } from '../lib/editorTypes';
5
6
  import {
@@ -17,47 +18,85 @@
17
18
  import UIRelinkConfirmPopover from './UIRelinkConfirmPopover.svelte';
18
19
  import { keepInViewport } from './keepInViewport';
19
20
 
20
- const dispatch = createEventDispatcher<{
21
- change: void;
22
- reset: void;
23
- open: void;
24
- close: void;
25
- 'var-change': void;
26
- }>();
27
-
28
- export let variable: string;
29
- /** When set, writes persist through the editor store under this component. */
30
- export let component: string | undefined = undefined;
31
- /** When true, render a link toggle that lets the user share this value across all sibling variants. */
32
- export let canBeLinked: boolean = false;
33
- /** Minimum width of the dropdown panel. */
34
- export let dropdownMinWidth: string = '14rem';
35
- /** Max width of the dropdown panel (useful for grids). */
36
- export let dropdownMaxWidth: string = '';
37
- /** When true, the default dropdown header (variable name + reset) is omitted. */
38
- export let hideDefaultHeader: boolean = false;
39
- /** When true, the trigger becomes non-interactive and visually dimmed. */
40
- export let disabled: boolean = false;
41
- /** When true, the trigger opens normally but the dropdown's selection area is
42
- * dimmed and non-interactive. The lock toggle in the header stays active so
43
- * the user can re-engage editing by re-linking. Used by the linked block to
44
- * make the row openable even when currently unshared. */
45
- export let selectionsLocked: boolean = false;
46
-
47
- let open = false;
21
+ type DropdownContext = { close: () => void; handleReset: () => void };
22
+
23
+ interface Props {
24
+ variable: string;
25
+ /** When set, writes persist through the editor store under this component. */
26
+ component?: string | undefined;
27
+ /** When true, render a link toggle that lets the user share this value across all sibling variants. */
28
+ canBeLinked?: boolean;
29
+ /** Minimum width of the dropdown panel. */
30
+ dropdownMinWidth?: string;
31
+ /** Max width of the dropdown panel (useful for grids). */
32
+ dropdownMaxWidth?: string;
33
+ /** When true, the default dropdown header (variable name + reset) is omitted. */
34
+ hideDefaultHeader?: boolean;
35
+ /** When true, the trigger becomes non-interactive and visually dimmed. */
36
+ disabled?: boolean;
37
+ /** When true, the trigger opens normally but the dropdown's selection area is
38
+ * dimmed and non-interactive. The lock toggle in the header stays active so
39
+ * the user can re-engage editing by re-linking. Used by the linked block to
40
+ * make the row openable even when currently unshared. */
41
+ selectionsLocked?: boolean;
42
+ /** Optional preview rendered before the trigger text (e.g. swatch). Renamed from `slot="trigger-preview"` in 0.5.0. */
43
+ triggerPreview?: Snippet;
44
+ /** Optional full trigger text replacement. Falls back to `triggerTitle`. Renamed from `slot="trigger-text"` in 0.5.0. */
45
+ triggerText?: Snippet;
46
+ /** Optional trigger title text. Renamed from `slot="trigger-title"` in 0.5.0. */
47
+ triggerTitle?: Snippet;
48
+ /** Optional meta text rendered after the trigger. Renamed from `slot="trigger-meta"` in 0.5.0. */
49
+ triggerMeta?: Snippet;
50
+ /** Optional dropdown header replacement. */
51
+ header?: Snippet;
52
+ /** Optional dropdown content above the selections. */
53
+ subheader?: Snippet;
54
+ /** Dropdown body content; receives `{ close, handleReset }`. */
55
+ children?: Snippet<[DropdownContext]>;
56
+ onchange?: () => void;
57
+ onreset?: () => void;
58
+ onopen?: () => void;
59
+ onclose?: () => void;
60
+ onvarChange?: () => void;
61
+ }
62
+
63
+ let {
64
+ variable,
65
+ component = undefined,
66
+ canBeLinked = false,
67
+ dropdownMinWidth = '14rem',
68
+ dropdownMaxWidth = '',
69
+ hideDefaultHeader = false,
70
+ disabled = false,
71
+ selectionsLocked = false,
72
+ triggerPreview,
73
+ triggerText,
74
+ triggerTitle,
75
+ triggerMeta,
76
+ header,
77
+ subheader,
78
+ children,
79
+ onchange,
80
+ onreset,
81
+ onopen,
82
+ onclose,
83
+ onvarChange,
84
+ }: Props = $props();
85
+
86
+ let open = $state(false);
48
87
  let container: HTMLElement;
49
- let relinkOpen = false;
50
- let relinkCandidates: { variable: string; alias: string }[] = [];
88
+ let relinkOpen = $state(false);
89
+ let relinkCandidates: { variable: string; alias: string }[] = $state([]);
51
90
 
52
- $: isLinkedFromData = canBeLinked && component && $editorState
91
+ let isLinkedFromData = $derived(canBeLinked && component && $editorState
53
92
  ? isComponentPropertyLinked(component, variable)
54
- : false;
55
- $: isLinkedDisplay = canBeLinked && !!component && isLinkedFromData;
56
- $: peerCount = canBeLinked && component && $editorState
93
+ : false);
94
+ let isLinkedDisplay = $derived(canBeLinked && !!component && isLinkedFromData);
95
+ let peerCount = $derived(canBeLinked && component && $editorState
57
96
  ? getComponentPropertySiblings(component, variable).length
58
- : 0;
59
- $: hasSiblings = peerCount >= 2;
60
- $: showLinkToggle = canBeLinked && !!component && hasSiblings;
97
+ : 0);
98
+ let hasSiblings = $derived(peerCount >= 2);
99
+ let showLinkToggle = $derived(canBeLinked && !!component && hasSiblings);
61
100
 
62
101
  /** Persist a semantic CSS-var reference (or clear it when null). */
63
102
  export function writeOverride(semanticName: string | null): void {
@@ -90,22 +129,25 @@
90
129
  export function close() {
91
130
  if (!open) return;
92
131
  open = false;
93
- dispatch('close');
132
+ onclose?.();
94
133
  }
95
134
 
96
135
  function toggle() {
97
136
  if (disabled) return;
98
137
  open = !open;
99
- dispatch(open ? 'open' : 'close');
138
+ if (open) onopen?.();
139
+ else onclose?.();
100
140
  }
101
141
 
102
- $: if (disabled && open) close();
142
+ $effect(() => {
143
+ if (disabled && open) close();
144
+ });
103
145
 
104
146
  function toggleLinked() {
105
147
  if (!canBeLinked || !component) return;
106
148
  if (isLinkedDisplay) {
107
149
  unlinkComponentProperty(component, variable);
108
- dispatch('change');
150
+ onchange?.();
109
151
  return;
110
152
  }
111
153
  const slice = $editorState.components[component];
@@ -126,7 +168,7 @@
126
168
  const currentValue = slice.aliases[variable];
127
169
  if (currentValue) {
128
170
  setComponentAliasLinked(component, variable, currentValue);
129
- dispatch('change');
171
+ onchange?.();
130
172
  }
131
173
  return;
132
174
  }
@@ -150,7 +192,7 @@
150
192
  : slice.aliases[variable];
151
193
  if (adoptRef) setComponentAliasLinked(component, variable, adoptRef);
152
194
  else relinkComponentProperty(component, variable);
153
- dispatch('change');
195
+ onchange?.();
154
196
  return;
155
197
  }
156
198
 
@@ -158,10 +200,10 @@
158
200
  relinkOpen = true;
159
201
  }
160
202
 
161
- function handleRelinkConfirm(e: CustomEvent<{ alias: string }>) {
203
+ function handleRelinkConfirm(payload: { alias: string }) {
162
204
  if (!component) return;
163
- setComponentAliasLinked(component, variable, { kind: 'token', name: e.detail.alias });
164
- dispatch('change');
205
+ setComponentAliasLinked(component, variable, { kind: 'token', name: payload.alias });
206
+ onchange?.();
165
207
  relinkOpen = false;
166
208
  }
167
209
 
@@ -182,10 +224,10 @@
182
224
  // Per-peer resets while preserving the link are impossible by definition
183
225
  // (linked = peers share one value); a "reset just this one" intent is
184
226
  // really "unlink, then reset," which the user does in two visible steps.
185
- dispatch('reset');
227
+ onreset?.();
186
228
  writeOverride(null);
187
229
  close();
188
- dispatch('change');
230
+ onchange?.();
189
231
  }
190
232
 
191
233
  function handleClickOutside(e: MouseEvent) {
@@ -197,7 +239,7 @@
197
239
 
198
240
  function handleVarChange(e: Event) {
199
241
  const detail = (e as CustomEvent<{ name: string }>).detail;
200
- if (detail?.name === variable) dispatch('var-change');
242
+ if (detail?.name === variable) onvarChange?.();
201
243
  }
202
244
 
203
245
  onMount(() => {
@@ -217,21 +259,21 @@
217
259
  class="ui-ts-trigger"
218
260
  class:linked={isLinkedDisplay}
219
261
  class:unlinked={showLinkToggle && !isLinkedDisplay}
220
- on:click={toggle}
262
+ onclick={toggle}
221
263
  {disabled}
222
264
  >
223
265
  <div class="ui-ts-content">
224
- {#if $$slots['trigger-preview']}
266
+ {#if triggerPreview}
225
267
  <div class="ui-ts-preview">
226
- <slot name="trigger-preview" />
268
+ {@render triggerPreview()}
227
269
  </div>
228
270
  {/if}
229
271
  <div class="ui-ts-text">
230
- <slot name="trigger-text">
231
- {#if $$slots['trigger-title']}
232
- <span class="ui-ts-category"><slot name="trigger-title" /></span>
233
- {/if}
234
- </slot>
272
+ {#if triggerText}
273
+ {@render triggerText()}
274
+ {:else if triggerTitle}
275
+ <span class="ui-ts-category">{@render triggerTitle()}</span>
276
+ {/if}
235
277
  </div>
236
278
  </div>
237
279
  <i class="fas fa-chevron-down ui-ts-chevron" class:open></i>
@@ -242,8 +284,8 @@
242
284
  candidates={relinkCandidates}
243
285
  initialVariable={variable}
244
286
  prefixToStrip={`--${component}-`}
245
- on:confirm={handleRelinkConfirm}
246
- on:cancel={handleRelinkCancel}
287
+ onconfirm={handleRelinkConfirm}
288
+ oncancel={handleRelinkCancel}
247
289
  />
248
290
  {/if}
249
291
 
@@ -254,15 +296,17 @@
254
296
  use:keepInViewport
255
297
  >
256
298
  {#if !hideDefaultHeader}
257
- <slot name="header">
299
+ {#if header}
300
+ {@render header()}
301
+ {:else}
258
302
  <div class="ui-ts-header">
259
303
  {#if showLinkToggle}
260
- <UILinkToggle linked={isLinkedDisplay} on:toggle={toggleLinked} />
304
+ <UILinkToggle linked={isLinkedDisplay} ontoggle={toggleLinked} />
261
305
  {/if}
262
306
  <button
263
307
  type="button"
264
308
  class="ui-ts-reset"
265
- on:click={handleReset}
309
+ onclick={handleReset}
266
310
  disabled={selectionsLocked}
267
311
  title={selectionsLocked ? 'Unlock to reset' : 'Reset to default'}
268
312
  >
@@ -270,18 +314,18 @@
270
314
  <span>Reset</span>
271
315
  </button>
272
316
  </div>
273
- </slot>
317
+ {/if}
274
318
  {/if}
275
319
  <div class="ui-ts-selections" class:locked={selectionsLocked}>
276
- <slot name="subheader" />
277
- <slot {close} {handleReset} />
320
+ {@render subheader?.()}
321
+ {@render children?.({ close, handleReset })}
278
322
  </div>
279
323
  </div>
280
324
  {/if}
281
325
  </div>
282
326
 
283
- {#if $$slots['trigger-meta']}
284
- <span class="ui-ts-meta-text"><slot name="trigger-meta" /></span>
327
+ {#if triggerMeta}
328
+ <span class="ui-ts-meta-text">{@render triggerMeta()}</span>
285
329
  {/if}
286
330
  </div>
287
331
 
@@ -1,37 +1,62 @@
1
1
  <script lang="ts" generics="T extends { key: string; label?: string; value?: string }">
2
- import { createEventDispatcher } from 'svelte';
2
+ import type { Snippet } from 'svelte';
3
3
  import { resolveAliasChain } from '../lib/tokenRegistry';
4
4
  import UITokenSelector from './UITokenSelector.svelte';
5
5
  import UIOptionList from './UIOptionList.svelte';
6
6
  import UIOptionItem from './UIOptionItem.svelte';
7
7
 
8
- const dispatch = createEventDispatcher<{
9
- change: void;
10
- }>();
8
+ interface Props<O extends { key: string; label?: string; value?: string }> {
9
+ variable: string;
10
+ component?: string | undefined;
11
+ canBeLinked?: boolean;
12
+ disabled?: boolean;
13
+ selectionsLocked?: boolean;
14
+ dropdownMinWidth?: string;
15
+ dropdownMaxWidth?: string;
16
+ /** Forwarded to UIOptionList — when set, options render in a linked-column grid. */
17
+ dropdownGridColumns?: string;
18
+ /** CSS var prefix that, joined with an option `key`, forms the target var (e.g. `--font-weight-`). */
19
+ varPrefix: string;
20
+ /** Selectable options. Each must have a unique `key`. */
21
+ options: ReadonlyArray<O>;
22
+ /** Trigger title slot. Renamed from `slot="trigger-title"` in 0.5.0. */
23
+ triggerTitle?: Snippet<[{ activeOption: O | null }]>;
24
+ /** Trigger meta slot. Renamed from `slot="trigger-meta"` in 0.5.0. */
25
+ triggerMeta?: Snippet<[{ currentValue: string; activeOption: O | null }]>;
26
+ /** Per-option custom render. */
27
+ option?: Snippet<[{ opt: O; active: boolean; select: () => void }]>;
28
+ /** Trailing dropdown content (e.g. action buttons). */
29
+ extras?: Snippet<[{ close: () => void }]>;
30
+ onchange?: () => void;
31
+ }
11
32
 
12
- export let variable: string;
13
- export let component: string | undefined = undefined;
14
- export let canBeLinked: boolean = false;
15
- export let disabled: boolean = false;
16
- export let selectionsLocked: boolean = false;
17
- export let dropdownMinWidth: string = '12rem';
18
- export let dropdownMaxWidth: string = '';
19
- /** Forwarded to UIOptionList — when set, options render in a linked-column grid. */
20
- export let dropdownGridColumns: string = '';
21
- /** CSS var prefix that, joined with an option `key`, forms the target var (e.g. `--font-weight-`). */
22
- export let varPrefix: string;
23
- /** Selectable options. Each must have a unique `key`. */
24
- export let options: ReadonlyArray<T>;
33
+ let {
34
+ variable,
35
+ component = undefined,
36
+ canBeLinked = false,
37
+ disabled = false,
38
+ selectionsLocked = false,
39
+ dropdownMinWidth = '12rem',
40
+ dropdownMaxWidth = '',
41
+ dropdownGridColumns = '',
42
+ varPrefix,
43
+ options,
44
+ triggerTitle: callerTriggerTitle,
45
+ triggerMeta: callerTriggerMeta,
46
+ option: callerOption,
47
+ extras: callerExtras,
48
+ onchange,
49
+ }: Props<T> = $props();
25
50
 
26
51
  let selector: UITokenSelector;
27
- let chosenKey: string | null = null;
28
- let currentValue: string = '';
52
+ let chosenKey: string | null = $state(null);
53
+ let currentValue: string = $state('');
29
54
 
30
- $: validKeys = new Set(options.map((o) => o.key));
31
- $: refMatcher = (() => {
55
+ let validKeys = $derived(new Set(options.map((o) => o.key)));
56
+ let refMatcher = $derived.by(() => {
32
57
  const escaped = varPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
58
  return new RegExp(`var\\((${escaped}[^)\\s]+)\\)`);
34
- })();
59
+ });
35
60
 
36
61
  function parseRef(value: string): string | null {
37
62
  const m = value.match(refMatcher);
@@ -70,7 +95,7 @@
70
95
  function handleReset() {
71
96
  chosenKey = null;
72
97
  readResolved();
73
- dispatch('change');
98
+ onchange?.();
74
99
  }
75
100
 
76
101
  function selectKey(key: string, close: () => void) {
@@ -84,19 +109,21 @@
84
109
  }
85
110
  readResolved();
86
111
  close();
87
- dispatch('change');
112
+ onchange?.();
88
113
  }
89
114
 
90
115
  // Re-derive `chosenKey` when the bound `variable` changes (e.g. when a
91
116
  // VariantGroup tabs view reuses the same selector across states). Without
92
117
  // this, prop swaps leave the trigger label stale.
93
118
  let lastSeenVariable: string | null = null;
94
- $: if (variable !== lastSeenVariable) {
95
- lastSeenVariable = variable;
96
- initFromCurrent();
97
- }
119
+ $effect(() => {
120
+ if (variable !== lastSeenVariable) {
121
+ lastSeenVariable = variable;
122
+ initFromCurrent();
123
+ }
124
+ });
98
125
 
99
- $: activeOption = (options.find((o) => o.key === chosenKey) ?? null) as T | null;
126
+ let activeOption = $derived((options.find((o) => o.key === chosenKey) ?? null) as T | null);
100
127
  </script>
101
128
 
102
129
  <UITokenSelector
@@ -108,38 +135,33 @@
108
135
  {selectionsLocked}
109
136
  {dropdownMinWidth}
110
137
  {dropdownMaxWidth}
111
- on:reset={handleReset}
112
- on:var-change={initFromCurrent}
138
+ onreset={handleReset}
139
+ onvarChange={initFromCurrent}
113
140
  >
114
- <svelte:fragment slot="trigger-title">
115
- <slot name="trigger-title" {activeOption}>{activeOption?.label ?? ''}</slot>
116
- </svelte:fragment>
117
- <svelte:fragment slot="trigger-meta">
118
- <slot name="trigger-meta" {currentValue} {activeOption}>{currentValue || '—'}</slot>
119
- </svelte:fragment>
141
+ {#snippet triggerTitle()}
142
+ {#if callerTriggerTitle}{@render callerTriggerTitle({ activeOption })}{:else}{activeOption?.label ?? ''}{/if}
143
+ {/snippet}
144
+ {#snippet triggerMeta()}
145
+ {#if callerTriggerMeta}{@render callerTriggerMeta({ currentValue, activeOption })}{:else}{currentValue || '—'}{/if}
146
+ {/snippet}
120
147
 
121
- <svelte:fragment let:close>
148
+ {#snippet children({ close })}
122
149
  <UIOptionList gridColumns={dropdownGridColumns}>
123
150
  {#each options as opt (opt.key)}
124
- <slot
125
- name="option"
126
- {opt}
127
- active={chosenKey === opt.key}
128
- select={() => selectKey(opt.key, close)}
129
- >
130
- {#if opt.value !== undefined}
131
- <UIOptionItem active={chosenKey === opt.key} on:click={() => selectKey(opt.key, close)}>
132
- <svelte:fragment slot="label">{opt.label ?? ''}</svelte:fragment>
133
- <svelte:fragment slot="meta">{opt.value}</svelte:fragment>
134
- </UIOptionItem>
135
- {:else}
136
- <UIOptionItem active={chosenKey === opt.key} on:click={() => selectKey(opt.key, close)}>
137
- <svelte:fragment slot="label">{opt.label ?? ''}</svelte:fragment>
138
- </UIOptionItem>
139
- {/if}
140
- </slot>
151
+ {#if callerOption}
152
+ {@render callerOption({ opt, active: chosenKey === opt.key, select: () => selectKey(opt.key, close) })}
153
+ {:else if opt.value !== undefined}
154
+ <UIOptionItem active={chosenKey === opt.key} onclick={() => selectKey(opt.key, close)}>
155
+ {#snippet label()}{opt.label ?? ''}{/snippet}
156
+ {#snippet meta()}{opt.value}{/snippet}
157
+ </UIOptionItem>
158
+ {:else}
159
+ <UIOptionItem active={chosenKey === opt.key} onclick={() => selectKey(opt.key, close)}>
160
+ {#snippet label()}{opt.label ?? ''}{/snippet}
161
+ </UIOptionItem>
162
+ {/if}
141
163
  {/each}
142
- <slot name="extras" {close} />
164
+ {@render callerExtras?.({ close })}
143
165
  </UIOptionList>
144
- </svelte:fragment>
166
+ {/snippet}
145
167
  </UITokenSelector>