@motion-proto/live-tokens 0.6.2 → 0.8.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 (232) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +854 -226
  3. package/dist-plugin/index.d.cts +2 -1
  4. package/dist-plugin/index.d.ts +2 -1
  5. package/dist-plugin/index.js +852 -225
  6. package/package.json +26 -40
  7. package/src/{styles → app}/site.css +1 -1
  8. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  9. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  10. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  11. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
  12. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  13. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  14. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  15. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  16. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  17. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
  18. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  19. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  20. package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
  21. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  22. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
  23. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  24. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  25. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  26. package/src/editor/component-editor/editors.d.ts +10 -0
  27. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  28. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
  31. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  32. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  33. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  34. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  35. package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
  36. package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -12
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  39. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  40. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
  41. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  42. package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
  43. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
  44. package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
  46. package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
  47. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  48. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  49. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
  50. package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
  51. package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
  52. package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
  53. package/src/editor/core/flashStatus.ts +30 -0
  54. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  55. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  56. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  57. package/src/editor/core/manifests/manifestService.ts +171 -0
  58. package/src/editor/core/palettes/familySwap.ts +99 -0
  59. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
  60. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
  61. package/src/editor/core/productionPulse.ts +37 -0
  62. package/src/{lib → editor/core/routing}/router.ts +1 -1
  63. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  64. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  65. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  66. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  67. package/src/{lib → editor/core/store}/editorStore.ts +222 -28
  68. package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
  69. package/src/editor/core/store/gradientSource.ts +192 -0
  70. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  71. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  72. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  73. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  74. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  75. package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
  76. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  77. package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
  78. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  79. package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
  80. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  81. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  82. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  83. package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
  84. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  85. package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
  86. package/src/editor/index.ts +69 -0
  87. package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
  88. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
  89. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  90. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  91. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  92. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  93. package/src/{styles → editor/styles}/ui-editor.css +43 -22
  94. package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
  95. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
  96. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  97. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
  98. package/src/editor/ui/FileLoadList.svelte +367 -0
  99. package/src/editor/ui/FilePill.svelte +80 -0
  100. package/src/editor/ui/FontStackEditor.svelte +499 -0
  101. package/src/editor/ui/GradientEditor.svelte +690 -0
  102. package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
  103. package/src/editor/ui/ManifestFileManager.svelte +438 -0
  104. package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
  105. package/src/editor/ui/ProjectFontsSection.svelte +638 -0
  106. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  107. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  108. package/src/editor/ui/ThemeFileManager.svelte +783 -0
  109. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  110. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
  111. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
  112. package/src/editor/ui/UIInfoPopover.svelte +243 -0
  113. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  114. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  115. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  116. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  117. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
  118. package/src/editor/ui/UIPillButton.svelte +168 -0
  119. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  120. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  121. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  122. package/src/editor/ui/UISquareButton.svelte +172 -0
  123. package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
  124. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  125. package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
  126. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  127. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
  128. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
  129. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  130. package/src/editor/ui/palette/paletteMath.ts +275 -0
  131. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
  132. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
  133. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
  134. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
  135. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  136. package/src/{components → system/components}/Badge.svelte +0 -36
  137. package/src/{components → system/components}/Button.svelte +2 -2
  138. package/src/{components → system/components}/Card.svelte +34 -60
  139. package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
  140. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  141. package/src/{components → system/components}/Dialog.svelte +1 -1
  142. package/src/system/components/FloatingTokenTags.css +275 -0
  143. package/src/system/components/FloatingTokenTags.svelte +543 -0
  144. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  145. package/src/system/components/MenuSelect.svelte +229 -0
  146. package/src/{components → system/components}/Notification.svelte +8 -1
  147. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  148. package/src/system/components/SectionDivider.svelte +560 -0
  149. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  150. package/src/{components → system/components}/TabBar.svelte +81 -65
  151. package/src/{components → system/components}/Table.svelte +17 -3
  152. package/src/{components → system/components}/Tooltip.svelte +6 -4
  153. package/src/system/styles/CONVENTIONS.md +178 -0
  154. package/src/system/styles/fonts.css +20 -0
  155. package/src/system/styles/tokens.css +601 -0
  156. package/src/system/styles/tokens.generated.css +544 -0
  157. package/src/component-editor/ImageEditor.svelte +0 -74
  158. package/src/component-editor/SectionDividerEditor.svelte +0 -265
  159. package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
  160. package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
  161. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  162. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  163. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  164. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  165. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  166. package/src/components/SectionDivider.svelte +0 -483
  167. package/src/data/google-fonts.json +0 -75
  168. package/src/lib/index.ts +0 -68
  169. package/src/lib/presetService.ts +0 -214
  170. package/src/lib/productionPulse.ts +0 -32
  171. package/src/styles/fonts.css +0 -30
  172. package/src/styles/tokens.css +0 -1324
  173. package/src/ui/FontStackEditor.svelte +0 -361
  174. package/src/ui/GradientEditor.svelte +0 -470
  175. package/src/ui/PresetFileManager.svelte +0 -1116
  176. package/src/ui/ProjectFontsSection.svelte +0 -645
  177. package/src/ui/ThemeFileManager.svelte +0 -1020
  178. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  179. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  180. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  181. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  182. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  183. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  184. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  185. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  186. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  187. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  188. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  189. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  190. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  191. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  192. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  193. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  194. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  195. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  196. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  197. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  198. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  199. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  200. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  201. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  202. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  203. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  204. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  205. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  206. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  207. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  208. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  209. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  210. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  211. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  212. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  213. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  214. /package/src/{ui → editor/ui}/index.ts +0 -0
  215. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  216. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  217. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  218. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  219. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  220. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  221. /package/src/{assets → system/assets}/offering.webp +0 -0
  222. /package/src/{components → system/components}/Callout.svelte +0 -0
  223. /package/src/{components → system/components}/Image.svelte +0 -0
  224. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  225. /package/src/{components → system/components}/types.ts +0 -0
  226. /package/src/{styles → system/styles}/_padding.scss +0 -0
  227. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  228. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  229. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  230. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  231. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  232. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -1,1020 +0,0 @@
1
- <script lang="ts">
2
- import { stopPropagation } from 'svelte/legacy';
3
-
4
- import { onMount, onDestroy } from 'svelte';
5
- import type { ThemeMeta } from '../lib/themeTypes';
6
- import { listThemes, deleteTheme, setActiveFile, getProductionInfo, setProductionFile } from '../lib/themeService';
7
- import { activeFileName } from '../lib/editorConfigStore';
8
- import { dirty } from '../lib/editorStore';
9
- import { productionRevision, bumpProductionRevision, themeProductionInfo } from '../lib/productionPulse';
10
- import UIDialog from './UIDialog.svelte';
11
- import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
12
-
13
- interface Props {
14
- saveStatus?: 'idle' | 'saving' | 'saved' | 'error';
15
- onsave?: (payload: { fileName: string; displayName: string }) => void;
16
- onload?: (payload: { fileName: string }) => void;
17
- }
18
-
19
- let { saveStatus = 'idle', onsave, onload }: Props = $props();
20
-
21
- let files: ThemeMeta[] = $state([]);
22
- let showFileList = $state(false);
23
- let saveAsDialog = $state(false);
24
- let currentDisplayName = $state('Default Theme');
25
-
26
- let prodApplyStatus: 'idle' | 'applying' | 'done' | 'error' = $state('idle');
27
-
28
- let infoOpen = $state(false);
29
- let infoBtnEl = $state<HTMLButtonElement | undefined>(undefined);
30
- let infoPopoverEl = $state<HTMLDivElement | undefined>(undefined);
31
- let infoPopoverReady = $state(false);
32
-
33
- let prodIsInSync = $derived($themeProductionInfo?.fileName === $activeFileName);
34
- let editorIsApplied = $derived(prodIsInSync && !$dirty);
35
- let prodName = $derived($themeProductionInfo?.name ?? '—');
36
-
37
- let isDefaultActive = $derived($activeFileName === 'default');
38
-
39
- async function refreshFiles() {
40
- try {
41
- files = await listThemes();
42
- const active = files.find((f) => f.isActive);
43
- if (active) {
44
- $activeFileName = active.fileName;
45
- currentDisplayName = active.name;
46
- }
47
- } catch {
48
- // silent — will show empty list
49
- }
50
- }
51
-
52
- async function refreshProduction() {
53
- try {
54
- const info = await getProductionInfo();
55
- themeProductionInfo.set(info);
56
- } catch {
57
- // silent — leave cached value in place
58
- }
59
- }
60
-
61
- onMount(async () => {
62
- await refreshFiles();
63
- await refreshProduction();
64
- window.addEventListener('keydown', handleKeydown);
65
- document.addEventListener('mousedown', handleDocumentMousedown, true);
66
- });
67
-
68
- onDestroy(() => {
69
- window.removeEventListener('keydown', handleKeydown);
70
- document.removeEventListener('mousedown', handleDocumentMousedown, true);
71
- });
72
-
73
- function handleKeydown(e: KeyboardEvent) {
74
- if (e.key === 'Escape' && infoOpen) {
75
- infoOpen = false;
76
- }
77
- }
78
-
79
- function handleDocumentMousedown(e: MouseEvent) {
80
- if (!infoOpen) return;
81
- const target = e.target as Element | null;
82
- if (target && !target.closest('.tfm-info-btn, .tfm-info-popover')) {
83
- infoOpen = false;
84
- }
85
- }
86
-
87
- /** Anchor the fixed-position popover to the right of the info button. The
88
- * sidebar lives on the left, so flow into the content area; flip up if the
89
- * button is near the bottom of the viewport. Mirrors the preset popover so
90
- * the two info surfaces feel identical. */
91
- function positionInfoPopover(): void {
92
- const btn = infoBtnEl;
93
- const pop = infoPopoverEl;
94
- if (!btn || !pop) return;
95
- const br = btn.getBoundingClientRect();
96
- const pr = pop.getBoundingClientRect();
97
- const margin = 8;
98
- const vw = window.innerWidth;
99
- const vh = window.innerHeight;
100
- let left = br.right + margin;
101
- if (left + pr.width > vw - margin) {
102
- left = br.left + br.width / 2 - pr.width / 2;
103
- if (left < margin) left = margin;
104
- if (left + pr.width > vw - margin) left = vw - margin - pr.width;
105
- }
106
- let top = br.bottom + margin;
107
- if (top + pr.height > vh - margin) {
108
- top = br.top - margin - pr.height;
109
- if (top < margin) top = margin;
110
- }
111
- pop.style.left = `${left}px`;
112
- pop.style.top = `${top}px`;
113
- infoPopoverReady = true;
114
- }
115
-
116
- $effect(() => {
117
- if (!infoOpen) {
118
- infoPopoverReady = false;
119
- return;
120
- }
121
- let raf1 = requestAnimationFrame(() => {
122
- raf1 = requestAnimationFrame(positionInfoPopover);
123
- });
124
- window.addEventListener('scroll', positionInfoPopover, true);
125
- window.addEventListener('resize', positionInfoPopover);
126
- return () => {
127
- cancelAnimationFrame(raf1);
128
- window.removeEventListener('scroll', positionInfoPopover, true);
129
- window.removeEventListener('resize', positionInfoPopover);
130
- };
131
- });
132
-
133
- // Refresh production state when any production pointer flips (e.g. a preset
134
- // is adopted elsewhere). Skip the initial tick — onMount already loaded it.
135
- let pulseInitialised = false;
136
- $effect(() => {
137
- void $productionRevision;
138
- if (!pulseInitialised) {
139
- pulseInitialised = true;
140
- return;
141
- }
142
- refreshProduction();
143
- });
144
-
145
- function handleSave() {
146
- if (isDefaultActive) return;
147
- onsave?.({ fileName: $activeFileName, displayName: currentDisplayName });
148
- }
149
-
150
- function openSaveAs() {
151
- showFileList = false;
152
- saveAsDialog = true;
153
- }
154
-
155
- function confirmSaveAs(detail: { displayName: string; fileName: string }) {
156
- const { displayName, fileName } = detail;
157
- onsave?.({ fileName, displayName });
158
- $activeFileName = fileName;
159
- currentDisplayName = displayName;
160
- setTimeout(() => refreshFiles(), 500);
161
- }
162
-
163
- async function handleApplyToProduction() {
164
- if (prodIsInSync) return;
165
- prodApplyStatus = 'applying';
166
- try {
167
- await setProductionFile($activeFileName);
168
- await refreshProduction();
169
- bumpProductionRevision();
170
- prodApplyStatus = 'done';
171
- setTimeout(() => { prodApplyStatus = 'idle'; }, 2000);
172
- } catch {
173
- prodApplyStatus = 'error';
174
- setTimeout(() => { prodApplyStatus = 'idle'; }, 3000);
175
- }
176
- }
177
-
178
- async function handleLoad(file: ThemeMeta) {
179
- if ($dirty) {
180
- const ok = window.confirm(
181
- 'Loading a theme will discard unsaved changes. Continue?',
182
- );
183
- if (!ok) return;
184
- }
185
- showFileList = false;
186
- await setActiveFile(file.fileName);
187
- $activeFileName = file.fileName;
188
- currentDisplayName = file.name;
189
- onload?.({ fileName: file.fileName });
190
- }
191
-
192
- async function handleDelete(file: ThemeMeta) {
193
- if (file.fileName === 'default') return;
194
- try {
195
- await deleteTheme(file.fileName);
196
- await refreshFiles();
197
- if (file.fileName === $activeFileName) {
198
- $activeFileName = 'default';
199
- currentDisplayName = 'Default Theme';
200
- onload?.({ fileName: 'default' });
201
- }
202
- } catch {
203
- // silent
204
- }
205
- }
206
-
207
- function toggleFileList() {
208
- showFileList = !showFileList;
209
- if (showFileList) refreshFiles();
210
- }
211
-
212
- const dateFormatter = new Intl.DateTimeFormat(undefined, {
213
- month: 'short',
214
- day: 'numeric',
215
- hour: 'numeric',
216
- minute: '2-digit',
217
- });
218
-
219
- function formatUpdatedAt(iso: string): string {
220
- if (!iso) return '';
221
- const d = new Date(iso);
222
- if (Number.isNaN(d.getTime())) return '';
223
- return dateFormatter.format(d);
224
- }
225
-
226
- type SortKey = 'name' | 'updatedAt';
227
- let sortKey: SortKey = $state('updatedAt');
228
- let sortDir: 'asc' | 'desc' = $state('desc');
229
-
230
- function toggleSort(key: SortKey) {
231
- if (sortKey === key) {
232
- sortDir = sortDir === 'asc' ? 'desc' : 'asc';
233
- } else {
234
- sortKey = key;
235
- sortDir = key === 'name' ? 'asc' : 'desc';
236
- }
237
- }
238
-
239
- let sortedFiles = $derived([...files].sort((a, b) => {
240
- let cmp = 0;
241
- if (sortKey === 'name') {
242
- cmp = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
243
- } else {
244
- cmp = (a.updatedAt || '').localeCompare(b.updatedAt || '');
245
- }
246
- return sortDir === 'asc' ? cmp : -cmp;
247
- }));
248
- </script>
249
-
250
- <div class="theme-file-manager">
251
- <div class="tfm-header">
252
- <span class="tfm-header-label">Theme</span>
253
- <button
254
- type="button"
255
- class="tfm-info-btn"
256
- aria-label="About themes"
257
- aria-expanded={infoOpen}
258
- bind:this={infoBtnEl}
259
- onclick={() => (infoOpen = !infoOpen)}
260
- >
261
- <i class="fas fa-circle-info"></i>
262
- </button>
263
- </div>
264
-
265
- <div class="tfm-cards" class:in-sync={prodIsInSync}>
266
- <div
267
- class="tfm-card tfm-card-editor"
268
- class:dirty={$dirty}
269
- class:applied={editorIsApplied}
270
- >
271
- <span class="tfm-rail" aria-hidden="true"></span>
272
- <div class="tfm-card-head">
273
- <span class="tfm-card-label">Editor</span>
274
- <span
275
- class="tfm-card-status"
276
- class:dirty={$dirty}
277
- class:applied={editorIsApplied}
278
- >
279
- <i class="tfm-status-dot" aria-hidden="true"></i>
280
- <span>{$dirty ? 'unsaved' : editorIsApplied ? 'live' : 'saved'}</span>
281
- </span>
282
- </div>
283
- <div class="tfm-pill" class:dirty={$dirty} class:applied={editorIsApplied}>
284
- <span class="tfm-pill-name" title={currentDisplayName}>{currentDisplayName}</span>
285
- </div>
286
- <div class="tfm-card-actions tfm-card-actions-stack">
287
- <button
288
- class="tfm-btn tfm-btn-row save-btn"
289
- class:saving={saveStatus === 'saving'}
290
- class:saved={saveStatus === 'saved'}
291
- class:error={saveStatus === 'error'}
292
- onclick={handleSave}
293
- disabled={saveStatus === 'saving' || isDefaultActive}
294
- title={isDefaultActive
295
- ? 'Default is read-only — use Save As to capture under a new name'
296
- : 'Save to current file'}
297
- >
298
- <i
299
- class="fas"
300
- class:fa-save={saveStatus === 'idle'}
301
- class:fa-spinner={saveStatus === 'saving'}
302
- class:fa-check={saveStatus === 'saved'}
303
- class:fa-times={saveStatus === 'error'}
304
- ></i>
305
- <span>
306
- {#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
307
- </span>
308
- </button>
309
- <button class="tfm-btn tfm-btn-row" onclick={openSaveAs} title="Save as new theme">
310
- <i class="fas fa-copy"></i>
311
- <span>Save As…</span>
312
- </button>
313
- <button
314
- class="tfm-btn tfm-btn-row"
315
- class:active={showFileList}
316
- onclick={toggleFileList}
317
- title="Load a theme"
318
- >
319
- <i class="fas fa-folder-open"></i>
320
- <span>Load…</span>
321
- </button>
322
- </div>
323
- </div>
324
-
325
- <button
326
- class="tfm-adopt-btn"
327
- class:saving={prodApplyStatus === 'applying'}
328
- class:saved={prodApplyStatus === 'done'}
329
- class:error={prodApplyStatus === 'error'}
330
- class:in-sync={prodIsInSync}
331
- onclick={handleApplyToProduction}
332
- disabled={prodApplyStatus === 'applying' || prodIsInSync}
333
- title={prodIsInSync
334
- ? 'This theme is already in production'
335
- : `Adopt "${currentDisplayName}" as the production theme`}
336
- >
337
- <i
338
- class="fas"
339
- class:fa-arrow-down={prodApplyStatus === 'idle'}
340
- class:fa-spinner={prodApplyStatus === 'applying'}
341
- class:fa-check={prodApplyStatus === 'done'}
342
- class:fa-xmark={prodApplyStatus === 'error'}
343
- ></i>
344
- <span>
345
- {#if prodApplyStatus === 'idle'}Adopt{:else if prodApplyStatus === 'applying'}Adopting{:else if prodApplyStatus === 'done'}Adopted{:else}Error{/if}
346
- </span>
347
- </button>
348
-
349
- <div
350
- class="tfm-card tfm-card-production"
351
- class:in-sync={prodIsInSync}
352
- >
353
- <span class="tfm-rail" aria-hidden="true"></span>
354
- <div class="tfm-card-head">
355
- <span class="tfm-card-label">Production</span>
356
- <span
357
- class="tfm-card-status"
358
- class:applied={prodIsInSync}
359
- >
360
- <i class="tfm-status-dot" aria-hidden="true"></i>
361
- <span>{prodIsInSync ? 'live' : 'out of sync'}</span>
362
- </span>
363
- </div>
364
- <div class="tfm-pill" class:applied={prodIsInSync}>
365
- <span class="tfm-pill-name" title={prodName}>{prodName}</span>
366
- </div>
367
- </div>
368
- </div>
369
- </div>
370
-
371
- {#if infoOpen}
372
- <div
373
- class="tfm-info-popover"
374
- class:ready={infoPopoverReady}
375
- role="dialog"
376
- aria-label="About themes"
377
- bind:this={infoPopoverEl}
378
- >
379
- <header class="tfm-info-header">
380
- <span class="tfm-info-title">Themes</span>
381
- <button
382
- type="button"
383
- class="tfm-info-close"
384
- aria-label="Close"
385
- onclick={() => (infoOpen = false)}
386
- >
387
- <i class="fas fa-xmark"></i>
388
- </button>
389
- </header>
390
- <div class="tfm-info-body">
391
- <p>
392
- A <strong>theme</strong> saves the design tokens for a site, components use these tokens to define their appearance.
393
- </p>
394
- </div>
395
- </div>
396
- {/if}
397
-
398
- <UIDialog
399
- bind:show={showFileList}
400
- title="Load Theme"
401
- cancelLabel="Close"
402
- width="420px"
403
- >
404
- <div class="load-list">
405
- <div class="load-header">
406
- <button
407
- class="sort-btn name-col"
408
- class:active-sort={sortKey === 'name'}
409
- onclick={() => toggleSort('name')}
410
- >
411
- <span>Name</span>
412
- {#if sortKey === 'name'}
413
- <i class="fas {sortDir === 'asc' ? 'fa-caret-up' : 'fa-caret-down'}"></i>
414
- {/if}
415
- </button>
416
- <button
417
- class="sort-btn date-col"
418
- class:active-sort={sortKey === 'updatedAt'}
419
- onclick={() => toggleSort('updatedAt')}
420
- >
421
- <span>Date</span>
422
- {#if sortKey === 'updatedAt'}
423
- <i class="fas {sortDir === 'asc' ? 'fa-caret-up' : 'fa-caret-down'}"></i>
424
- {/if}
425
- </button>
426
- <span class="header-spacer"></span>
427
- </div>
428
- {#each sortedFiles as file}
429
- <div class="load-item" class:active={file.fileName === $activeFileName}>
430
- <button class="load-name-btn" onclick={() => handleLoad(file)}>
431
- {file.name}
432
- </button>
433
- <span class="updated-at" title={file.updatedAt}>{formatUpdatedAt(file.updatedAt)}</span>
434
- {#if file.fileName === $activeFileName}
435
- <span class="active-badge">active</span>
436
- {/if}
437
- {#if file.fileName !== 'default'}
438
- <button
439
- class="file-delete-btn"
440
- onclick={stopPropagation(() => handleDelete(file))}
441
- title="Delete {file.name}"
442
- >
443
- <i class="fas fa-trash-alt"></i>
444
- </button>
445
- {/if}
446
- </div>
447
- {/each}
448
- {#if files.length === 0}
449
- <div class="load-item empty">No saved files</div>
450
- {/if}
451
- </div>
452
- </UIDialog>
453
-
454
- <SaveAsDialog
455
- bind:show={saveAsDialog}
456
- {currentDisplayName}
457
- {files}
458
- title="Save Theme As"
459
- placeholder="Theme name…"
460
- reservedNameMessage='The name "default" is reserved for the initial distribution.'
461
- onsave={confirmSaveAs}
462
- />
463
-
464
- <style>
465
- .theme-file-manager {
466
- --tfm-applied: #5aa85e;
467
- --tfm-rail-neutral: var(--ui-border-default);
468
- --tfm-rail-dirty: var(--ui-highlight);
469
- --tfm-rail-applied: var(--tfm-applied);
470
-
471
- display: flex;
472
- flex-direction: column;
473
- gap: var(--ui-space-8);
474
- }
475
-
476
- .tfm-header {
477
- display: flex;
478
- align-items: center;
479
- justify-content: space-between;
480
- gap: var(--ui-space-4);
481
- padding: 0 var(--ui-space-4);
482
- }
483
-
484
- .tfm-header-label {
485
- font-size: var(--ui-font-size-xs);
486
- color: var(--ui-text-secondary);
487
- text-transform: uppercase;
488
- letter-spacing: 0.05em;
489
- }
490
-
491
- .tfm-info-btn {
492
- display: inline-flex;
493
- align-items: center;
494
- justify-content: center;
495
- width: 22px;
496
- height: 22px;
497
- padding: 0;
498
- background: transparent;
499
- border: 0;
500
- color: var(--ui-text-tertiary);
501
- font-size: 0.95rem;
502
- line-height: 1;
503
- cursor: pointer;
504
- transition: color var(--ui-transition-fast);
505
- }
506
-
507
- .tfm-info-btn:hover,
508
- .tfm-info-btn[aria-expanded='true'] {
509
- color: var(--ui-text-primary);
510
- }
511
-
512
- /* Two-card pipeline (Editor → Production) — mirrors PresetFileManager so
513
- theme and preset surfaces share one visual idiom. */
514
- .tfm-cards {
515
- display: flex;
516
- flex-direction: column;
517
- gap: var(--ui-space-6);
518
- }
519
-
520
- .tfm-card {
521
- position: relative;
522
- display: flex;
523
- flex-direction: column;
524
- gap: var(--ui-space-6);
525
- padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-10) var(--ui-space-16);
526
- background: var(--ui-surface-lower);
527
- border: 1px solid var(--ui-border-subtle);
528
- border-radius: var(--ui-radius-md);
529
- }
530
-
531
- .tfm-rail {
532
- position: absolute;
533
- left: 0;
534
- top: 0;
535
- bottom: 0;
536
- width: 3px;
537
- border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
538
- background: var(--tfm-rail-neutral);
539
- transition: background var(--ui-transition-base);
540
- }
541
-
542
- .tfm-card-editor.dirty .tfm-rail { background: var(--tfm-rail-dirty); }
543
- .tfm-card-editor.applied .tfm-rail { background: var(--tfm-rail-applied); }
544
- .tfm-card-production.in-sync .tfm-rail { background: var(--tfm-rail-applied); }
545
-
546
- .tfm-card-head {
547
- display: flex;
548
- align-items: baseline;
549
- justify-content: space-between;
550
- gap: var(--ui-space-8);
551
- }
552
-
553
- .tfm-card-label {
554
- font-size: var(--ui-font-size-xs);
555
- font-weight: var(--ui-font-weight-semibold);
556
- text-transform: uppercase;
557
- letter-spacing: 0.08em;
558
- color: var(--ui-text-secondary);
559
- line-height: 1.1;
560
- }
561
-
562
- .tfm-card-status {
563
- display: inline-flex;
564
- align-items: center;
565
- gap: var(--ui-space-4);
566
- font-size: 0.7rem;
567
- letter-spacing: 0.02em;
568
- color: var(--ui-text-muted);
569
- line-height: 1;
570
- }
571
-
572
- .tfm-status-dot {
573
- width: 5px;
574
- height: 5px;
575
- border-radius: 50%;
576
- background: currentColor;
577
- opacity: 0.7;
578
- flex-shrink: 0;
579
- }
580
-
581
- .tfm-card-status.dirty {
582
- color: var(--ui-highlight);
583
- }
584
-
585
- .tfm-card-status.dirty .tfm-status-dot {
586
- opacity: 1;
587
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent);
588
- animation: tfm-pulse 1.6s ease-in-out infinite;
589
- }
590
-
591
- .tfm-card-status.applied {
592
- color: var(--tfm-applied);
593
- }
594
- .tfm-card-status.applied .tfm-status-dot {
595
- opacity: 1;
596
- }
597
-
598
- .tfm-pill {
599
- display: flex;
600
- align-items: center;
601
- padding: var(--ui-space-6) var(--ui-space-10);
602
- background: var(--ui-surface-lowest);
603
- border: 1px solid var(--ui-border-subtle);
604
- border-radius: var(--ui-radius-md);
605
- transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
606
- }
607
-
608
- .tfm-pill.dirty {
609
- border-color: color-mix(in srgb, var(--ui-highlight) 60%, var(--ui-border-subtle));
610
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-highlight) 35%, transparent);
611
- }
612
-
613
- .tfm-pill.applied {
614
- border-color: color-mix(in srgb, var(--tfm-applied) 50%, var(--ui-border-subtle));
615
- }
616
-
617
- .tfm-pill-name {
618
- flex: 1;
619
- min-width: 0;
620
- font-size: var(--ui-font-size-md);
621
- font-weight: var(--ui-font-weight-semibold);
622
- color: var(--ui-text-primary);
623
- white-space: nowrap;
624
- overflow: hidden;
625
- text-overflow: ellipsis;
626
- }
627
-
628
- .tfm-card-actions {
629
- display: flex;
630
- gap: var(--ui-space-4);
631
- flex-wrap: wrap;
632
- }
633
-
634
- .tfm-card-actions-stack {
635
- flex-direction: column;
636
- }
637
-
638
- .tfm-btn-row {
639
- width: 100%;
640
- justify-content: flex-start;
641
- gap: var(--ui-space-8);
642
- flex: 0 0 auto;
643
- text-align: left;
644
- }
645
-
646
- .tfm-btn-row i {
647
- width: 1rem;
648
- text-align: center;
649
- flex: 0 0 auto;
650
- }
651
-
652
- .tfm-btn-row span {
653
- flex: 1 1 auto;
654
- text-align: left;
655
- }
656
-
657
- /* Bridge button — sits between Editor and Production cards as the arrow that
658
- promotes the editor theme into production. */
659
- .tfm-adopt-btn {
660
- align-self: stretch;
661
- width: 100%;
662
- display: flex;
663
- align-items: center;
664
- justify-content: center;
665
- gap: var(--ui-space-6);
666
- margin: calc(var(--ui-space-2) * -1) 0;
667
- padding: var(--ui-space-6) var(--ui-space-12);
668
- background: color-mix(in srgb, var(--tfm-applied) 18%, var(--ui-surface-high));
669
- border: 1px solid color-mix(in srgb, var(--tfm-applied) 45%, var(--ui-border-medium));
670
- border-radius: var(--ui-radius-md);
671
- color: var(--ui-text-primary);
672
- font-size: var(--ui-font-size-md);
673
- font-weight: var(--ui-font-weight-medium);
674
- cursor: pointer;
675
- transition: all var(--ui-transition-fast);
676
- white-space: nowrap;
677
- position: relative;
678
- z-index: 1;
679
- }
680
-
681
- .tfm-adopt-btn i {
682
- width: 1rem;
683
- text-align: center;
684
- font-size: 0.85em;
685
- }
686
-
687
- .tfm-adopt-btn:hover:not(:disabled) {
688
- background: color-mix(in srgb, var(--tfm-applied) 30%, var(--ui-surface-higher));
689
- border-color: color-mix(in srgb, var(--tfm-applied) 70%, var(--ui-border-strong));
690
- }
691
-
692
- .tfm-adopt-btn:disabled {
693
- cursor: not-allowed;
694
- }
695
-
696
- .tfm-adopt-btn.in-sync {
697
- background: transparent;
698
- border-color: var(--ui-border-subtle);
699
- color: var(--ui-text-muted);
700
- opacity: 0.7;
701
- }
702
-
703
- .tfm-adopt-btn.saving i { animation: spin 1s linear infinite; }
704
- .tfm-adopt-btn.saved {
705
- background: color-mix(in srgb, var(--tfm-applied) 30%, var(--ui-surface-high));
706
- color: var(--tfm-applied);
707
- }
708
- .tfm-adopt-btn.error { color: var(--ui-text-muted); }
709
-
710
- .tfm-btn {
711
- display: inline-flex;
712
- align-items: center;
713
- justify-content: center;
714
- gap: var(--ui-space-4);
715
- padding: var(--ui-space-6) var(--ui-space-10);
716
- background: var(--ui-surface);
717
- border: 1px solid var(--ui-border-subtle);
718
- border-radius: var(--ui-radius-md);
719
- color: var(--ui-text-secondary);
720
- font-size: var(--ui-font-size-md);
721
- font-weight: var(--ui-font-weight-medium);
722
- cursor: pointer;
723
- transition: all var(--ui-transition-fast);
724
- white-space: nowrap;
725
- flex: 1 1 0;
726
- min-width: 0;
727
- }
728
-
729
- .tfm-btn i {
730
- width: 1rem;
731
- text-align: center;
732
- font-size: 0.85em;
733
- }
734
-
735
- .tfm-btn:hover:not(:disabled) {
736
- background: var(--ui-surface-high);
737
- color: var(--ui-text-primary);
738
- border-color: var(--ui-border-default);
739
- }
740
-
741
- .tfm-btn:disabled {
742
- opacity: 0.45;
743
- cursor: not-allowed;
744
- }
745
-
746
- .tfm-btn.active {
747
- background: var(--ui-surface);
748
- border-color: var(--ui-border-default);
749
- color: var(--ui-text-primary);
750
- }
751
-
752
- .save-btn {
753
- background: var(--ui-surface-high);
754
- border-color: var(--ui-border-medium);
755
- color: var(--ui-text-primary);
756
- }
757
-
758
- .save-btn:hover:not(:disabled) {
759
- background: var(--ui-surface-higher);
760
- border-color: var(--ui-border-strong);
761
- }
762
-
763
- .save-btn.saving i { animation: spin 1s linear infinite; }
764
- .save-btn.saved {
765
- background: var(--ui-surface-highest);
766
- color: var(--ui-text-success);
767
- }
768
- .save-btn.error {
769
- background: var(--ui-surface-high);
770
- color: var(--ui-text-muted);
771
- }
772
-
773
- .load-list {
774
- display: flex;
775
- flex-direction: column;
776
- max-height: 60vh;
777
- overflow-y: auto;
778
- }
779
-
780
- .load-header {
781
- display: flex;
782
- align-items: center;
783
- gap: 6px;
784
- padding: 4px 6px;
785
- border-bottom: 1px solid #3a3a3a;
786
- position: sticky;
787
- top: 0;
788
- background: var(--ui-surface, #1a1a1a);
789
- z-index: 1;
790
- }
791
-
792
- .sort-btn {
793
- display: inline-flex;
794
- align-items: center;
795
- gap: 4px;
796
- padding: 4px 0;
797
- background: none;
798
- border: none;
799
- color: #888;
800
- font-size: 11px;
801
- font-weight: 600;
802
- text-transform: uppercase;
803
- letter-spacing: 0.04em;
804
- cursor: pointer;
805
- text-align: left;
806
- }
807
-
808
- .sort-btn:hover {
809
- color: #ccc;
810
- }
811
-
812
- .sort-btn.active-sort {
813
- color: #e0e0e0;
814
- }
815
-
816
- .sort-btn i {
817
- font-size: 10px;
818
- opacity: 0.85;
819
- }
820
-
821
- .sort-btn.name-col {
822
- flex: 1;
823
- min-width: 0;
824
- padding-left: 4px;
825
- }
826
-
827
- .sort-btn.date-col {
828
- flex-shrink: 0;
829
- }
830
-
831
- .header-spacer {
832
- flex-shrink: 0;
833
- width: 24px;
834
- }
835
-
836
- .load-item {
837
- display: flex;
838
- align-items: center;
839
- gap: 6px;
840
- padding: 4px 6px;
841
- border-bottom: 1px solid #2a2a2a;
842
- }
843
-
844
- .load-item:last-child {
845
- border-bottom: none;
846
- }
847
-
848
- .load-item.empty {
849
- padding: 16px;
850
- color: #888;
851
- font-size: 14px;
852
- text-align: center;
853
- }
854
-
855
- .load-name-btn {
856
- flex: 1;
857
- min-width: 0;
858
- overflow: hidden;
859
- text-overflow: ellipsis;
860
- white-space: nowrap;
861
- padding: 6px 4px;
862
- background: none;
863
- border: none;
864
- color: #aaa;
865
- font-size: 14px;
866
- cursor: pointer;
867
- text-align: left;
868
- border-radius: 3px;
869
- }
870
-
871
- .load-name-btn:hover {
872
- color: #e0e0e0;
873
- }
874
-
875
- .load-item.active .load-name-btn {
876
- color: #e0e0e0;
877
- font-weight: 600;
878
- }
879
-
880
- .updated-at {
881
- flex-shrink: 0;
882
- font-size: 12px;
883
- color: #777;
884
- font-variant-numeric: tabular-nums;
885
- white-space: nowrap;
886
- }
887
-
888
- .active-badge {
889
- flex-shrink: 0;
890
- font-size: 12px;
891
- padding: 1px 6px;
892
- border-radius: 3px;
893
- background: #333;
894
- color: #ccc;
895
- }
896
-
897
- .file-delete-btn {
898
- flex-shrink: 0;
899
- display: flex;
900
- align-items: center;
901
- justify-content: center;
902
- width: 24px;
903
- height: 24px;
904
- padding: 0;
905
- background: none;
906
- border: none;
907
- color: #555;
908
- font-size: 12px;
909
- cursor: pointer;
910
- opacity: 0;
911
- }
912
-
913
- .load-item:hover .file-delete-btn {
914
- opacity: 1;
915
- }
916
-
917
- .file-delete-btn:hover {
918
- color: #ccc;
919
- }
920
-
921
- /* Info popover — fixed positioning escapes the sidebar's overflow and any
922
- parent stacking context. JS in this file anchors it to the right of the
923
- info button (the sidebar is on the left, so there's room to flow into
924
- the main content area without obscuring the button). */
925
- .tfm-info-popover {
926
- position: fixed;
927
- top: 0;
928
- left: 0;
929
- width: 22rem;
930
- max-width: calc(100vw - var(--ui-space-24));
931
- padding: 0;
932
- background: var(--ui-surface-higher);
933
- border: 1px solid var(--ui-border-medium);
934
- border-radius: var(--ui-radius-lg);
935
- box-shadow: var(--ui-shadow-lg);
936
- z-index: 1000;
937
- color: var(--ui-text-secondary);
938
- font-family: var(--ui-font-family, system-ui, sans-serif);
939
- overflow: hidden;
940
- visibility: hidden;
941
- animation: tfm-info-in 140ms ease-out;
942
- }
943
-
944
- .tfm-info-popover.ready {
945
- visibility: visible;
946
- }
947
-
948
- .tfm-info-header {
949
- display: flex;
950
- align-items: center;
951
- justify-content: space-between;
952
- gap: var(--ui-space-8);
953
- padding: var(--ui-space-10) var(--ui-space-12) var(--ui-space-10) var(--ui-space-16);
954
- border-bottom: 1px solid var(--ui-border-subtle);
955
- }
956
-
957
- .tfm-info-title {
958
- color: var(--ui-text-primary);
959
- font-size: var(--ui-font-size-sm);
960
- font-weight: var(--ui-font-weight-semibold);
961
- letter-spacing: -0.01em;
962
- line-height: 1.2;
963
- }
964
-
965
- .tfm-info-close {
966
- display: inline-flex;
967
- align-items: center;
968
- justify-content: center;
969
- width: var(--ui-space-24);
970
- height: var(--ui-space-24);
971
- padding: 0;
972
- background: transparent;
973
- border: 0;
974
- border-radius: var(--ui-radius-sm);
975
- color: var(--ui-text-tertiary);
976
- font-size: var(--ui-font-size-xs);
977
- line-height: 1;
978
- cursor: pointer;
979
- transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
980
- }
981
-
982
- .tfm-info-close:hover {
983
- color: var(--ui-text-primary);
984
- background: var(--ui-hover);
985
- }
986
-
987
- .tfm-info-body {
988
- padding: var(--ui-space-16);
989
- }
990
-
991
- .tfm-info-popover p {
992
- margin: 0 0 var(--ui-space-12) 0;
993
- font-size: var(--ui-font-size-xs);
994
- line-height: 1.55;
995
- }
996
-
997
- .tfm-info-popover p:last-child {
998
- margin-bottom: 0;
999
- }
1000
-
1001
- .tfm-info-popover strong {
1002
- color: var(--ui-text-primary);
1003
- font-weight: var(--ui-font-weight-semibold);
1004
- }
1005
-
1006
- @keyframes tfm-info-in {
1007
- from { opacity: 0; transform: translateY(-3px); }
1008
- to { opacity: 1; transform: translateY(0); }
1009
- }
1010
-
1011
- @keyframes tfm-pulse {
1012
- 0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
1013
- 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
1014
- }
1015
-
1016
- @keyframes spin {
1017
- from { transform: rotate(0deg); }
1018
- to { transform: rotate(360deg); }
1019
- }
1020
- </style>