@motion-proto/live-tokens 0.3.7 → 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 (104) hide show
  1. package/README.md +1 -1
  2. package/package.json +11 -9
  3. package/src/component-editor/BadgeEditor.svelte +24 -22
  4. package/src/component-editor/CalloutEditor.svelte +3 -3
  5. package/src/component-editor/CardEditor.svelte +25 -21
  6. package/src/component-editor/CollapsibleSectionEditor.svelte +27 -25
  7. package/src/component-editor/CornerBadgeEditor.svelte +37 -35
  8. package/src/component-editor/DialogEditor.svelte +26 -24
  9. package/src/component-editor/ImageEditor.svelte +11 -9
  10. package/src/component-editor/InlineEditActionsEditor.svelte +17 -15
  11. package/src/component-editor/NotificationEditor.svelte +32 -30
  12. package/src/component-editor/ProgressBarEditor.svelte +3 -3
  13. package/src/component-editor/RadioButtonEditor.svelte +31 -29
  14. package/src/component-editor/SectionDividerEditor.svelte +30 -28
  15. package/src/component-editor/SegmentedControlEditor.svelte +29 -25
  16. package/src/component-editor/StandardButtonsEditor.svelte +42 -38
  17. package/src/component-editor/TabBarEditor.svelte +20 -18
  18. package/src/component-editor/TableEditor.svelte +4 -4
  19. package/src/component-editor/TooltipEditor.svelte +11 -9
  20. package/src/component-editor/registry.ts +2 -2
  21. package/src/component-editor/scaffolding/AngleDial.svelte +20 -19
  22. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +44 -20
  23. package/src/component-editor/scaffolding/ComponentFileManager.svelte +260 -37
  24. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +41 -29
  25. package/src/component-editor/scaffolding/ComponentsTab.svelte +7 -3
  26. package/src/component-editor/scaffolding/CopyFromMenu.svelte +21 -12
  27. package/src/component-editor/scaffolding/DemoHeader.svelte +13 -4
  28. package/src/component-editor/scaffolding/DividerEditor.svelte +27 -14
  29. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +10 -4
  30. package/src/component-editor/scaffolding/GradientCard.svelte +25 -20
  31. package/src/component-editor/scaffolding/LinkageChart.svelte +43 -34
  32. package/src/component-editor/scaffolding/LinkedBlock.svelte +24 -21
  33. package/src/component-editor/scaffolding/NonStylableConfig.svelte +6 -1
  34. package/src/component-editor/scaffolding/SaveAsDialog.svelte +39 -35
  35. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +21 -9
  36. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +8 -3
  37. package/src/component-editor/scaffolding/StateBlock.svelte +30 -13
  38. package/src/component-editor/scaffolding/TokenLayout.svelte +46 -30
  39. package/src/component-editor/scaffolding/TypeEditor.svelte +52 -26
  40. package/src/component-editor/scaffolding/VariantGroup.svelte +81 -48
  41. package/src/component-editor/scaffolding/componentSectionType.ts +2 -2
  42. package/src/components/Badge.svelte +45 -26
  43. package/src/components/Button.svelte +44 -21
  44. package/src/components/Callout.svelte +17 -12
  45. package/src/components/Card.svelte +23 -11
  46. package/src/components/CollapsibleSection.svelte +56 -27
  47. package/src/components/CornerBadge.svelte +32 -18
  48. package/src/components/Dialog.svelte +55 -31
  49. package/src/components/Image.svelte +14 -5
  50. package/src/components/InlineEditActions.svelte +22 -10
  51. package/src/components/Notification.svelte +39 -19
  52. package/src/components/ProgressBar.svelte +27 -17
  53. package/src/components/RadioButton.svelte +27 -10
  54. package/src/components/SectionDivider.svelte +34 -26
  55. package/src/components/SegmentedControl.svelte +23 -9
  56. package/src/components/TabBar.svelte +23 -10
  57. package/src/components/Table.svelte +8 -3
  58. package/src/components/Tooltip.svelte +15 -5
  59. package/src/lib/ColumnsOverlay.svelte +3 -3
  60. package/src/lib/LiveEditorOverlay.svelte +73 -36
  61. package/src/pages/ComponentEditorPage.svelte +17 -13
  62. package/src/pages/EditorShell.svelte +24 -20
  63. package/src/styles/form-controls.css +2 -2
  64. package/src/styles/tokens.css +59 -81
  65. package/src/ui/BezierCurveEditor.svelte +59 -43
  66. package/src/ui/ColorEditPanel.svelte +71 -44
  67. package/src/ui/EditorViewSwitcher.svelte +9 -5
  68. package/src/ui/FontStackEditor.svelte +16 -15
  69. package/src/ui/GradientEditor.svelte +42 -33
  70. package/src/ui/GradientStopPicker.svelte +18 -29
  71. package/src/ui/PaletteEditor.svelte +238 -212
  72. package/src/ui/PresetFileManager.svelte +20 -18
  73. package/src/ui/ProjectFontsSection.svelte +30 -30
  74. package/src/ui/SurfacesTab.svelte +3 -3
  75. package/src/ui/TextTab.svelte +2 -2
  76. package/src/ui/ThemeFileManager.svelte +38 -35
  77. package/src/ui/Toggle.svelte +11 -9
  78. package/src/ui/UICopyPopover.svelte +19 -15
  79. package/src/ui/UIDialog.svelte +48 -30
  80. package/src/ui/UIFontFamilySelector.svelte +104 -78
  81. package/src/ui/UIFontSizeSelector.svelte +38 -20
  82. package/src/ui/UIFontWeightSelector.svelte +33 -13
  83. package/src/ui/UILineHeightSelector.svelte +33 -13
  84. package/src/ui/UILinkToggle.svelte +7 -6
  85. package/src/ui/UIOptionItem.svelte +21 -7
  86. package/src/ui/UIOptionList.svelte +9 -3
  87. package/src/ui/UIPaddingSelector.svelte +108 -82
  88. package/src/ui/UIPaletteSelector.svelte +186 -161
  89. package/src/ui/UIRadio.svelte +23 -8
  90. package/src/ui/UIRadioGroup.svelte +9 -8
  91. package/src/ui/UIRelinkConfirmPopover.svelte +26 -16
  92. package/src/ui/UITokenSelector.svelte +112 -68
  93. package/src/ui/UIVariantSelector.svelte +79 -57
  94. package/src/ui/VariablesTab.svelte +15 -15
  95. package/src/ui/palette/GradientStopEditor.svelte +45 -26
  96. package/src/ui/palette/OverridesPanel.svelte +85 -49
  97. package/src/ui/palette/PaletteBase.svelte +60 -32
  98. package/src/ui/palette/ScaleCurveEditor.svelte +25 -10
  99. package/src/ui/sections/ColumnsSection.svelte +13 -13
  100. package/src/ui/sections/GradientsSection.svelte +12 -9
  101. package/src/ui/sections/OverlaysSection.svelte +50 -47
  102. package/src/ui/sections/ShadowsSection.svelte +110 -104
  103. package/src/ui/sections/TokenScaleTable.svelte +38 -22
  104. package/src/ui/sections/tokenScales.ts +2 -2
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { stopPropagation } from 'svelte/legacy';
3
+
2
4
  import { onMount } from 'svelte';
3
5
  import type { PresetMeta } from '../lib/themeTypes';
4
6
  import {
@@ -12,17 +14,17 @@
12
14
  import { dirty } from '../lib/editorStore';
13
15
  import UIDialog from './UIDialog.svelte';
14
16
 
15
- let files: PresetMeta[] = [];
16
- let showFileList = false;
17
- let saveAsEditing = false;
18
- let saveAsName = '';
19
- let saveAsInput: HTMLInputElement;
17
+ let files: PresetMeta[] = $state([]);
18
+ let showFileList = $state(false);
19
+ let saveAsEditing = $state(false);
20
+ let saveAsName = $state('');
21
+ let saveAsInput: HTMLInputElement | undefined = $state();
20
22
 
21
- let activeFileName = 'default';
22
- let currentDisplayName = 'Default';
23
+ let activeFileName = $state('default');
24
+ let currentDisplayName = $state('Default');
23
25
 
24
- let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = 'idle';
25
- let applyStatus: 'idle' | 'applying' = 'idle';
26
+ let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
27
+ let applyStatus: 'idle' | 'applying' = $state('idle');
26
28
 
27
29
  async function refreshFiles() {
28
30
  try {
@@ -186,7 +188,7 @@
186
188
  class:saving={saveStatus === 'saving'}
187
189
  class:saved={saveStatus === 'saved'}
188
190
  class:error={saveStatus === 'error'}
189
- on:click={handleSave}
191
+ onclick={handleSave}
190
192
  disabled={saveStatus === 'saving' || applyStatus === 'applying'}
191
193
  title="Capture the current theme + component configs into this preset"
192
194
  >
@@ -203,7 +205,7 @@
203
205
  </button>
204
206
  <button
205
207
  class="pfm-btn increment-btn"
206
- on:click={handleSaveIncrement}
208
+ onclick={handleSaveIncrement}
207
209
  disabled={saveStatus === 'saving' || applyStatus === 'applying'}
208
210
  title="Save as incremented preset"
209
211
  >
@@ -218,25 +220,25 @@
218
220
  type="text"
219
221
  bind:value={saveAsName}
220
222
  bind:this={saveAsInput}
221
- on:keydown={handleSaveAsKeydown}
223
+ onkeydown={handleSaveAsKeydown}
222
224
  placeholder="Preset name..."
223
225
  />
224
226
  <div class="save-as-actions">
225
227
  <button
226
228
  class="inline-btn confirm-btn"
227
- on:click={confirmSaveAs}
229
+ onclick={confirmSaveAs}
228
230
  disabled={!saveAsName.trim()}
229
231
  title="Save"
230
232
  >
231
233
  <i class="fas fa-check"></i>
232
234
  </button>
233
- <button class="inline-btn cancel-btn" on:click={cancelSaveAs} title="Cancel">
235
+ <button class="inline-btn cancel-btn" onclick={cancelSaveAs} title="Cancel">
234
236
  <i class="fas fa-times"></i>
235
237
  </button>
236
238
  </div>
237
239
  </div>
238
240
  {:else}
239
- <button class="pfm-btn" on:click={openSaveAs} title="Save as new preset">
241
+ <button class="pfm-btn" onclick={openSaveAs} title="Save as new preset">
240
242
  <i class="fas fa-copy"></i>
241
243
  <span>Save As</span>
242
244
  </button>
@@ -245,7 +247,7 @@
245
247
  <button
246
248
  class="pfm-btn"
247
249
  class:active={showFileList}
248
- on:click={toggleFileList}
250
+ onclick={toggleFileList}
249
251
  disabled={applyStatus === 'applying'}
250
252
  title="Load a preset"
251
253
  >
@@ -263,7 +265,7 @@
263
265
  <div class="load-list">
264
266
  {#each files as file}
265
267
  <div class="load-item" class:active={file.fileName === activeFileName}>
266
- <button class="load-name-btn" on:click={() => handleApply(file)}>
268
+ <button class="load-name-btn" onclick={() => handleApply(file)}>
267
269
  {file.name}
268
270
  </button>
269
271
  {#if file.fileName === activeFileName}
@@ -272,7 +274,7 @@
272
274
  {#if file.fileName !== 'default'}
273
275
  <button
274
276
  class="file-delete-btn"
275
- on:click|stopPropagation={() => handleDelete(file)}
277
+ onclick={stopPropagation(() => handleDelete(file))}
276
278
  title="Delete {file.name}"
277
279
  >
278
280
  <i class="fas fa-trash-alt"></i>
@@ -15,28 +15,28 @@
15
15
  const GOOGLE_FONTS: GoogleFontEntry[] = (googleFontsData as any).fonts;
16
16
 
17
17
  type AddMode = 'closed' | 'google' | 'url' | 'fontface';
18
- let addMode: AddMode = 'closed';
18
+ let addMode: AddMode = $state('closed');
19
19
 
20
20
  // Google search
21
- let googleQuery = '';
22
- $: googleMatches = googleQuery.trim().length >= 1
21
+ let googleQuery = $state('');
22
+ let googleMatches = $derived(googleQuery.trim().length >= 1
23
23
  ? GOOGLE_FONTS
24
24
  .filter((f) => f.family.toLowerCase().includes(googleQuery.toLowerCase()))
25
25
  .slice(0, 20)
26
- : GOOGLE_FONTS.slice(0, 10);
26
+ : GOOGLE_FONTS.slice(0, 10));
27
27
 
28
28
  // URL paste
29
- let urlInput = '';
30
- let urlError = '';
31
- let urlDiscovering = false;
32
- let urlParsed: ParsedFamily[] | null = null;
33
- let urlPickedNames = new Set<string>();
34
- let urlNeedsManualFamilies = false;
35
- let urlManualFamilies = '';
29
+ let urlInput = $state('');
30
+ let urlError = $state('');
31
+ let urlDiscovering = $state(false);
32
+ let urlParsed: ParsedFamily[] | null = $state(null);
33
+ let urlPickedNames = $state(new Set<string>());
34
+ let urlNeedsManualFamilies = $state(false);
35
+ let urlManualFamilies = $state('');
36
36
 
37
37
  // @font-face paste
38
- let fontFaceText = '';
39
- let fontFaceParsed: ParsedFamily[] = [];
38
+ let fontFaceText = $state('');
39
+ let fontFaceParsed: ParsedFamily[] = $state([]);
40
40
 
41
41
  function reset() {
42
42
  addMode = 'closed';
@@ -52,8 +52,8 @@
52
52
  fontFaceParsed = [];
53
53
  }
54
54
 
55
- $: fontSourcesList = $editorState.fonts.sources;
56
- $: fontStacksList = $editorState.fonts.stacks;
55
+ let fontSourcesList = $derived($editorState.fonts.sources);
56
+ let fontStacksList = $derived($editorState.fonts.stacks);
57
57
 
58
58
  function commitSources(next: FontSource[]) {
59
59
  setFontSources(next);
@@ -161,7 +161,7 @@
161
161
  .map((s) => s.variable);
162
162
  }
163
163
 
164
- let expanded = new Set<string>();
164
+ let expanded = $state(new Set<string>());
165
165
  function toggleExpanded(familyId: string) {
166
166
  const next = new Set(expanded);
167
167
  if (next.has(familyId)) next.delete(familyId); else next.add(familyId);
@@ -200,7 +200,7 @@
200
200
  type="button"
201
201
  class="pf-family-disclosure"
202
202
  class:open={isOpen}
203
- on:click={() => toggleExpanded(fam.id)}
203
+ onclick={() => toggleExpanded(fam.id)}
204
204
  aria-label={isOpen ? 'Collapse weights' : 'Expand weights'}
205
205
  aria-expanded={isOpen}
206
206
  ><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
@@ -212,7 +212,7 @@
212
212
  <button
213
213
  type="button"
214
214
  class="pf-family-remove"
215
- on:click={() => removeFamily(source.id, fam.id)}
215
+ onclick={() => removeFamily(source.id, fam.id)}
216
216
  aria-label={`Remove ${fam.name}`}
217
217
  title="Remove family"
218
218
  ><i class="fas fa-xmark" aria-hidden="true"></i></button>
@@ -239,14 +239,14 @@
239
239
  </div>
240
240
 
241
241
  {#if addMode === 'closed'}
242
- <button type="button" class="pf-add-toggle" on:click={() => (addMode = 'google')}>+ add font</button>
242
+ <button type="button" class="pf-add-toggle" onclick={() => (addMode = 'google')}>+ add font</button>
243
243
  {:else}
244
244
  <div class="pf-add-panel">
245
245
  <div class="pf-add-tabs">
246
- <button type="button" class:active={addMode === 'google'} on:click={() => (addMode = 'google')}>Google search</button>
247
- <button type="button" class:active={addMode === 'url'} on:click={() => (addMode = 'url')}>Paste URL</button>
248
- <button type="button" class:active={addMode === 'fontface'} on:click={() => (addMode = 'fontface')}>@font-face</button>
249
- <button type="button" class="pf-add-close" on:click={reset} aria-label="Cancel">×</button>
246
+ <button type="button" class:active={addMode === 'google'} onclick={() => (addMode = 'google')}>Google search</button>
247
+ <button type="button" class:active={addMode === 'url'} onclick={() => (addMode = 'url')}>Paste URL</button>
248
+ <button type="button" class:active={addMode === 'fontface'} onclick={() => (addMode = 'fontface')}>@font-face</button>
249
+ <button type="button" class="pf-add-close" onclick={reset} aria-label="Cancel">×</button>
250
250
  </div>
251
251
 
252
252
  {#if addMode === 'google'}
@@ -261,7 +261,7 @@
261
261
  <li class="pf-result">
262
262
  <span class="pf-result-preview" style="font-family: '{m.family}', {m.category};">{m.family}</span>
263
263
  <span class="pf-result-meta">{m.category} · {m.variants.length}w</span>
264
- <button type="button" class="pf-result-add" on:click={() => addGoogleFont(m)}>add</button>
264
+ <button type="button" class="pf-result-add" onclick={() => addGoogleFont(m)}>add</button>
265
265
  </li>
266
266
  {/each}
267
267
  {#if googleMatches.length === 0}
@@ -276,7 +276,7 @@
276
276
  bind:value={urlInput}
277
277
  />
278
278
  <div class="pf-row">
279
- <button type="button" class="pf-btn" on:click={discoverUrl} disabled={!urlInput.trim() || urlDiscovering}>
279
+ <button type="button" class="pf-btn" onclick={discoverUrl} disabled={!urlInput.trim() || urlDiscovering}>
280
280
  {urlDiscovering ? 'Checking…' : 'Detect families'}
281
281
  </button>
282
282
  </div>
@@ -290,7 +290,7 @@
290
290
  <input
291
291
  type="checkbox"
292
292
  checked={urlPickedNames.has(f.name)}
293
- on:change={(e) => {
293
+ onchange={(e) => {
294
294
  const target = e.currentTarget;
295
295
  const s = new Set(urlPickedNames);
296
296
  if (target.checked) s.add(f.name); else s.delete(f.name);
@@ -305,7 +305,7 @@
305
305
  </li>
306
306
  {/each}
307
307
  </ul>
308
- <button type="button" class="pf-btn primary" on:click={addUrlSource}>Add {urlPickedNames.size} selected</button>
308
+ <button type="button" class="pf-btn primary" onclick={addUrlSource}>Add {urlPickedNames.size} selected</button>
309
309
  {:else if urlNeedsManualFamilies}
310
310
  <div class="pf-detected">Couldn't auto-detect families (CORS or no metadata). Name them:</div>
311
311
  <input
@@ -314,7 +314,7 @@
314
314
  placeholder="Comma-separated family names"
315
315
  bind:value={urlManualFamilies}
316
316
  />
317
- <button type="button" class="pf-btn primary" on:click={addUrlSource} disabled={!urlManualFamilies.trim()}>Add</button>
317
+ <button type="button" class="pf-btn primary" onclick={addUrlSource} disabled={!urlManualFamilies.trim()}>Add</button>
318
318
  {/if}
319
319
  {:else if addMode === 'fontface'}
320
320
  <textarea
@@ -322,12 +322,12 @@
322
322
  placeholder={'Paste one or more @font-face { ... } rules'}
323
323
  rows="6"
324
324
  bind:value={fontFaceText}
325
- on:input={parseFontFaceTextInput}
325
+ oninput={parseFontFaceTextInput}
326
326
  ></textarea>
327
327
  {#if fontFaceParsed.length > 0}
328
328
  <div class="pf-detected">Detected: {fontFaceParsed.map((f) => f.name).join(', ')}</div>
329
329
  {/if}
330
- <button type="button" class="pf-btn primary" on:click={addFontFaceSource} disabled={fontFaceParsed.length === 0}>Add</button>
330
+ <button type="button" class="pf-btn primary" onclick={addFontFaceSource} disabled={fontFaceParsed.length === 0}>Add</button>
331
331
  {/if}
332
332
  </div>
333
333
  {/if}
@@ -126,7 +126,7 @@
126
126
  }
127
127
  ];
128
128
 
129
- let copiedVar: string | null = null;
129
+ let copiedVar: string | null = $state(null);
130
130
  function copyVariable(v: string) {
131
131
  navigator.clipboard.writeText(v);
132
132
  copiedVar = v;
@@ -222,7 +222,7 @@
222
222
  class="swatch-box"
223
223
  style="background: var({swatch.variable});"
224
224
  ></div>
225
- <button class="swatch-name copyable" class:copied={copiedVar === swatch.variable} on:click={() => copyVariable(swatch.variable)}>{copiedVar === swatch.variable ? 'copied!' : swatch.name}</button>
225
+ <button class="swatch-name copyable" class:copied={copiedVar === swatch.variable} onclick={() => copyVariable(swatch.variable)}>{copiedVar === swatch.variable ? 'copied!' : swatch.name}</button>
226
226
  {:else}
227
227
  <div class="swatch-box empty"></div>
228
228
  <div class="swatch-name">&nbsp;</div>
@@ -248,7 +248,7 @@
248
248
  class="border-box"
249
249
  style="border: 2px solid var({border.variable});"
250
250
  ></div>
251
- <button class="border-name copyable" class:copied={copiedVar === border.variable} on:click={() => copyVariable(border.variable)}>{copiedVar === border.variable ? 'copied!' : border.name}</button>
251
+ <button class="border-name copyable" class:copied={copiedVar === border.variable} onclick={() => copyVariable(border.variable)}>{copiedVar === border.variable ? 'copied!' : border.name}</button>
252
252
  </div>
253
253
  {/each}
254
254
  </div>
@@ -78,7 +78,7 @@
78
78
  }
79
79
  ];
80
80
 
81
- let copiedVar: string | null = null;
81
+ let copiedVar: string | null = $state(null);
82
82
  function copyVariable(v: string) {
83
83
  navigator.clipboard.writeText(v);
84
84
  copiedVar = v;
@@ -100,7 +100,7 @@
100
100
  Ag
101
101
  </div>
102
102
  <div class="text-color-info">
103
- <button class="text-color-name copyable" class:copied={copiedVar === color.variable} on:click={() => copyVariable(color.variable)}>{copiedVar === color.variable ? 'copied!' : color.name}</button>
103
+ <button class="text-color-name copyable" class:copied={copiedVar === color.variable} onclick={() => copyVariable(color.variable)}>{copiedVar === color.variable ? 'copied!' : color.name}</button>
104
104
  <div class="text-color-variable">{color.variable}</div>
105
105
  <div class="text-color-description">{color.description}</div>
106
106
  </div>
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, onMount } from 'svelte';
2
+ import { stopPropagation } from 'svelte/legacy';
3
+
4
+ import { onMount } from 'svelte';
3
5
  import type { ThemeMeta } from '../lib/themeTypes';
4
6
  import { listThemes, deleteTheme, setActiveFile, sanitizeFileName, getProductionInfo, setProductionFile } from '../lib/themeService';
5
7
  import type { ProductionInfo } from '../lib/themeService';
@@ -7,23 +9,24 @@
7
9
  import { dirty } from '../lib/editorStore';
8
10
  import UIDialog from './UIDialog.svelte';
9
11
 
10
- const dispatch = createEventDispatcher<{
11
- save: { fileName: string; displayName: string };
12
- load: { fileName: string };
13
- }>();
12
+ interface Props {
13
+ saveStatus?: 'idle' | 'saving' | 'saved' | 'error';
14
+ onsave?: (payload: { fileName: string; displayName: string }) => void;
15
+ onload?: (payload: { fileName: string }) => void;
16
+ }
14
17
 
15
- export let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = 'idle';
18
+ let { saveStatus = 'idle', onsave, onload }: Props = $props();
16
19
 
17
- let files: ThemeMeta[] = [];
18
- let showFileList = false;
19
- let saveAsEditing = false;
20
- let saveAsName = '';
21
- let saveAsInput: HTMLInputElement;
22
- let currentDisplayName = 'Default';
20
+ let files: ThemeMeta[] = $state([]);
21
+ let showFileList = $state(false);
22
+ let saveAsEditing = $state(false);
23
+ let saveAsName = $state('');
24
+ let saveAsInput: HTMLInputElement | undefined = $state();
25
+ let currentDisplayName = $state('Default');
23
26
 
24
27
  // --- Production state ---
25
- let productionInfo: ProductionInfo | null = null;
26
- let productionUpdateStatus: 'idle' | 'updating' | 'done' | 'error' = 'idle';
28
+ let productionInfo: ProductionInfo | null = $state(null);
29
+ let productionUpdateStatus: 'idle' | 'updating' | 'done' | 'error' = $state('idle');
27
30
 
28
31
  async function refreshFiles() {
29
32
  try {
@@ -65,7 +68,7 @@
65
68
  });
66
69
 
67
70
  function handleSave() {
68
- dispatch('save', { fileName: $activeFileName, displayName: currentDisplayName });
71
+ onsave?.({ fileName: $activeFileName, displayName: currentDisplayName });
69
72
  }
70
73
 
71
74
  function handleSaveIncrement() {
@@ -83,7 +86,7 @@
83
86
  const displayName = `${baseName}_${suffix}`;
84
87
  const fileName = `${baseFileName}_${suffix}`;
85
88
 
86
- dispatch('save', { fileName, displayName });
89
+ onsave?.({ fileName, displayName });
87
90
  $activeFileName = fileName;
88
91
  currentDisplayName = displayName;
89
92
  setTimeout(() => refreshFiles(), 500);
@@ -106,7 +109,7 @@
106
109
  return;
107
110
  }
108
111
  saveAsEditing = false;
109
- dispatch('save', { fileName, displayName });
112
+ onsave?.({ fileName, displayName });
110
113
  $activeFileName = fileName;
111
114
  currentDisplayName = displayName;
112
115
  setTimeout(() => refreshFiles(), 500);
@@ -122,7 +125,7 @@
122
125
  await setActiveFile(file.fileName);
123
126
  $activeFileName = file.fileName;
124
127
  currentDisplayName = file.name;
125
- dispatch('load', { fileName: file.fileName });
128
+ onload?.({ fileName: file.fileName });
126
129
  // editorStore.loadFromFile clears history and resets dirty — no snapshot needed here.
127
130
  }
128
131
 
@@ -135,7 +138,7 @@
135
138
  if (file.fileName === $activeFileName) {
136
139
  $activeFileName = 'default';
137
140
  currentDisplayName = 'Default';
138
- dispatch('load', { fileName: 'default' });
141
+ onload?.({ fileName: 'default' });
139
142
  }
140
143
  } catch {
141
144
  // silent
@@ -168,8 +171,8 @@
168
171
  }
169
172
 
170
173
  type SortKey = 'name' | 'updatedAt';
171
- let sortKey: SortKey = 'updatedAt';
172
- let sortDir: 'asc' | 'desc' = 'desc';
174
+ let sortKey: SortKey = $state('updatedAt');
175
+ let sortDir: 'asc' | 'desc' = $state('desc');
173
176
 
174
177
  function toggleSort(key: SortKey) {
175
178
  if (sortKey === key) {
@@ -181,7 +184,7 @@
181
184
  }
182
185
  }
183
186
 
184
- $: sortedFiles = [...files].sort((a, b) => {
187
+ let sortedFiles = $derived([...files].sort((a, b) => {
185
188
  let cmp = 0;
186
189
  if (sortKey === 'name') {
187
190
  cmp = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
@@ -189,7 +192,7 @@
189
192
  cmp = (a.updatedAt || '').localeCompare(b.updatedAt || '');
190
193
  }
191
194
  return sortDir === 'asc' ? cmp : -cmp;
192
- });
195
+ }));
193
196
  </script>
194
197
 
195
198
  <div class="theme-file-manager">
@@ -206,7 +209,7 @@
206
209
  class:saving={saveStatus === 'saving'}
207
210
  class:saved={saveStatus === 'saved'}
208
211
  class:error={saveStatus === 'error'}
209
- on:click={handleSave}
212
+ onclick={handleSave}
210
213
  disabled={saveStatus === 'saving'}
211
214
  title="Save to current file"
212
215
  >
@@ -217,7 +220,7 @@
217
220
  </button>
218
221
  <button
219
222
  class="tfm-btn increment-btn"
220
- on:click={handleSaveIncrement}
223
+ onclick={handleSaveIncrement}
221
224
  disabled={saveStatus === 'saving'}
222
225
  title="Save as incremented copy"
223
226
  >
@@ -232,14 +235,14 @@
232
235
  type="text"
233
236
  bind:value={saveAsName}
234
237
  bind:this={saveAsInput}
235
- on:keydown={handleSaveAsKeydown}
238
+ onkeydown={handleSaveAsKeydown}
236
239
  placeholder="Theme name..."
237
240
  />
238
241
  <div class="save-as-actions">
239
- <button class="inline-btn confirm-btn" on:click={confirmSaveAs} disabled={!saveAsName.trim()} title="Save">
242
+ <button class="inline-btn confirm-btn" onclick={confirmSaveAs} disabled={!saveAsName.trim()} title="Save">
240
243
  <i class="fas fa-check"></i>
241
244
  </button>
242
- <button class="inline-btn cancel-btn" on:click={cancelSaveAs} title="Cancel">
245
+ <button class="inline-btn cancel-btn" onclick={cancelSaveAs} title="Cancel">
243
246
  <i class="fas fa-times"></i>
244
247
  </button>
245
248
  </div>
@@ -247,7 +250,7 @@
247
250
  {:else}
248
251
  <button
249
252
  class="tfm-btn"
250
- on:click={openSaveAs}
253
+ onclick={openSaveAs}
251
254
  title="Save as new file"
252
255
  >
253
256
  <i class="fas fa-copy"></i>
@@ -258,7 +261,7 @@
258
261
  <button
259
262
  class="tfm-btn"
260
263
  class:active={showFileList}
261
- on:click={toggleFileList}
264
+ onclick={toggleFileList}
262
265
  title="Load a theme"
263
266
  >
264
267
  <i class="fas fa-folder-open"></i>
@@ -287,7 +290,7 @@
287
290
  class:updating={productionUpdateStatus === 'updating'}
288
291
  class:done={productionUpdateStatus === 'done'}
289
292
  class:error={productionUpdateStatus === 'error'}
290
- on:click={handleUpdateProduction}
293
+ onclick={handleUpdateProduction}
291
294
  disabled={productionUpdateStatus === 'updating' || (productionInfo?.fileName === $activeFileName)}
292
295
  title={productionInfo?.fileName === $activeFileName ? 'Already in production' : `Set "${currentDisplayName}" as production`}
293
296
  >
@@ -317,7 +320,7 @@
317
320
  <button
318
321
  class="sort-btn name-col"
319
322
  class:active-sort={sortKey === 'name'}
320
- on:click={() => toggleSort('name')}
323
+ onclick={() => toggleSort('name')}
321
324
  >
322
325
  <span>Name</span>
323
326
  {#if sortKey === 'name'}
@@ -327,7 +330,7 @@
327
330
  <button
328
331
  class="sort-btn date-col"
329
332
  class:active-sort={sortKey === 'updatedAt'}
330
- on:click={() => toggleSort('updatedAt')}
333
+ onclick={() => toggleSort('updatedAt')}
331
334
  >
332
335
  <span>Date</span>
333
336
  {#if sortKey === 'updatedAt'}
@@ -338,7 +341,7 @@
338
341
  </div>
339
342
  {#each sortedFiles as file}
340
343
  <div class="load-item" class:active={file.fileName === $activeFileName}>
341
- <button class="load-name-btn" on:click={() => handleLoad(file)}>
344
+ <button class="load-name-btn" onclick={() => handleLoad(file)}>
342
345
  {file.name}
343
346
  </button>
344
347
  <span class="updated-at" title={file.updatedAt}>{formatUpdatedAt(file.updatedAt)}</span>
@@ -348,7 +351,7 @@
348
351
  {#if file.fileName !== 'default'}
349
352
  <button
350
353
  class="file-delete-btn"
351
- on:click|stopPropagation={() => handleDelete(file)}
354
+ onclick={stopPropagation(() => handleDelete(file))}
352
355
  title="Delete {file.name}"
353
356
  >
354
357
  <i class="fas fa-trash-alt"></i>
@@ -1,16 +1,17 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from 'svelte';
3
-
4
- export let checked: boolean = false;
5
- export let disabled: boolean = false;
6
- export let label: string = '';
2
+ interface Props {
3
+ checked?: boolean;
4
+ disabled?: boolean;
5
+ label?: string;
6
+ onchange?: (checked: boolean) => void;
7
+ }
7
8
 
8
- const dispatch = createEventDispatcher<{ change: boolean }>();
9
+ let { checked = $bindable(false), disabled = false, label = '', onchange }: Props = $props();
9
10
 
10
11
  function toggle() {
11
12
  if (disabled) return;
12
13
  checked = !checked;
13
- dispatch('change', checked);
14
+ onchange?.(checked);
14
15
  }
15
16
  </script>
16
17
 
@@ -19,12 +20,13 @@
19
20
  type="button"
20
21
  role="switch"
21
22
  aria-checked={checked}
23
+ aria-label={label || 'Toggle'}
22
24
  {disabled}
23
25
  class="toggle-track"
24
26
  class:on={checked}
25
- on:click={toggle}
27
+ onclick={toggle}
26
28
  >
27
- <span class="toggle-thumb" />
29
+ <span class="toggle-thumb"></span>
28
30
  </button>
29
31
  {#if label}
30
32
  <span class="toggle-label">{label}</span>
@@ -1,25 +1,29 @@
1
1
  <script lang="ts">
2
+ import { run } from 'svelte/legacy';
3
+
2
4
  import { copyPopover } from '../lib/copyPopover';
3
5
 
4
- let bubbleEl: HTMLDivElement | null = null;
5
- let bubbleW = 0;
6
- let bubbleH = 0;
6
+ let bubbleEl: HTMLDivElement | null = $state(null);
7
+ let bubbleW = $state(0);
8
+ let bubbleH = $state(0);
7
9
 
8
- $: if (bubbleEl && $copyPopover.visible) {
9
- const r = bubbleEl.getBoundingClientRect();
10
- bubbleW = r.width;
11
- bubbleH = r.height;
12
- }
10
+ run(() => {
11
+ if (bubbleEl && $copyPopover.visible) {
12
+ const r = bubbleEl.getBoundingClientRect();
13
+ bubbleW = r.width;
14
+ bubbleH = r.height;
15
+ }
16
+ });
13
17
 
14
18
  const GAP = 8;
15
19
 
16
- $: anchor = $copyPopover.anchor;
17
- $: rawLeft = anchor ? anchor.left + anchor.width / 2 - bubbleW / 2 : 0;
18
- $: rawTop = anchor ? anchor.top - bubbleH - GAP : 0;
19
- $: maxLeft = typeof window !== 'undefined' ? window.innerWidth - bubbleW - 4 : rawLeft;
20
- $: clampedLeft = Math.max(4, Math.min(rawLeft, maxLeft));
21
- $: clampedTop = Math.max(4, rawTop);
22
- $: arrowLeft = anchor ? anchor.left + anchor.width / 2 - clampedLeft : bubbleW / 2;
20
+ let anchor = $derived($copyPopover.anchor);
21
+ let rawLeft = $derived(anchor ? anchor.left + anchor.width / 2 - bubbleW / 2 : 0);
22
+ let rawTop = $derived(anchor ? anchor.top - bubbleH - GAP : 0);
23
+ let maxLeft = $derived(typeof window !== 'undefined' ? window.innerWidth - bubbleW - 4 : rawLeft);
24
+ let clampedLeft = $derived(Math.max(4, Math.min(rawLeft, maxLeft)));
25
+ let clampedTop = $derived(Math.max(4, rawTop));
26
+ let arrowLeft = $derived(anchor ? anchor.left + anchor.width / 2 - clampedLeft : bubbleW / 2);
23
27
  </script>
24
28
 
25
29
  {#if $copyPopover.visible && anchor}