@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,1116 +0,0 @@
1
- <script lang="ts">
2
- import { stopPropagation } from 'svelte/legacy';
3
-
4
- import { onMount, onDestroy } from 'svelte';
5
- import type { Preset, PresetMeta } from '../lib/themeTypes';
6
- import {
7
- listPresets,
8
- deletePreset,
9
- captureCurrentAsPreset,
10
- captureProductionAsPreset,
11
- applyPreset,
12
- applyPresetToProduction,
13
- compareActiveToProduction,
14
- getActivePreset,
15
- } from '../lib/presetService';
16
- import { sanitizeFileName } from '../lib/themeService';
17
- import { dirty, componentDirty } from '../lib/editorStore';
18
- import { productionRevision, presetProductionComparison } from '../lib/productionPulse';
19
- import UIDialog from './UIDialog.svelte';
20
- import UnsavedComponentsDialog from './UnsavedComponentsDialog.svelte';
21
- import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
22
-
23
- let files: PresetMeta[] = $state([]);
24
- let showFileList = $state(false);
25
- let saveAsDialog = $state(false);
26
- let captureProdDialog = $state(false);
27
-
28
- let activeFileName = $state('default');
29
- let currentDisplayName = $state('Default Preset');
30
- let activePreset = $state<Preset | null>(null);
31
-
32
- let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
33
- let applyStatus: 'idle' | 'applying' = $state('idle');
34
- let prodApplyStatus: 'idle' | 'applying' | 'done' | 'error' = $state('idle');
35
- let prodCaptureStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
36
-
37
- let unsavedDialog = $state(false);
38
- /** Pending capture target while UnsavedComponentsDialog is open. The dialog
39
- * resolves to either "save the preset using current on-disk files" (proceed)
40
- * or "user cancelled / closed". */
41
- let pendingCapture: { fileName: string; displayName: string } | null = null;
42
-
43
- let infoOpen = $state(false);
44
- let infoBtnEl = $state<HTMLButtonElement | undefined>(undefined);
45
- let infoPopoverEl = $state<HTMLDivElement | undefined>(undefined);
46
- let infoPopoverReady = $state(false);
47
-
48
- let dirtyComponentIds = $derived(
49
- Object.entries($componentDirty)
50
- .filter(([, isDirty]) => isDirty)
51
- .map(([id]) => id),
52
- );
53
-
54
- /** True when there are unsaved theme/component edits that won't make it into
55
- * the next preset Save (capture reads on-disk files, not the editor). */
56
- let presetStale = $derived(dirtyComponentIds.length > 0 || $dirty);
57
-
58
- let isDefaultActive = $derived(activeFileName === 'default');
59
-
60
- let prodStatus = $derived($presetProductionComparison?.status ?? 'editor-only');
61
- let prodPresetName = $derived($presetProductionComparison?.productionPreset?.name ?? '—');
62
- let prodIsInSync = $derived(prodStatus === 'in-production');
63
- let prodIsDiverged = $derived(prodStatus === 'diverged');
64
- /** Editor row is "live" when this preset IS the one in production AND
65
- * nothing's pending — mirrors the cfm-row-editor.applied rule so the green
66
- * dot vocabulary is consistent across components and presets. */
67
- let editorIsApplied = $derived(prodIsInSync && !presetStale);
68
-
69
- async function refreshFiles() {
70
- try {
71
- files = await listPresets();
72
- const active = files.find((f) => f.isActive);
73
- if (active) {
74
- activeFileName = active.fileName;
75
- currentDisplayName = active.name;
76
- }
77
- } catch {
78
- // silent — empty list
79
- }
80
- }
81
-
82
- async function refreshActive() {
83
- try {
84
- const active = await getActivePreset();
85
- if (active) {
86
- activePreset = active;
87
- activeFileName = active._fileName ?? activeFileName;
88
- currentDisplayName = active.name;
89
- }
90
- } catch {
91
- // silent
92
- }
93
- }
94
-
95
- async function refreshProductionComparison() {
96
- if (!activePreset) return;
97
- try {
98
- $presetProductionComparison = await compareActiveToProduction(activePreset);
99
- } catch {
100
- // silent — leave previous comparison in place
101
- }
102
- }
103
-
104
- onMount(async () => {
105
- await refreshActive();
106
- await refreshFiles();
107
- await refreshProductionComparison();
108
- window.addEventListener('keydown', handleKeydown);
109
- document.addEventListener('mousedown', handleDocumentMousedown, true);
110
- });
111
-
112
- // Re-compare when any production pointer ticks (per-component Adopt,
113
- // theme production change, or our own apply). Skip the initial tick on
114
- // mount — refreshProductionComparison runs there already. Plain closure
115
- // variable (not $state) so writing it doesn't add a reactive dependency
116
- // back to the effect.
117
- let pulseInitialised = false;
118
- $effect(() => {
119
- void $productionRevision;
120
- if (!pulseInitialised) {
121
- pulseInitialised = true;
122
- return;
123
- }
124
- refreshProductionComparison();
125
- });
126
-
127
- onDestroy(() => {
128
- window.removeEventListener('keydown', handleKeydown);
129
- document.removeEventListener('mousedown', handleDocumentMousedown, true);
130
- });
131
-
132
- function handleKeydown(e: KeyboardEvent) {
133
- if (e.key === 'Escape' && infoOpen) {
134
- infoOpen = false;
135
- }
136
- }
137
-
138
- function handleDocumentMousedown(e: MouseEvent) {
139
- if (!infoOpen) return;
140
- const target = e.target as Element | null;
141
- if (target && !target.closest('.pfm-info-btn, .pfm-info-popover')) {
142
- infoOpen = false;
143
- }
144
- }
145
-
146
- /** Anchor the fixed-position popover relative to the info button. Lives in
147
- * the sidebar footer, so flip-up when there's no room below. */
148
- function positionInfoPopover(): void {
149
- const btn = infoBtnEl;
150
- const pop = infoPopoverEl;
151
- if (!btn || !pop) return;
152
- const br = btn.getBoundingClientRect();
153
- const pr = pop.getBoundingClientRect();
154
- const margin = 8;
155
- const vw = window.innerWidth;
156
- const vh = window.innerHeight;
157
- let left = br.right + margin;
158
- if (left + pr.width > vw - margin) {
159
- left = br.left + br.width / 2 - pr.width / 2;
160
- if (left < margin) left = margin;
161
- if (left + pr.width > vw - margin) left = vw - margin - pr.width;
162
- }
163
- let top = br.bottom + margin;
164
- if (top + pr.height > vh - margin) {
165
- top = br.top - margin - pr.height;
166
- if (top < margin) top = margin;
167
- }
168
- pop.style.left = `${left}px`;
169
- pop.style.top = `${top}px`;
170
- infoPopoverReady = true;
171
- }
172
-
173
- $effect(() => {
174
- if (!infoOpen) {
175
- infoPopoverReady = false;
176
- return;
177
- }
178
- let raf1 = requestAnimationFrame(() => {
179
- raf1 = requestAnimationFrame(positionInfoPopover);
180
- });
181
- window.addEventListener('scroll', positionInfoPopover, true);
182
- window.addEventListener('resize', positionInfoPopover);
183
- return () => {
184
- cancelAnimationFrame(raf1);
185
- window.removeEventListener('scroll', positionInfoPopover, true);
186
- window.removeEventListener('resize', positionInfoPopover);
187
- };
188
- });
189
-
190
- /** Standard "save dirty editor first?" gate. If any component has unsaved
191
- * edits, defer to UnsavedComponentsDialog (which can save them in place);
192
- * callers receive `false` and the dialog's "proceed" path re-invokes the
193
- * capture with the pending args. Returns `true` if the caller can capture
194
- * immediately (no dirty components). */
195
- function gateDirtyCapture(fileName: string, displayName: string): boolean {
196
- if (dirtyComponentIds.length === 0 && !$dirty) return true;
197
- pendingCapture = { fileName, displayName };
198
- unsavedDialog = true;
199
- return false;
200
- }
201
-
202
- function handleUnsavedProceed() {
203
- const target = pendingCapture;
204
- pendingCapture = null;
205
- if (target) doCapture(target.fileName, target.displayName);
206
- }
207
-
208
- async function doCapture(fileName: string, displayName: string) {
209
- saveStatus = 'saving';
210
- try {
211
- await captureCurrentAsPreset(fileName, displayName);
212
- activeFileName = fileName;
213
- currentDisplayName = displayName;
214
- saveStatus = 'saved';
215
- setTimeout(() => { saveStatus = 'idle'; }, 2000);
216
- await refreshFiles();
217
- await refreshActive();
218
- await refreshProductionComparison();
219
- } catch {
220
- saveStatus = 'error';
221
- setTimeout(() => { saveStatus = 'idle'; }, 3000);
222
- }
223
- }
224
-
225
- async function handleApplyToProduction() {
226
- if (!activeFileName) return;
227
- prodApplyStatus = 'applying';
228
- try {
229
- await applyPresetToProduction(activeFileName);
230
- // applyPresetToProduction bakes css; refresh comparison so the
231
- // Production card flips to the live state.
232
- await refreshProductionComparison();
233
- prodApplyStatus = 'done';
234
- setTimeout(() => { prodApplyStatus = 'idle'; }, 2000);
235
- } catch {
236
- prodApplyStatus = 'error';
237
- setTimeout(() => { prodApplyStatus = 'idle'; }, 3000);
238
- }
239
- }
240
-
241
- function openCaptureFromProduction() {
242
- captureProdDialog = true;
243
- }
244
-
245
- async function confirmCaptureFromProduction(detail: { displayName: string; fileName: string }) {
246
- const { displayName, fileName } = detail;
247
- prodCaptureStatus = 'saving';
248
- try {
249
- await captureProductionAsPreset(fileName, displayName);
250
- await refreshFiles();
251
- prodCaptureStatus = 'saved';
252
- setTimeout(() => { prodCaptureStatus = 'idle'; }, 2000);
253
- } catch {
254
- prodCaptureStatus = 'error';
255
- setTimeout(() => { prodCaptureStatus = 'idle'; }, 3000);
256
- }
257
- }
258
-
259
- async function handleSave() {
260
- // Default is the protected initial-distribution preset — disabled in the
261
- // UI when active, but guard here too. Callers in that state should route
262
- // through Save As (which seeds an incremented name).
263
- if (isDefaultActive) return;
264
- if (!gateDirtyCapture(activeFileName, currentDisplayName)) return;
265
- await doCapture(activeFileName, currentDisplayName);
266
- }
267
-
268
- function openSaveAs() {
269
- showFileList = false;
270
- saveAsDialog = true;
271
- }
272
-
273
- async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
274
- const { displayName, fileName } = detail;
275
- if (!gateDirtyCapture(fileName, displayName)) return;
276
- await doCapture(fileName, displayName);
277
- }
278
-
279
- async function handleApply(file: PresetMeta) {
280
- if ($dirty) {
281
- const ok = window.confirm(
282
- 'Loading a preset will reload the editor and discard unsaved changes. Continue?',
283
- );
284
- if (!ok) return;
285
- }
286
- showFileList = false;
287
- applyStatus = 'applying';
288
- try {
289
- await applyPreset(file.fileName);
290
- // Page reload: simplest path — editor rehydrates from the now-active
291
- // theme + component configs the server just pinned for us.
292
- window.location.reload();
293
- } catch (err) {
294
- applyStatus = 'idle';
295
- window.alert(`Failed to apply preset: ${(err as Error).message}`);
296
- }
297
- }
298
-
299
- async function handleDelete(file: PresetMeta) {
300
- if (file.fileName === 'default') return;
301
- try {
302
- await deletePreset(file.fileName);
303
- await refreshFiles();
304
- if (file.fileName === activeFileName) {
305
- activeFileName = 'default';
306
- currentDisplayName = 'Default Preset';
307
- }
308
- } catch {
309
- // silent
310
- }
311
- }
312
-
313
- function toggleFileList() {
314
- showFileList = !showFileList;
315
- if (showFileList) refreshFiles();
316
- }
317
- </script>
318
-
319
- <div class="preset-file-manager">
320
- <div class="pfm-header">
321
- <span class="pfm-header-label">Preset</span>
322
- <button
323
- type="button"
324
- class="pfm-info-btn"
325
- aria-label="About presets"
326
- aria-expanded={infoOpen}
327
- bind:this={infoBtnEl}
328
- onclick={() => (infoOpen = !infoOpen)}
329
- >
330
- <i class="fas fa-circle-info"></i>
331
- </button>
332
- </div>
333
-
334
- <div class="pfm-cards" class:in-sync={prodIsInSync}>
335
- <div
336
- class="pfm-card pfm-card-editor"
337
- class:dirty={presetStale}
338
- class:applied={editorIsApplied}
339
- >
340
- <span class="pfm-rail" aria-hidden="true"></span>
341
- <div class="pfm-card-head">
342
- <span class="pfm-card-label">Editor</span>
343
- <span
344
- class="pfm-card-status"
345
- class:dirty={presetStale}
346
- class:applied={editorIsApplied}
347
- >
348
- <i class="pfm-status-dot" aria-hidden="true"></i>
349
- <span>{presetStale ? 'unsaved' : editorIsApplied ? 'live' : 'saved'}</span>
350
- </span>
351
- </div>
352
- <div class="pfm-pill" class:dirty={presetStale} class:applied={editorIsApplied}>
353
- <span class="pfm-pill-name" title={currentDisplayName}>{currentDisplayName}</span>
354
- </div>
355
- {#if presetStale}
356
- <span class="pfm-stale-note" aria-live="polite">unsaved edits won't be captured</span>
357
- {/if}
358
- <div class="pfm-card-actions pfm-card-actions-stack">
359
- <button
360
- class="pfm-btn pfm-btn-row save-btn"
361
- class:saving={saveStatus === 'saving'}
362
- class:saved={saveStatus === 'saved'}
363
- class:error={saveStatus === 'error'}
364
- onclick={handleSave}
365
- disabled={saveStatus === 'saving' || applyStatus === 'applying' || isDefaultActive}
366
- title={isDefaultActive
367
- ? 'Default is read-only — use Save As to capture under a new name'
368
- : 'Capture the current theme + component configs into this preset'}
369
- >
370
- <i
371
- class="fas"
372
- class:fa-save={saveStatus === 'idle'}
373
- class:fa-spinner={saveStatus === 'saving'}
374
- class:fa-check={saveStatus === 'saved'}
375
- class:fa-times={saveStatus === 'error'}
376
- ></i>
377
- <span>
378
- {#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
379
- </span>
380
- </button>
381
- <button class="pfm-btn pfm-btn-row" onclick={openSaveAs} title="Save as new preset">
382
- <i class="fas fa-copy"></i>
383
- <span>Save As…</span>
384
- </button>
385
- <button
386
- class="pfm-btn pfm-btn-row"
387
- class:active={showFileList}
388
- onclick={toggleFileList}
389
- disabled={applyStatus === 'applying'}
390
- title="Load a preset"
391
- >
392
- <i class="fas fa-folder-open"></i>
393
- <span>Load…</span>
394
- </button>
395
- </div>
396
- </div>
397
-
398
- <button
399
- class="pfm-adopt-btn"
400
- class:saving={prodApplyStatus === 'applying'}
401
- class:saved={prodApplyStatus === 'done'}
402
- class:error={prodApplyStatus === 'error'}
403
- class:in-sync={prodIsInSync}
404
- onclick={handleApplyToProduction}
405
- disabled={prodApplyStatus === 'applying' || prodIsInSync}
406
- title={prodIsInSync
407
- ? 'This preset is already in production'
408
- : prodIsDiverged
409
- ? `Re-adopt "${currentDisplayName}" — discards individual component adopts`
410
- : `Adopt "${currentDisplayName}" as the production preset`}
411
- >
412
- <i
413
- class="fas"
414
- class:fa-arrow-down={prodApplyStatus === 'idle'}
415
- class:fa-spinner={prodApplyStatus === 'applying'}
416
- class:fa-check={prodApplyStatus === 'done'}
417
- class:fa-xmark={prodApplyStatus === 'error'}
418
- ></i>
419
- <span>
420
- {#if prodApplyStatus === 'idle'}Adopt{:else if prodApplyStatus === 'applying'}Adopting{:else if prodApplyStatus === 'done'}Adopted{:else}Error{/if}
421
- </span>
422
- </button>
423
-
424
- <div
425
- class="pfm-card pfm-card-production"
426
- class:in-sync={prodIsInSync}
427
- class:diverged={prodIsDiverged}
428
- >
429
- <span class="pfm-rail" aria-hidden="true"></span>
430
- <div class="pfm-card-head">
431
- <span class="pfm-card-label">Production</span>
432
- <span
433
- class="pfm-card-status"
434
- class:applied={prodIsInSync}
435
- class:diverged={prodIsDiverged}
436
- >
437
- <i class="pfm-status-dot" aria-hidden="true"></i>
438
- <span>
439
- {#if prodIsInSync}live{:else if prodIsDiverged}diverged{:else}out of sync{/if}
440
- </span>
441
- </span>
442
- </div>
443
- <div class="pfm-pill" class:applied={prodIsInSync} class:diverged={prodIsDiverged}>
444
- <span class="pfm-pill-name" title={prodPresetName}>{prodPresetName}</span>
445
- </div>
446
- {#if prodIsDiverged && $presetProductionComparison}
447
- <span class="pfm-diverged-note" aria-live="polite">
448
- {$presetProductionComparison.driftedComponents.length > 0
449
- ? `${$presetProductionComparison.driftedComponents.length} component${
450
- $presetProductionComparison.driftedComponents.length === 1 ? '' : 's'
451
- } adopted individually`
452
- : 'theme production differs from this preset'}
453
- </span>
454
- {/if}
455
- {#if prodIsDiverged}
456
- <div class="pfm-card-actions">
457
- <button
458
- class="pfm-btn pfm-btn-wide"
459
- onclick={openCaptureFromProduction}
460
- disabled={prodCaptureStatus === 'saving'}
461
- title="Save the current production state as a new named preset"
462
- >
463
- <i
464
- class="fas"
465
- class:fa-bookmark={prodCaptureStatus === 'idle'}
466
- class:fa-spinner={prodCaptureStatus === 'saving'}
467
- class:fa-check={prodCaptureStatus === 'saved'}
468
- class:fa-times={prodCaptureStatus === 'error'}
469
- ></i>
470
- <span>
471
- {#if prodCaptureStatus === 'idle'}Capture as preset{:else if prodCaptureStatus === 'saving'}Saving{:else if prodCaptureStatus === 'saved'}Saved{:else}Error{/if}
472
- </span>
473
- </button>
474
- </div>
475
- {/if}
476
- </div>
477
- </div>
478
-
479
- {#if applyStatus === 'applying'}
480
- <span class="apply-status">Applying preset…</span>
481
- {/if}
482
- </div>
483
-
484
- {#if infoOpen}
485
- <div
486
- class="pfm-info-popover"
487
- class:ready={infoPopoverReady}
488
- role="dialog"
489
- aria-label="About presets"
490
- bind:this={infoPopoverEl}
491
- >
492
- <header class="pfm-info-header">
493
- <span class="pfm-info-title">Presets</span>
494
- <button
495
- type="button"
496
- class="pfm-info-close"
497
- aria-label="Close"
498
- onclick={() => (infoOpen = false)}
499
- >
500
- <i class="fas fa-xmark"></i>
501
- </button>
502
- </header>
503
- <div class="pfm-info-body">
504
- <p>
505
- A <strong>preset</strong> is a manifest naming one theme file and one
506
- config file per component.
507
- </p>
508
- <p>
509
- The <strong>Editor</strong> row is the preset you're working under.
510
- <strong>Save</strong> captures the currently active files into its
511
- manifest.
512
- </p>
513
- <p>
514
- The <strong>Production</strong> row is the preset currently baked into
515
- <code>tokens.css</code>. <strong>Adopt</strong> sets every component's
516
- production file from the editor preset.
517
- </p>
518
- <p>
519
- Adopting a single component elsewhere can leave production
520
- <em>diverged</em> from any preset. <strong>Capture</strong> saves that
521
- state under a new name.
522
- </p>
523
- </div>
524
- </div>
525
- {/if}
526
-
527
- <UIDialog bind:show={showFileList} title="Load Preset" cancelLabel="Close" width="420px">
528
- <div class="load-list">
529
- {#each files as file}
530
- <div class="load-item" class:active={file.fileName === activeFileName}>
531
- <button class="load-name-btn" onclick={() => handleApply(file)}>
532
- {file.name}
533
- </button>
534
- {#if file.fileName === activeFileName}
535
- <span class="active-badge">active</span>
536
- {/if}
537
- {#if file.fileName !== 'default'}
538
- <button
539
- class="file-delete-btn"
540
- onclick={stopPropagation(() => handleDelete(file))}
541
- title="Delete {file.name}"
542
- >
543
- <i class="fas fa-trash-alt"></i>
544
- </button>
545
- {/if}
546
- </div>
547
- {/each}
548
- {#if files.length === 0}
549
- <div class="load-item empty">No saved presets</div>
550
- {/if}
551
- </div>
552
- </UIDialog>
553
-
554
- <SaveAsDialog
555
- bind:show={saveAsDialog}
556
- {currentDisplayName}
557
- {files}
558
- title="Save Preset As"
559
- placeholder="Preset name…"
560
- reservedNameMessage='The name "default" is reserved for the initial distribution.'
561
- onsave={confirmSaveAs}
562
- />
563
-
564
- <SaveAsDialog
565
- bind:show={captureProdDialog}
566
- currentDisplayName={prodPresetName}
567
- {files}
568
- title="Capture Production as Preset"
569
- placeholder="Preset name…"
570
- reservedNameMessage='The name "default" is reserved for the initial distribution.'
571
- onsave={confirmCaptureFromProduction}
572
- />
573
-
574
- <UnsavedComponentsDialog
575
- bind:show={unsavedDialog}
576
- dirtyComponents={dirtyComponentIds}
577
- onproceed={handleUnsavedProceed}
578
- />
579
-
580
- <style>
581
- .preset-file-manager {
582
- --pfm-applied: #5aa85e;
583
- --pfm-rail-neutral: var(--ui-border-default);
584
- --pfm-rail-dirty: var(--ui-highlight);
585
- --pfm-rail-applied: var(--pfm-applied);
586
-
587
- display: flex;
588
- flex-direction: column;
589
- gap: var(--ui-space-8);
590
- }
591
-
592
- .pfm-header {
593
- display: flex;
594
- align-items: center;
595
- justify-content: space-between;
596
- gap: var(--ui-space-4);
597
- padding: 0 var(--ui-space-4);
598
- }
599
-
600
- .pfm-header-label {
601
- font-size: var(--ui-font-size-xs);
602
- color: var(--ui-text-secondary);
603
- text-transform: uppercase;
604
- letter-spacing: 0.05em;
605
- }
606
-
607
- /* Naked icon button — same affordance language as cfm-info-btn. */
608
- .pfm-info-btn {
609
- display: inline-flex;
610
- align-items: center;
611
- justify-content: center;
612
- width: 22px;
613
- height: 22px;
614
- padding: 0;
615
- background: transparent;
616
- border: 0;
617
- color: var(--ui-text-tertiary);
618
- font-size: 0.95rem;
619
- line-height: 1;
620
- cursor: pointer;
621
- transition: color var(--ui-transition-fast);
622
- }
623
-
624
- .pfm-info-btn:hover,
625
- .pfm-info-btn[aria-expanded='true'] {
626
- color: var(--ui-text-primary);
627
- }
628
-
629
- /* ── two-card pipeline (Editor → Production) ─────────────────────────────
630
- Mirrors the cfm-rows pattern in ComponentFileManager so the preset reads
631
- as the same kind of artifact as themes and components, just one level up. */
632
- .pfm-cards {
633
- display: flex;
634
- flex-direction: column;
635
- gap: var(--ui-space-6);
636
- }
637
-
638
- .pfm-card {
639
- position: relative;
640
- display: flex;
641
- flex-direction: column;
642
- gap: var(--ui-space-6);
643
- padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-10) var(--ui-space-16);
644
- background: var(--ui-surface-lower);
645
- border: 1px solid var(--ui-border-subtle);
646
- border-radius: var(--ui-radius-md);
647
- }
648
-
649
- .pfm-rail {
650
- position: absolute;
651
- left: 0;
652
- top: 0;
653
- bottom: 0;
654
- width: 3px;
655
- border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
656
- background: var(--pfm-rail-neutral);
657
- transition: background var(--ui-transition-base);
658
- }
659
-
660
- .pfm-card-editor.dirty .pfm-rail { background: var(--pfm-rail-dirty); }
661
- .pfm-card-editor.applied .pfm-rail { background: var(--pfm-rail-applied); }
662
- .pfm-card-production.in-sync .pfm-rail { background: var(--pfm-rail-applied); }
663
- .pfm-card-production.diverged .pfm-rail { background: var(--ui-highlight); }
664
-
665
- .pfm-card-head {
666
- display: flex;
667
- align-items: baseline;
668
- justify-content: space-between;
669
- gap: var(--ui-space-8);
670
- }
671
-
672
- .pfm-card-label {
673
- font-size: var(--ui-font-size-xs);
674
- font-weight: var(--ui-font-weight-semibold);
675
- text-transform: uppercase;
676
- letter-spacing: 0.08em;
677
- color: var(--ui-text-secondary);
678
- line-height: 1.1;
679
- }
680
-
681
- .pfm-card-status {
682
- display: inline-flex;
683
- align-items: center;
684
- gap: var(--ui-space-4);
685
- font-size: 0.7rem;
686
- letter-spacing: 0.02em;
687
- color: var(--ui-text-muted);
688
- line-height: 1;
689
- }
690
-
691
- .pfm-status-dot {
692
- width: 5px;
693
- height: 5px;
694
- border-radius: 50%;
695
- background: currentColor;
696
- opacity: 0.7;
697
- flex-shrink: 0;
698
- }
699
-
700
- .pfm-card-status.dirty,
701
- .pfm-card-status.diverged {
702
- color: var(--ui-highlight);
703
- }
704
-
705
- .pfm-card-status.dirty .pfm-status-dot,
706
- .pfm-card-status.diverged .pfm-status-dot {
707
- opacity: 1;
708
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent);
709
- animation: pfm-pulse 1.6s ease-in-out infinite;
710
- }
711
-
712
- .pfm-card-status.applied {
713
- color: var(--pfm-applied);
714
- }
715
- .pfm-card-status.applied .pfm-status-dot {
716
- opacity: 1;
717
- }
718
-
719
- /* filename pill — matches the cfm-pill vocabulary so the editor side and
720
- the preset side use one visual idiom for "this is a named file". */
721
- .pfm-pill {
722
- display: flex;
723
- align-items: center;
724
- padding: var(--ui-space-6) var(--ui-space-10);
725
- background: var(--ui-surface-lowest);
726
- border: 1px solid var(--ui-border-subtle);
727
- border-radius: var(--ui-radius-md);
728
- transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
729
- }
730
-
731
- .pfm-pill.dirty {
732
- border-color: color-mix(in srgb, var(--ui-highlight) 60%, var(--ui-border-subtle));
733
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-highlight) 35%, transparent);
734
- }
735
-
736
- .pfm-pill.applied {
737
- border-color: color-mix(in srgb, var(--pfm-applied) 50%, var(--ui-border-subtle));
738
- }
739
-
740
- .pfm-pill.diverged {
741
- border-color: color-mix(in srgb, var(--ui-highlight) 50%, var(--ui-border-subtle));
742
- }
743
-
744
- .pfm-pill-name {
745
- flex: 1;
746
- min-width: 0;
747
- font-size: var(--ui-font-size-md);
748
- font-weight: var(--ui-font-weight-semibold);
749
- color: var(--ui-text-primary);
750
- white-space: nowrap;
751
- overflow: hidden;
752
- text-overflow: ellipsis;
753
- }
754
-
755
- .pfm-stale-note,
756
- .pfm-diverged-note {
757
- font-size: 0.7rem;
758
- letter-spacing: 0.02em;
759
- color: var(--ui-highlight);
760
- line-height: 1.2;
761
- }
762
-
763
- .pfm-card-actions {
764
- display: flex;
765
- gap: var(--ui-space-4);
766
- flex-wrap: wrap;
767
- }
768
-
769
- /* Stack variant — Save / Save As / Load as left-aligned rows. */
770
- .pfm-card-actions-stack {
771
- flex-direction: column;
772
- }
773
-
774
- .pfm-btn-row {
775
- width: 100%;
776
- justify-content: flex-start;
777
- gap: var(--ui-space-8);
778
- flex: 0 0 auto;
779
- text-align: left;
780
- }
781
-
782
- .pfm-btn-row i {
783
- width: 1rem;
784
- text-align: center;
785
- flex: 0 0 auto;
786
- }
787
-
788
- .pfm-btn-row span {
789
- flex: 1 1 auto;
790
- text-align: left;
791
- }
792
-
793
- /* Bridge button — lives between the Editor and Production cards so the
794
- promote action visually IS the arrow that connects them. Matches the
795
- .cfm-btn.primary geometry (--ui-radius-md, --ui-space-6/12 padding) so
796
- it reads as the same affordance as the per-component Adopt. */
797
- .pfm-adopt-btn {
798
- align-self: stretch;
799
- width: 100%;
800
- display: flex;
801
- align-items: center;
802
- justify-content: center;
803
- gap: var(--ui-space-6);
804
- margin: calc(var(--ui-space-2) * -1) 0;
805
- padding: var(--ui-space-6) var(--ui-space-12);
806
- background: color-mix(in srgb, var(--pfm-applied) 18%, var(--ui-surface-high));
807
- border: 1px solid color-mix(in srgb, var(--pfm-applied) 45%, var(--ui-border-medium));
808
- border-radius: var(--ui-radius-md);
809
- color: var(--ui-text-primary);
810
- font-size: var(--ui-font-size-md);
811
- font-weight: var(--ui-font-weight-medium);
812
- cursor: pointer;
813
- transition: all var(--ui-transition-fast);
814
- white-space: nowrap;
815
- position: relative;
816
- z-index: 1;
817
- }
818
-
819
- .pfm-adopt-btn i {
820
- width: 1rem;
821
- text-align: center;
822
- font-size: 0.85em;
823
- }
824
-
825
- .pfm-adopt-btn:hover:not(:disabled) {
826
- background: color-mix(in srgb, var(--pfm-applied) 30%, var(--ui-surface-higher));
827
- border-color: color-mix(in srgb, var(--pfm-applied) 70%, var(--ui-border-strong));
828
- }
829
-
830
- .pfm-adopt-btn:disabled {
831
- cursor: not-allowed;
832
- }
833
-
834
- .pfm-adopt-btn.in-sync {
835
- background: transparent;
836
- border-color: var(--ui-border-subtle);
837
- color: var(--ui-text-muted);
838
- opacity: 0.7;
839
- }
840
-
841
- .pfm-adopt-btn.saving i { animation: spin 1s linear infinite; }
842
- .pfm-adopt-btn.saved {
843
- background: color-mix(in srgb, var(--pfm-applied) 30%, var(--ui-surface-high));
844
- color: var(--pfm-applied);
845
- }
846
- .pfm-adopt-btn.error { color: var(--ui-text-muted); }
847
-
848
- .pfm-btn {
849
- display: inline-flex;
850
- align-items: center;
851
- justify-content: center;
852
- gap: var(--ui-space-4);
853
- padding: var(--ui-space-6) var(--ui-space-10);
854
- background: var(--ui-surface);
855
- border: 1px solid var(--ui-border-subtle);
856
- border-radius: var(--ui-radius-md);
857
- color: var(--ui-text-secondary);
858
- font-size: var(--ui-font-size-md);
859
- font-weight: var(--ui-font-weight-medium);
860
- cursor: pointer;
861
- transition: all var(--ui-transition-fast);
862
- white-space: nowrap;
863
- flex: 1 1 0;
864
- min-width: 0;
865
- }
866
-
867
- .pfm-btn i {
868
- width: 1rem;
869
- text-align: center;
870
- font-size: 0.85em;
871
- }
872
-
873
- .pfm-btn:hover:not(:disabled) {
874
- background: var(--ui-surface-high);
875
- color: var(--ui-text-primary);
876
- border-color: var(--ui-border-default);
877
- }
878
-
879
- .pfm-btn:disabled {
880
- opacity: 0.45;
881
- cursor: not-allowed;
882
- }
883
-
884
- .pfm-btn.active {
885
- background: var(--ui-surface);
886
- border-color: var(--ui-border-default);
887
- color: var(--ui-text-primary);
888
- }
889
-
890
- .pfm-btn-wide {
891
- flex: 1 1 100%;
892
- width: 100%;
893
- }
894
-
895
- .save-btn {
896
- background: var(--ui-surface-high);
897
- border-color: var(--ui-border-medium);
898
- color: var(--ui-text-primary);
899
- }
900
-
901
- .save-btn:hover:not(:disabled) {
902
- background: var(--ui-surface-higher);
903
- border-color: var(--ui-border-strong);
904
- }
905
-
906
- .save-btn.saving i { animation: spin 1s linear infinite; }
907
- .save-btn.saved {
908
- background: var(--ui-surface-highest);
909
- color: var(--ui-text-success);
910
- }
911
- .save-btn.error {
912
- background: var(--ui-surface-high);
913
- color: var(--ui-text-muted);
914
- }
915
-
916
-
917
- .load-list {
918
- display: flex;
919
- flex-direction: column;
920
- max-height: 60vh;
921
- overflow-y: auto;
922
- }
923
-
924
- .load-item {
925
- display: flex;
926
- align-items: center;
927
- gap: 6px;
928
- padding: 4px 6px;
929
- border-bottom: 1px solid #2a2a2a;
930
- }
931
-
932
- .load-item:last-child {
933
- border-bottom: none;
934
- }
935
-
936
- .load-item.empty {
937
- padding: 16px;
938
- color: #888;
939
- font-size: 14px;
940
- text-align: center;
941
- }
942
-
943
- .load-name-btn {
944
- flex: 1;
945
- min-width: 0;
946
- overflow: hidden;
947
- text-overflow: ellipsis;
948
- white-space: nowrap;
949
- padding: 6px 4px;
950
- background: none;
951
- border: none;
952
- color: #aaa;
953
- font-size: 14px;
954
- cursor: pointer;
955
- text-align: left;
956
- border-radius: 3px;
957
- }
958
-
959
- .load-name-btn:hover {
960
- color: #e0e0e0;
961
- }
962
-
963
- .load-item.active .load-name-btn {
964
- color: #e0e0e0;
965
- font-weight: 600;
966
- }
967
-
968
- .active-badge {
969
- flex-shrink: 0;
970
- font-size: 12px;
971
- padding: 1px 6px;
972
- border-radius: 3px;
973
- background: #333;
974
- color: #ccc;
975
- }
976
-
977
- .file-delete-btn {
978
- flex-shrink: 0;
979
- display: flex;
980
- align-items: center;
981
- justify-content: center;
982
- width: 24px;
983
- height: 24px;
984
- padding: 0;
985
- background: none;
986
- border: none;
987
- color: #555;
988
- font-size: 12px;
989
- cursor: pointer;
990
- opacity: 0;
991
- }
992
-
993
- .load-item:hover .file-delete-btn {
994
- opacity: 1;
995
- }
996
-
997
- .file-delete-btn:hover {
998
- color: #ccc;
999
- }
1000
-
1001
- .apply-status {
1002
- font-size: var(--ui-font-size-xs);
1003
- color: var(--ui-text-secondary);
1004
- padding: 0 var(--ui-space-4);
1005
- letter-spacing: 0.02em;
1006
- }
1007
-
1008
- /* Info popover — fixed positioning escapes the sidebar's overflow and any
1009
- parent stacking context. JS in this file anchors it to the right of the
1010
- info button (the sidebar is on the left, so there's room to flow into
1011
- the main content area without obscuring the button). */
1012
- .pfm-info-popover {
1013
- position: fixed;
1014
- top: 0;
1015
- left: 0;
1016
- width: 22rem;
1017
- max-width: calc(100vw - var(--ui-space-24));
1018
- padding: 0;
1019
- background: var(--ui-surface-higher);
1020
- border: 1px solid var(--ui-border-medium);
1021
- border-radius: var(--ui-radius-lg);
1022
- box-shadow: var(--ui-shadow-lg);
1023
- z-index: 1000;
1024
- color: var(--ui-text-secondary);
1025
- font-family: var(--ui-font-family, system-ui, sans-serif);
1026
- overflow: hidden;
1027
- visibility: hidden;
1028
- animation: pfm-info-in 140ms ease-out;
1029
- }
1030
-
1031
- .pfm-info-popover.ready {
1032
- visibility: visible;
1033
- }
1034
-
1035
- .pfm-info-header {
1036
- display: flex;
1037
- align-items: center;
1038
- justify-content: space-between;
1039
- gap: var(--ui-space-8);
1040
- padding: var(--ui-space-10) var(--ui-space-12) var(--ui-space-10) var(--ui-space-16);
1041
- border-bottom: 1px solid var(--ui-border-subtle);
1042
- }
1043
-
1044
- .pfm-info-title {
1045
- color: var(--ui-text-primary);
1046
- font-size: var(--ui-font-size-sm);
1047
- font-weight: var(--ui-font-weight-semibold);
1048
- letter-spacing: -0.01em;
1049
- line-height: 1.2;
1050
- }
1051
-
1052
- .pfm-info-close {
1053
- display: inline-flex;
1054
- align-items: center;
1055
- justify-content: center;
1056
- width: var(--ui-space-24);
1057
- height: var(--ui-space-24);
1058
- padding: 0;
1059
- background: transparent;
1060
- border: 0;
1061
- border-radius: var(--ui-radius-sm);
1062
- color: var(--ui-text-tertiary);
1063
- font-size: var(--ui-font-size-xs);
1064
- line-height: 1;
1065
- cursor: pointer;
1066
- transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
1067
- }
1068
-
1069
- .pfm-info-close:hover {
1070
- color: var(--ui-text-primary);
1071
- background: var(--ui-hover);
1072
- }
1073
-
1074
- .pfm-info-body {
1075
- padding: var(--ui-space-16);
1076
- }
1077
-
1078
- .pfm-info-popover p {
1079
- margin: 0 0 var(--ui-space-12) 0;
1080
- font-size: var(--ui-font-size-xs);
1081
- line-height: 1.55;
1082
- }
1083
-
1084
- .pfm-info-popover p:last-child {
1085
- margin-bottom: 0;
1086
- }
1087
-
1088
- .pfm-info-popover strong {
1089
- color: var(--ui-text-primary);
1090
- font-weight: var(--ui-font-weight-semibold);
1091
- }
1092
-
1093
- .pfm-info-popover em {
1094
- font-style: italic;
1095
- color: var(--ui-text-primary);
1096
- }
1097
-
1098
- @keyframes pfm-pulse {
1099
- 0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
1100
- 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
1101
- }
1102
-
1103
- @keyframes pfm-info-in {
1104
- from { opacity: 0; transform: translateY(-3px); }
1105
- to { opacity: 1; transform: translateY(0); }
1106
- }
1107
-
1108
- @keyframes spin {
1109
- from {
1110
- transform: rotate(0deg);
1111
- }
1112
- to {
1113
- transform: rotate(360deg);
1114
- }
1115
- }
1116
- </style>