@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,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { run } from 'svelte/legacy';
3
+
2
4
  import DemoHeader from './DemoHeader.svelte';
3
5
  import NonStylableConfig from './NonStylableConfig.svelte';
4
6
  import LinkedBlock from './LinkedBlock.svelte';
@@ -6,36 +8,58 @@
6
8
  import type { Token } from './types';
7
9
  import type { LinkedBlockResult } from './linkedBlock';
8
10
 
9
- export let component: string;
10
- export let title: string;
11
- export let description: string = '';
12
- /** Token list used to drive the reset action in the header. The editor itself
11
+
12
+
13
+
14
+ interface Props {
15
+ component: string;
16
+ title: string;
17
+ description?: string;
18
+ /** Token list used to drive the reset action in the header. The editor itself
13
19
  is responsible for calling `registerComponentSchema` synchronously. */
14
- export let tokens: Token[] = [];
15
- /** Optional linked-block result. When provided, the LinkedBlock is rendered
20
+ tokens?: Token[];
21
+ /** Optional linked-block result. When provided, the LinkedBlock is rendered
16
22
  and hover highlights propagate to VariantGroup children via context. */
17
- export let linked: LinkedBlockResult | null = null;
18
- /** Canonical {value,label} list of variants in display order. When provided
23
+ linked?: LinkedBlockResult | null;
24
+ /** Canonical {value,label} list of variants in display order. When provided
19
25
  with 2+ entries, a single variant tab strip is rendered that drives which
20
26
  VariantGroup is focused. */
21
- export let variants: { value: string; label: string }[] = [];
27
+ variants?: { value: string; label: string }[];
28
+ config?: import('svelte').Snippet;
29
+ children?: import('svelte').Snippet<[any]>;
30
+ }
31
+
32
+ let {
33
+ component,
34
+ title,
35
+ description = '',
36
+ tokens = [],
37
+ linked = null,
38
+ variants = [],
39
+ config,
40
+ children
41
+ }: Props = $props();
22
42
 
23
43
  const ctx = createEditorContext();
24
44
  const { focusedVariant } = ctx;
25
45
 
26
- $: ctx._linkedOrder.set(linked?.linkedOrder ?? null);
27
- $: showVariantTabs = variants.length >= 2;
28
- $: if (showVariantTabs && ($focusedVariant === null || !variants.some((v) => v.value === $focusedVariant))) {
29
- focusedVariant.set(variants[0].value);
30
- }
31
- $: resetVariables = tokens.map((t) => t.variable);
46
+ run(() => {
47
+ ctx._linkedOrder.set(linked?.linkedOrder ?? null);
48
+ });
49
+ let showVariantTabs = $derived(variants.length >= 2);
50
+ run(() => {
51
+ if (showVariantTabs && ($focusedVariant === null || !variants.some((v) => v.value === $focusedVariant))) {
52
+ focusedVariant.set(variants[0].value);
53
+ }
54
+ });
55
+ let resetVariables = $derived(tokens.map((t) => t.variable));
32
56
  </script>
33
57
 
34
58
  <div class="demo-block">
35
59
  <DemoHeader {component} {title} {description} {resetVariables} />
36
- {#if $$slots.config}
60
+ {#if config}
37
61
  <NonStylableConfig>
38
- <slot name="config" />
62
+ {@render config?.()}
39
63
  </NonStylableConfig>
40
64
  {/if}
41
65
  {#if showVariantTabs}
@@ -47,14 +71,14 @@
47
71
  class:active={opt.value === $focusedVariant}
48
72
  role="tab"
49
73
  aria-selected={opt.value === $focusedVariant}
50
- on:click={() => focusedVariant.set(opt.value)}
74
+ onclick={() => focusedVariant.set(opt.value)}
51
75
  >{opt.label}</button>
52
76
  {/each}
53
77
  </div>
54
78
  {/if}
55
- <slot focusedVariant={$focusedVariant} />
79
+ {@render children?.({ focusedVariant: $focusedVariant, })}
56
80
  {#if linked}
57
- <LinkedBlock {component} {linked} on:change />
81
+ <LinkedBlock {component} {linked} />
58
82
  {/if}
59
83
  </div>
60
84
 
@@ -1,4 +1,4 @@
1
- <script context="module" lang="ts">
1
+ <script module lang="ts">
2
2
  declare const __PROJECT_ROOT__: string | undefined;
3
3
  </script>
4
4
 
@@ -27,18 +27,25 @@
27
27
  import ComponentFileMenu from './ComponentFileMenu.svelte';
28
28
  import SaveAsDialog from './SaveAsDialog.svelte';
29
29
 
30
- /** Which component this manager controls (e.g. "button"). */
31
- export let component: string;
32
- /** Display name shown at the start of the bar (e.g. "Segmented Control"). */
33
- export let title: string = '';
34
- /** When provided, renders a Reset button that reverts the component to its
30
+
31
+
32
+
33
+ interface Props {
34
+ /** Which component this manager controls (e.g. "button"). */
35
+ component: string;
36
+ /** Display name shown at the start of the bar (e.g. "Segmented Control"). */
37
+ title?: string;
38
+ /** When provided, renders a Reset button that reverts the component to its
35
39
  currently-loaded config file (discarding unsaved edits). To switch
36
40
  configs or return to default, use the File menu. */
37
- export let resetVariables: string[] | null = null;
41
+ resetVariables?: string[] | null;
42
+ }
43
+
44
+ let { component, title = '', resetVariables = null }: Props = $props();
38
45
 
39
46
  const projectRoot: string =
40
47
  typeof __PROJECT_ROOT__ !== 'undefined' ? (__PROJECT_ROOT__ ?? '') : '';
41
- $: sourceFile = componentSourceFile(component);
48
+ let sourceFile = $derived(componentSourceFile(component));
42
49
 
43
50
  type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
44
51
  let saveStatus: SaveStatus = 'idle';
@@ -49,15 +56,58 @@
49
56
  saveStatus = state;
50
57
  setTimeout(() => (saveStatus = 'idle'), 2000);
51
58
  }
52
- let files: ComponentConfigMeta[] = [];
53
- let activeFileName = 'default';
54
- let currentDisplayName = 'Default';
55
- let saveAsDialog = false;
59
+ let files: ComponentConfigMeta[] = $state([]);
60
+ let activeFileName = $state('default');
61
+ let currentDisplayName = $state('Default');
62
+ let saveAsDialog = $state(false);
56
63
 
57
- let productionInfo: ComponentProductionInfo | null = null;
64
+ let productionInfo = $state<ComponentProductionInfo | null>(null);
58
65
  type ProductionStatus = 'idle' | 'updating' | 'done' | 'error';
59
- let productionUpdateStatus: ProductionStatus = 'idle';
60
- let adoptFeedback = '';
66
+ let productionUpdateStatus: ProductionStatus = $state('idle');
67
+ let adoptFeedback = $state('');
68
+
69
+ let infoOpen = $state(false);
70
+ let infoBtnEl = $state<HTMLButtonElement | undefined>(undefined);
71
+ let infoPopoverEl = $state<HTMLDivElement | undefined>(undefined);
72
+ let infoPopoverReady = $state(false);
73
+
74
+ /** Anchor the fixed-position popover centered below the info button.
75
+ Uses position: fixed so it escapes the sticky header's stacking
76
+ context (which was letting the side panels paint over it). */
77
+ function positionInfoPopover(): void {
78
+ const btn = infoBtnEl;
79
+ const pop = infoPopoverEl;
80
+ if (!btn || !pop) return;
81
+ const br = btn.getBoundingClientRect();
82
+ const pr = pop.getBoundingClientRect();
83
+ const margin = 8;
84
+ let left = br.left + br.width / 2 - pr.width / 2;
85
+ const vw = window.innerWidth;
86
+ if (left < margin) left = margin;
87
+ if (left + pr.width > vw - margin) left = vw - margin - pr.width;
88
+ pop.style.left = `${left}px`;
89
+ pop.style.top = `${br.bottom + margin}px`;
90
+ infoPopoverReady = true;
91
+ }
92
+
93
+ $effect(() => {
94
+ if (!infoOpen) {
95
+ infoPopoverReady = false;
96
+ return;
97
+ }
98
+ // Two rAFs: first so Svelte mounts the popover and the bind: ref is set,
99
+ // second so its rendered width is measurable before we anchor it.
100
+ let raf1 = requestAnimationFrame(() => {
101
+ raf1 = requestAnimationFrame(positionInfoPopover);
102
+ });
103
+ window.addEventListener('scroll', positionInfoPopover, true);
104
+ window.addEventListener('resize', positionInfoPopover);
105
+ return () => {
106
+ cancelAnimationFrame(raf1);
107
+ window.removeEventListener('scroll', positionInfoPopover, true);
108
+ window.removeEventListener('resize', positionInfoPopover);
109
+ };
110
+ });
61
111
 
62
112
  /** Same idle-after-2s pattern for the production-update flash. */
63
113
  function flashProductionStatus(state: Exclude<ProductionStatus, 'idle'>) {
@@ -68,9 +118,9 @@
68
118
  }, 2000);
69
119
  }
70
120
 
71
- $: compDirty = $componentDirty[component] ?? false;
72
- $: isApplied = !!productionInfo && productionInfo.fileName === activeFileName && !compDirty;
73
- $: resetDirty = !!resetVariables && compDirty;
121
+ let compDirty = $derived($componentDirty[component] ?? false);
122
+ let isApplied = $derived(!!productionInfo && productionInfo.fileName === activeFileName && !compDirty);
123
+ let resetDirty = $derived(!!resetVariables && compDirty);
74
124
 
75
125
  async function refreshFiles() {
76
126
  // safeFetch returns null on dev-server unavailable / non-2xx — silently
@@ -99,16 +149,28 @@
99
149
  await refreshFiles();
100
150
  await refreshProduction();
101
151
  window.addEventListener('keydown', handleKeydown);
152
+ document.addEventListener('mousedown', handleDocumentMousedown, true);
102
153
  });
103
154
 
104
155
  onDestroy(() => {
105
156
  window.removeEventListener('keydown', handleKeydown);
157
+ document.removeEventListener('mousedown', handleDocumentMousedown, true);
106
158
  });
107
159
 
108
160
  function handleKeydown(e: KeyboardEvent) {
109
161
  if ((e.metaKey || e.ctrlKey) && e.key === 's') {
110
162
  e.preventDefault();
111
163
  handleSave();
164
+ } else if (e.key === 'Escape' && infoOpen) {
165
+ infoOpen = false;
166
+ }
167
+ }
168
+
169
+ function handleDocumentMousedown(e: MouseEvent) {
170
+ if (!infoOpen) return;
171
+ const target = e.target as Element | null;
172
+ if (target && !target.closest('.cfm-info-btn, .cfm-info-popover')) {
173
+ infoOpen = false;
112
174
  }
113
175
  }
114
176
 
@@ -166,8 +228,8 @@
166
228
  saveAsDialog = true;
167
229
  }
168
230
 
169
- async function confirmSaveAs(e: CustomEvent<{ displayName: string; fileName: string }>) {
170
- const { displayName, fileName } = e.detail;
231
+ async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
232
+ const { displayName, fileName } = detail;
171
233
  saveStatus = 'saving';
172
234
  try {
173
235
  await persist(fileName, displayName);
@@ -178,8 +240,7 @@
178
240
  }
179
241
  }
180
242
 
181
- async function handleLoad(e: CustomEvent<ComponentConfigMeta>) {
182
- const file = e.detail;
243
+ async function handleLoad(file: ComponentConfigMeta) {
183
244
  // Multi-step service flow (load + set-active) — if any network call
184
245
  // fails, the dialog is already closed and the local state stays on the
185
246
  // previous selection. Silent by design; the same boot resilience that
@@ -195,8 +256,7 @@
195
256
  }
196
257
  }
197
258
 
198
- async function handleDelete(e: CustomEvent<ComponentConfigMeta>) {
199
- const file = e.detail;
259
+ async function handleDelete(file: ComponentConfigMeta) {
200
260
  if (file.fileName === 'default') return;
201
261
  // Multi-step service flow (delete + reload-default-on-active-removal).
202
262
  // Silent by design — see handleLoad.
@@ -268,7 +328,7 @@
268
328
  title="Open {sourceFile} in VS Code"
269
329
  >
270
330
  <i class="fas fa-code"></i>
271
- <span>Source</span>
331
+ <span>Show component source</span>
272
332
  </a>
273
333
  {/if}
274
334
  </div>
@@ -300,16 +360,16 @@
300
360
  {component}
301
361
  {files}
302
362
  {activeFileName}
303
- on:save={handleSave}
304
- on:saveAs={openSaveAs}
305
- on:openLoad={refreshFiles}
306
- on:load={handleLoad}
307
- on:delete={handleDelete}
363
+ onsave={handleSave}
364
+ onsaveAs={openSaveAs}
365
+ onopenLoad={refreshFiles}
366
+ onload={handleLoad}
367
+ ondelete={handleDelete}
308
368
  />
309
369
  {#if resetVariables}
310
370
  <button
311
371
  class="cfm-btn reset-btn"
312
- on:click={handleReset}
372
+ onclick={handleReset}
313
373
  disabled={!resetDirty}
314
374
  title="Revert unsaved changes to {currentDisplayName}"
315
375
  >
@@ -342,7 +402,7 @@
342
402
  class:saving={productionUpdateStatus === 'updating'}
343
403
  class:saved={productionUpdateStatus === 'done'}
344
404
  class:error={productionUpdateStatus === 'error'}
345
- on:click={handleUpdateProduction}
405
+ onclick={handleUpdateProduction}
346
406
  disabled={productionUpdateStatus === 'updating' || !productionInfo || (productionInfo.fileName === activeFileName && !compDirty)}
347
407
  title={!productionInfo
348
408
  ? ''
@@ -359,6 +419,50 @@
359
419
  {#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
360
420
  </span>
361
421
  </button>
422
+ <button
423
+ type="button"
424
+ class="cfm-info-btn"
425
+ aria-label="About Save and Adopt"
426
+ aria-expanded={infoOpen}
427
+ bind:this={infoBtnEl}
428
+ onclick={() => (infoOpen = !infoOpen)}
429
+ >
430
+ <i class="fas fa-circle-info"></i>
431
+ </button>
432
+ {#if infoOpen}
433
+ <div
434
+ class="cfm-info-popover"
435
+ class:ready={infoPopoverReady}
436
+ role="dialog"
437
+ aria-label="About Save and Adopt"
438
+ bind:this={infoPopoverEl}
439
+ >
440
+ <header class="cfm-info-header">
441
+ <span class="cfm-info-title">Component Configuration</span>
442
+ <button
443
+ type="button"
444
+ class="cfm-info-close"
445
+ aria-label="Close"
446
+ onclick={() => (infoOpen = false)}
447
+ >
448
+ <i class="fas fa-xmark"></i>
449
+ </button>
450
+ </header>
451
+ <div class="cfm-info-body">
452
+ <p>
453
+ Editor and Prod both use a saved file. When they share the
454
+ <em>same</em> file, <strong>Saved changes</strong> go to into production
455
+ immediately. They are sharing the configuration.
456
+ </p>
457
+ <p>
458
+ To experiment without changing production,<strong>Save As</strong> a new file first.
459
+ </p>
460
+ <p>
461
+ When ready, click <strong>Adopt</strong> to use the new file on prod.
462
+ </p>
463
+ </div>
464
+ </div>
465
+ {/if}
362
466
  {#if adoptFeedback}
363
467
  <span class="cfm-feedback" aria-live="polite">{adoptFeedback}</span>
364
468
  {/if}
@@ -371,7 +475,7 @@
371
475
  bind:show={saveAsDialog}
372
476
  {currentDisplayName}
373
477
  {files}
374
- on:save={confirmSaveAs}
478
+ onsave={confirmSaveAs}
375
479
  />
376
480
 
377
481
  <style>
@@ -413,13 +517,15 @@
413
517
  .source-link {
414
518
  display: inline-flex;
415
519
  align-items: center;
416
- gap: var(--ui-space-4);
417
- padding: var(--ui-space-2) var(--ui-space-6);
520
+ gap: var(--ui-space-6);
521
+ height: 26px;
522
+ padding: 0 14px;
418
523
  font-size: var(--ui-font-size-xs);
524
+ font-weight: 500;
419
525
  color: var(--ui-text-secondary);
420
526
  text-decoration: none;
421
527
  border: 1px solid var(--ui-border-default);
422
- border-radius: var(--ui-radius-sm);
528
+ border-radius: 999px;
423
529
  transition: all var(--ui-transition-fast);
424
530
  }
425
531
 
@@ -553,8 +659,10 @@
553
659
  }
554
660
 
555
661
  /* actions cluster — sits directly next to the filename pill so the
556
- buttons stay near the input and don't drift under the open editor panel */
662
+ buttons stay near the input and don't drift under the open editor panel.
663
+ position: relative anchors the info popover below the cluster. */
557
664
  .cfm-actions {
665
+ position: relative;
558
666
  display: flex;
559
667
  align-items: center;
560
668
  gap: var(--ui-space-6);
@@ -674,6 +782,121 @@
674
782
  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
675
783
  }
676
784
 
785
+ /* info button — naked icon, no chrome. The icon itself carries the
786
+ affordance; hover/active simply brighten its color. */
787
+ .cfm-info-btn {
788
+ display: inline-flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ padding: var(--ui-space-2) var(--ui-space-4);
792
+ background: transparent;
793
+ border: 0;
794
+ color: var(--ui-text-tertiary);
795
+ font-size: 1.15rem;
796
+ line-height: 1;
797
+ cursor: pointer;
798
+ transition: color var(--ui-transition-fast);
799
+ }
800
+
801
+ .cfm-info-btn:hover,
802
+ .cfm-info-btn[aria-expanded='true'] {
803
+ color: var(--ui-text-primary);
804
+ }
805
+
806
+ .cfm-info-popover {
807
+ /* Fixed positioning escapes the sticky header's stacking context,
808
+ so the popover paints over the side panels. JS in this file
809
+ anchors it centered below the info button. */
810
+ position: fixed;
811
+ top: 0;
812
+ left: 0;
813
+ width: 22rem;
814
+ max-width: calc(100vw - var(--ui-space-24));
815
+ padding: 0;
816
+ background: var(--ui-surface-higher);
817
+ border: 1px solid var(--ui-border-medium);
818
+ border-radius: var(--ui-radius-lg);
819
+ box-shadow: var(--ui-shadow-lg);
820
+ z-index: 1000;
821
+ color: var(--ui-text-secondary);
822
+ font-family: var(--ui-font-family, system-ui, sans-serif);
823
+ overflow: hidden;
824
+ visibility: hidden;
825
+ animation: cfm-info-in 140ms ease-out;
826
+ }
827
+
828
+ .cfm-info-popover.ready {
829
+ visibility: visible;
830
+ }
831
+
832
+ .cfm-info-header {
833
+ display: flex;
834
+ align-items: center;
835
+ justify-content: space-between;
836
+ gap: var(--ui-space-8);
837
+ padding: var(--ui-space-10) var(--ui-space-12) var(--ui-space-10) var(--ui-space-16);
838
+ border-bottom: 1px solid var(--ui-border-subtle);
839
+ }
840
+
841
+ .cfm-info-title {
842
+ color: var(--ui-text-primary);
843
+ font-size: var(--ui-font-size-sm);
844
+ font-weight: var(--ui-font-weight-semibold);
845
+ letter-spacing: -0.01em;
846
+ line-height: 1.2;
847
+ }
848
+
849
+ .cfm-info-close {
850
+ display: inline-flex;
851
+ align-items: center;
852
+ justify-content: center;
853
+ width: var(--ui-space-24);
854
+ height: var(--ui-space-24);
855
+ padding: 0;
856
+ background: transparent;
857
+ border: 0;
858
+ border-radius: var(--ui-radius-sm);
859
+ color: var(--ui-text-tertiary);
860
+ font-size: var(--ui-font-size-xs);
861
+ line-height: 1;
862
+ cursor: pointer;
863
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
864
+ }
865
+
866
+ .cfm-info-close:hover {
867
+ color: var(--ui-text-primary);
868
+ background: var(--ui-hover);
869
+ }
870
+
871
+ .cfm-info-body {
872
+ padding: var(--ui-space-16);
873
+ }
874
+
875
+ .cfm-info-popover p {
876
+ margin: 0 0 var(--ui-space-12) 0;
877
+ font-size: var(--ui-font-size-xs);
878
+ line-height: 1.55;
879
+ }
880
+
881
+ .cfm-info-popover p:last-child {
882
+ margin-bottom: 0;
883
+ }
884
+
885
+ .cfm-info-popover strong {
886
+ color: var(--ui-text-primary);
887
+ font-weight: var(--ui-font-weight-semibold);
888
+ }
889
+
890
+ .cfm-info-popover em {
891
+ font-style: italic;
892
+ color: var(--ui-text-primary);
893
+ }
894
+
895
+ @keyframes cfm-info-in {
896
+ from { opacity: 0; transform: translateY(-3px); }
897
+ to { opacity: 1; transform: translateY(0); }
898
+ }
899
+
677
900
  /* narrow viewports: hide button text, keep icons visible */
678
901
  @media (max-width: 640px) {
679
902
  .cfm-btn span { display: none; }
@@ -1,27 +1,39 @@
1
1
  <script lang="ts">
2
- import { onMount, onDestroy, createEventDispatcher } from 'svelte';
2
+ import { stopPropagation } from 'svelte/legacy';
3
+
4
+ import { onMount, onDestroy } from 'svelte';
3
5
  import type { ComponentConfigMeta } from '../../lib/themeTypes';
4
6
  import UIDialog from '../../ui/UIDialog.svelte';
5
7
 
6
- /** Component slug used in the load-dialog title (e.g. "button"). */
7
- export let component: string;
8
- /** Files shown in the load dialog. */
9
- export let files: ComponentConfigMeta[] = [];
10
- /** Currently active file — highlighted in the load list. */
11
- export let activeFileName: string = 'default';
12
-
13
- const dispatch = createEventDispatcher<{
14
- save: void;
15
- saveAs: void;
8
+ interface Props {
9
+ /** Component slug used in the load-dialog title (e.g. "button"). */
10
+ component: string;
11
+ /** Files shown in the load dialog. */
12
+ files?: ComponentConfigMeta[];
13
+ /** Currently active file highlighted in the load list. */
14
+ activeFileName?: string;
15
+ onsave?: () => void;
16
+ onsaveAs?: () => void;
16
17
  /** Fired when the user clicks "Load…" in the menu — parent should refresh `files`. */
17
- openLoad: void;
18
- load: ComponentConfigMeta;
19
- delete: ComponentConfigMeta;
20
- }>();
18
+ onopenLoad?: () => void;
19
+ onload?: (file: ComponentConfigMeta) => void;
20
+ ondelete?: (file: ComponentConfigMeta) => void;
21
+ }
21
22
 
22
- let fileMenuOpen = false;
23
- let fileMenuRoot: HTMLElement;
24
- let showFileList = false;
23
+ let {
24
+ component,
25
+ files = [],
26
+ activeFileName = 'default',
27
+ onsave,
28
+ onsaveAs,
29
+ onopenLoad,
30
+ onload,
31
+ ondelete,
32
+ }: Props = $props();
33
+
34
+ let fileMenuOpen = $state(false);
35
+ let fileMenuRoot: HTMLElement | undefined = $state();
36
+ let showFileList = $state(false);
25
37
 
26
38
  onMount(() => {
27
39
  document.addEventListener('click', handleDocClick, true);
@@ -40,28 +52,28 @@
40
52
 
41
53
  function handleSave() {
42
54
  fileMenuOpen = false;
43
- dispatch('save');
55
+ onsave?.();
44
56
  }
45
57
 
46
58
  function handleSaveAs() {
47
59
  fileMenuOpen = false;
48
- dispatch('saveAs');
60
+ onsaveAs?.();
49
61
  }
50
62
 
51
63
  function handleOpenLoad() {
52
64
  fileMenuOpen = false;
53
65
  showFileList = true;
54
- dispatch('openLoad');
66
+ onopenLoad?.();
55
67
  }
56
68
 
57
69
  function handleLoad(file: ComponentConfigMeta) {
58
70
  showFileList = false;
59
- dispatch('load', file);
71
+ onload?.(file);
60
72
  }
61
73
 
62
74
  function handleDelete(file: ComponentConfigMeta) {
63
75
  if (file.fileName === 'default') return;
64
- dispatch('delete', file);
76
+ ondelete?.(file);
65
77
  }
66
78
  </script>
67
79
 
@@ -69,7 +81,7 @@
69
81
  <button
70
82
  class="cfm-btn"
71
83
  class:active={fileMenuOpen}
72
- on:click={() => (fileMenuOpen = !fileMenuOpen)}
84
+ onclick={() => (fileMenuOpen = !fileMenuOpen)}
73
85
  title="File menu"
74
86
  >
75
87
  <i class="fas fa-file"></i>
@@ -78,15 +90,15 @@
78
90
  </button>
79
91
  {#if fileMenuOpen}
80
92
  <div class="file-menu-dropdown" role="menu">
81
- <button class="file-menu-item" on:click={handleSave} role="menuitem">
93
+ <button class="file-menu-item" onclick={handleSave} role="menuitem">
82
94
  <i class="fas fa-save"></i>
83
95
  <span>Save</span>
84
96
  </button>
85
- <button class="file-menu-item" on:click={handleSaveAs} role="menuitem">
97
+ <button class="file-menu-item" onclick={handleSaveAs} role="menuitem">
86
98
  <i class="fas fa-copy"></i>
87
99
  <span>Save As…</span>
88
100
  </button>
89
- <button class="file-menu-item" on:click={handleOpenLoad} role="menuitem">
101
+ <button class="file-menu-item" onclick={handleOpenLoad} role="menuitem">
90
102
  <i class="fas fa-folder-open"></i>
91
103
  <span>Load…</span>
92
104
  </button>
@@ -103,7 +115,7 @@
103
115
  <div class="load-list">
104
116
  {#each files as file}
105
117
  <div class="load-item" class:active={file.fileName === activeFileName}>
106
- <button class="load-name-btn" on:click={() => handleLoad(file)}>
118
+ <button class="load-name-btn" onclick={() => handleLoad(file)}>
107
119
  {file.name}
108
120
  </button>
109
121
  {#if file.fileName === activeFileName}
@@ -112,7 +124,7 @@
112
124
  {#if file.fileName !== 'default'}
113
125
  <button
114
126
  class="file-delete-btn"
115
- on:click|stopPropagation={() => handleDelete(file)}
127
+ onclick={stopPropagation(() => handleDelete(file))}
116
128
  title="Delete {file.name}"
117
129
  >
118
130
  <i class="fas fa-trash-alt"></i>
@@ -2,14 +2,18 @@
2
2
  import type { ComponentSection } from './componentSectionType';
3
3
  import { defaultSections } from './defaultSections';
4
4
 
5
- export let sections: ComponentSection[] = defaultSections;
6
- export let selectedComponent: string = sections[0]?.id ?? '';
5
+ interface Props {
6
+ sections?: ComponentSection[];
7
+ selectedComponent?: string;
8
+ }
9
+
10
+ let { sections = defaultSections, selectedComponent = sections[0]?.id ?? '' }: Props = $props();
7
11
  </script>
8
12
 
9
13
  <div class="components-container">
10
14
  {#each sections as section (section.id)}
11
15
  {#if selectedComponent === section.id}
12
- <svelte:component this={section.component} {...(section.props ?? {})} />
16
+ <section.component {...(section.props ?? {})} />
13
17
  {/if}
14
18
  {/each}
15
19
  </div>