@motion-proto/live-tokens 0.6.2 → 0.7.1

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 (212) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +147 -136
  3. package/dist-plugin/index.d.cts +1 -1
  4. package/dist-plugin/index.d.ts +1 -1
  5. package/dist-plugin/index.js +145 -135
  6. package/package.json +25 -40
  7. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  8. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  9. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  10. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +3 -3
  11. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  12. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  13. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  14. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  15. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  16. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +64 -37
  17. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  18. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  19. package/src/{component-editor → editor/component-editor}/SectionDividerEditor.svelte +57 -84
  20. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  21. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +16 -20
  22. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  23. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  24. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  25. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  26. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +2 -2
  27. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  28. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +144 -416
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  31. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  32. package/src/{component-editor → editor/component-editor}/scaffolding/DividerEditor.svelte +1 -1
  33. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  34. package/src/{component-editor → editor/component-editor}/scaffolding/GradientCard.svelte +6 -6
  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 -11
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  39. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +72 -0
  40. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  41. package/src/editor/component-editor/scaffolding/StateBlock.svelte +257 -0
  42. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +9 -7
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +644 -0
  44. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  45. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  46. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +14 -0
  47. package/src/{lib → editor/core/components}/componentConfigService.ts +2 -2
  48. package/src/{lib → editor/core/components}/componentPersist.ts +5 -5
  49. package/src/editor/core/flashStatus.ts +30 -0
  50. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  51. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  52. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  53. package/src/editor/core/manifests/manifestService.ts +116 -0
  54. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +2 -2
  55. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +5 -5
  56. package/src/editor/core/productionPulse.ts +37 -0
  57. package/src/{lib → editor/core/routing}/router.ts +1 -1
  58. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  59. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  60. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  61. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  62. package/src/{lib → editor/core/store}/editorStore.ts +17 -17
  63. package/src/{lib → editor/core/store}/editorTypes.ts +1 -1
  64. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  65. package/src/{lib → editor/core/themes}/slices/components.ts +2 -2
  66. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  67. package/src/{lib → editor/core/themes}/slices/gradients.ts +2 -2
  68. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  69. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  70. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  71. package/src/{lib → editor/core/themes}/themeInit.ts +6 -6
  72. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  73. package/src/{lib → editor/core/themes}/themeTypes.ts +11 -7
  74. package/src/editor/index.ts +69 -0
  75. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +79 -125
  76. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  77. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  78. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  79. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  80. package/src/{styles → editor/styles}/ui-editor.css +41 -21
  81. package/src/{styles → editor/styles}/ui-form-controls.css +8 -8
  82. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +8 -8
  83. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  84. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +8 -6
  85. package/src/editor/ui/FileLoadList.svelte +350 -0
  86. package/src/editor/ui/FilePill.svelte +80 -0
  87. package/src/{ui → editor/ui}/FontStackEditor.svelte +7 -7
  88. package/src/{ui → editor/ui}/GradientEditor.svelte +11 -11
  89. package/src/{ui → editor/ui}/GradientStopPicker.svelte +1 -1
  90. package/src/editor/ui/ManifestFileManager.svelte +371 -0
  91. package/src/{ui → editor/ui}/PaletteEditor.svelte +132 -598
  92. package/src/{ui → editor/ui}/ProjectFontsSection.svelte +102 -144
  93. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  94. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  95. package/src/{ui → editor/ui}/ThemeFileManager.svelte +286 -519
  96. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  97. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -6
  98. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +1 -1
  99. package/src/editor/ui/UIInfoPopover.svelte +244 -0
  100. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  101. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  102. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  103. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +26 -26
  104. package/src/editor/ui/UIPillButton.svelte +138 -0
  105. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  106. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  107. package/src/editor/ui/UISquareButton.svelte +172 -0
  108. package/src/{ui → editor/ui}/UITokenSelector.svelte +10 -10
  109. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  110. package/src/{ui → editor/ui}/VariablesTab.svelte +31 -8
  111. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  112. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +13 -13
  113. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +8 -5
  114. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  115. package/src/editor/ui/palette/paletteMath.ts +275 -0
  116. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -17
  117. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +7 -7
  118. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +17 -17
  119. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +22 -22
  120. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  121. package/src/{components → system/components}/Badge.svelte +0 -36
  122. package/src/{components → system/components}/Card.svelte +8 -62
  123. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  124. package/src/{components → system/components}/Dialog.svelte +1 -1
  125. package/src/system/components/FloatingTokenTags.css +256 -0
  126. package/src/system/components/FloatingTokenTags.svelte +592 -0
  127. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  128. package/src/system/components/MenuSelect.svelte +229 -0
  129. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  130. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  131. package/src/{components → system/components}/TabBar.svelte +81 -65
  132. package/src/{components → system/components}/Table.svelte +17 -3
  133. package/src/{components → system/components}/Tooltip.svelte +6 -4
  134. package/src/system/styles/CONVENTIONS.md +178 -0
  135. package/src/{styles → system/styles}/fonts.css +6 -3
  136. package/src/{styles → system/styles}/tokens.css +149 -29
  137. package/src/component-editor/ImageEditor.svelte +0 -74
  138. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  139. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  140. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  141. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  142. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  143. package/src/data/google-fonts.json +0 -75
  144. package/src/lib/index.ts +0 -68
  145. package/src/lib/presetService.ts +0 -214
  146. package/src/lib/productionPulse.ts +0 -32
  147. package/src/ui/PresetFileManager.svelte +0 -1116
  148. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  149. /package/src/{styles → app}/site.css +0 -0
  150. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  151. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  152. /package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +0 -0
  153. /package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +0 -0
  154. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  155. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  156. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  157. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  158. /package/src/{lib → editor/core/components}/componentConfigKeys.ts +0 -0
  159. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  160. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  161. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  162. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  163. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  164. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  165. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  166. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  167. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  168. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  169. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  170. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  171. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  172. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  173. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  174. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  175. /package/src/{lib → editor/core/themes}/migrations/index.ts +0 -0
  176. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  177. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  178. /package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -0
  179. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  180. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  181. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  182. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  183. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  184. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  185. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  186. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  187. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  188. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  189. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  190. /package/src/{ui → editor/ui}/index.ts +0 -0
  191. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  192. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  193. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  194. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  195. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  196. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  197. /package/src/{assets → system/assets}/offering.webp +0 -0
  198. /package/src/{components → system/components}/Button.svelte +0 -0
  199. /package/src/{components → system/components}/Callout.svelte +0 -0
  200. /package/src/{components → system/components}/CollapsibleSection.svelte +0 -0
  201. /package/src/{components → system/components}/Image.svelte +0 -0
  202. /package/src/{components → system/components}/Notification.svelte +0 -0
  203. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  204. /package/src/{components → system/components}/SectionDivider.svelte +0 -0
  205. /package/src/{components → system/components}/types.ts +0 -0
  206. /package/src/{styles → system/styles}/_padding.scss +0 -0
  207. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  208. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  209. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  210. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  211. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  212. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -4,8 +4,9 @@
4
4
 
5
5
  <script lang="ts">
6
6
  import { onMount, onDestroy } from 'svelte';
7
+ import UIInfoPopover from '../../ui/UIInfoPopover.svelte';
7
8
  import { get } from 'svelte/store';
8
- import type { ComponentConfig, ComponentConfigMeta } from '../../lib/themeTypes';
9
+ import type { ComponentConfig, ComponentConfigMeta } from '../../core/themes/themeTypes';
9
10
  import { componentSourceFile } from './componentSources';
10
11
  import {
11
12
  loadComponentConfig,
@@ -14,19 +15,25 @@
14
15
  setActiveComponentFile,
15
16
  setComponentProductionFile,
16
17
  type ComponentProductionInfo,
17
- } from '../../lib/componentConfigService';
18
+ } from '../../core/components/componentConfigService';
18
19
  import {
19
20
  editorState,
20
21
  componentDirty,
21
22
  loadComponentActive,
22
23
  markComponentSaved,
23
- } from '../../lib/editorStore';
24
- import { bumpProductionRevision } from '../../lib/productionPulse';
25
- import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../lib/migrations';
26
- import type { CssVarRef } from '../../lib/editorTypes';
27
- import { safeFetch } from '../../lib/storage';
24
+ } from '../../core/store/editorStore';
25
+ import { bumpProductionRevision } from '../../core/productionPulse';
26
+ import { listManifests, saveAsManifest } from '../../core/manifests/manifestService';
27
+ import type { ManifestMeta } from '../../core/themes/themeTypes';
28
+ import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../core/themes/migrations';
29
+ import type { CssVarRef } from '../../core/store/editorTypes';
30
+ import { safeFetch } from '../../core/storage/storage';
31
+ import { flashStatus } from '../../core/flashStatus';
28
32
  import ComponentFileMenu from './ComponentFileMenu.svelte';
29
33
  import SaveAsDialog from './SaveAsDialog.svelte';
34
+ import FilePill from '../../ui/FilePill.svelte';
35
+ import UIPillButton from '../../ui/UIPillButton.svelte';
36
+ import UISquareButton from '../../ui/UISquareButton.svelte';
30
37
 
31
38
 
32
39
 
@@ -50,74 +57,29 @@
50
57
 
51
58
  type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
52
59
  let saveStatus: SaveStatus = 'idle';
60
+ const setSaveStatus = (s: SaveStatus) => (saveStatus = s);
53
61
 
54
- /** Show a transient saveStatus (saved/error) and revert to idle after 2s.
55
- Centralises the timing so all flash sites stay in sync. */
56
- function flashStatus(state: Exclude<SaveStatus, 'idle'>) {
57
- saveStatus = state;
58
- setTimeout(() => (saveStatus = 'idle'), 2000);
59
- }
60
62
  let files: ComponentConfigMeta[] = $state([]);
61
63
  let activeFileName = $state('default');
62
64
  let currentDisplayName = $state('Default');
63
65
  let saveAsDialog = $state(false);
66
+ // Title + description re-set per trigger so the auto-prompts (Save on
67
+ // default, Adopt on default+dirty) explain why they appeared, while the
68
+ // manual Save As button stays terse.
69
+ let saveAsTitle = $state('Save Component As');
70
+ let saveAsDescription = $state('');
64
71
 
65
72
  let productionInfo = $state<ComponentProductionInfo | null>(null);
66
73
  type ProductionStatus = 'idle' | 'updating' | 'done' | 'error';
67
74
  let productionUpdateStatus: ProductionStatus = $state('idle');
68
75
  let adoptFeedback = $state('');
69
76
 
70
- let infoOpen = $state(false);
71
- let infoBtnEl = $state<HTMLButtonElement | undefined>(undefined);
72
- let infoPopoverEl = $state<HTMLDivElement | undefined>(undefined);
73
- let infoPopoverReady = $state(false);
74
-
75
- /** Anchor the fixed-position popover centered below the info button.
76
- Uses position: fixed so it escapes the sticky header's stacking
77
- context (which was letting the side panels paint over it). */
78
- function positionInfoPopover(): void {
79
- const btn = infoBtnEl;
80
- const pop = infoPopoverEl;
81
- if (!btn || !pop) return;
82
- const br = btn.getBoundingClientRect();
83
- const pr = pop.getBoundingClientRect();
84
- const margin = 8;
85
- let left = br.left + br.width / 2 - pr.width / 2;
86
- const vw = window.innerWidth;
87
- if (left < margin) left = margin;
88
- if (left + pr.width > vw - margin) left = vw - margin - pr.width;
89
- pop.style.left = `${left}px`;
90
- pop.style.top = `${br.bottom + margin}px`;
91
- infoPopoverReady = true;
92
- }
93
-
94
- $effect(() => {
95
- if (!infoOpen) {
96
- infoPopoverReady = false;
97
- return;
98
- }
99
- // Two rAFs: first so Svelte mounts the popover and the bind: ref is set,
100
- // second so its rendered width is measurable before we anchor it.
101
- let raf1 = requestAnimationFrame(() => {
102
- raf1 = requestAnimationFrame(positionInfoPopover);
103
- });
104
- window.addEventListener('scroll', positionInfoPopover, true);
105
- window.addEventListener('resize', positionInfoPopover);
106
- return () => {
107
- cancelAnimationFrame(raf1);
108
- window.removeEventListener('scroll', positionInfoPopover, true);
109
- window.removeEventListener('resize', positionInfoPopover);
110
- };
111
- });
77
+ // Manifest SaveAs prompt for the "Adopt while default manifest is active" case.
78
+ let manifestSaveAsDialog = $state(false);
79
+ let manifests: ManifestMeta[] = $state([]);
80
+ let retryAdoptAfterManifestSave = false;
112
81
 
113
- /** Same idle-after-2s pattern for the production-update flash. */
114
- function flashProductionStatus(state: Exclude<ProductionStatus, 'idle'>) {
115
- productionUpdateStatus = state;
116
- setTimeout(() => {
117
- productionUpdateStatus = 'idle';
118
- adoptFeedback = '';
119
- }, 2000);
120
- }
82
+ const setProductionStatus = (s: ProductionStatus) => (productionUpdateStatus = s);
121
83
 
122
84
  let compDirty = $derived($componentDirty[component] ?? false);
123
85
  let isApplied = $derived(!!productionInfo && productionInfo.fileName === activeFileName && !compDirty);
@@ -150,28 +112,16 @@
150
112
  await refreshFiles();
151
113
  await refreshProduction();
152
114
  window.addEventListener('keydown', handleKeydown);
153
- document.addEventListener('mousedown', handleDocumentMousedown, true);
154
115
  });
155
116
 
156
117
  onDestroy(() => {
157
118
  window.removeEventListener('keydown', handleKeydown);
158
- document.removeEventListener('mousedown', handleDocumentMousedown, true);
159
119
  });
160
120
 
161
121
  function handleKeydown(e: KeyboardEvent) {
162
122
  if ((e.metaKey || e.ctrlKey) && e.key === 's') {
163
123
  e.preventDefault();
164
124
  handleSave();
165
- } else if (e.key === 'Escape' && infoOpen) {
166
- infoOpen = false;
167
- }
168
- }
169
-
170
- function handleDocumentMousedown(e: MouseEvent) {
171
- if (!infoOpen) return;
172
- const target = e.target as Element | null;
173
- if (target && !target.closest('.cfm-info-btn, .cfm-info-popover')) {
174
- infoOpen = false;
175
125
  }
176
126
  }
177
127
 
@@ -211,21 +161,24 @@
211
161
 
212
162
  async function handleSave() {
213
163
  if (activeFileName === 'default') {
214
- // Default is regenerated from source can't overwrite directly.
215
- saveAsDialog = true;
164
+ // Default is regenerated from source, so the first Save quietly opens
165
+ // Save As — same dialog as the Save As button, no scolding description.
166
+ openSaveAs();
216
167
  return;
217
168
  }
218
169
  saveStatus = 'saving';
219
170
  try {
220
171
  await persist(activeFileName, currentDisplayName);
221
- flashStatus('saved');
172
+ flashStatus(setSaveStatus, 'saved');
222
173
  await refreshFiles();
223
174
  } catch {
224
- flashStatus('error');
175
+ flashStatus(setSaveStatus, 'error');
225
176
  }
226
177
  }
227
178
 
228
179
  function openSaveAs() {
180
+ saveAsTitle = 'Save Component As';
181
+ saveAsDescription = '';
229
182
  saveAsDialog = true;
230
183
  }
231
184
 
@@ -234,10 +187,10 @@
234
187
  saveStatus = 'saving';
235
188
  try {
236
189
  await persist(fileName, displayName);
237
- flashStatus('saved');
190
+ flashStatus(setSaveStatus, 'saved');
238
191
  await refreshFiles();
239
192
  } catch {
240
- flashStatus('error');
193
+ flashStatus(setSaveStatus, 'error');
241
194
  }
242
195
  }
243
196
 
@@ -262,11 +215,16 @@
262
215
  // Multi-step service flow (delete + reload-default-on-active-removal).
263
216
  // Silent by design — see handleLoad.
264
217
  try {
218
+ // Capture before refreshFiles() reads the server's reverted active back
219
+ // into local state — otherwise the "was this the active file?" check
220
+ // below sees the post-revert value and skips the reload.
221
+ const wasActive = file.fileName === activeFileName;
265
222
  await deleteComponentConfig(component, file.fileName);
266
223
  await refreshFiles();
267
224
  await refreshProduction();
268
- if (file.fileName === activeFileName) {
269
- // Server reverts active to default; reload default aliases into the store.
225
+ if (wasActive) {
226
+ // Server reverts active to default; reload default aliases into the store
227
+ // so the deleted file's CSS vars are replaced by default's.
270
228
  const defaultCfg = await loadComponentConfig(component, 'default');
271
229
  loadComponentActive(component, 'default', defaultCfg.aliases, defaultCfg.config, defaultCfg.schemaVersion ?? 0);
272
230
  activeFileName = 'default';
@@ -283,6 +241,9 @@
283
241
  // regenerated from source and can't be overwritten, so route to Save As
284
242
  // and bail; the user can re-trigger Adopt after the new file is saved.
285
243
  if (compDirty && activeFileName === 'default') {
244
+ saveAsTitle = 'Save Component As';
245
+ saveAsDescription =
246
+ 'Adopting pushes this component to production. The default config is read-only, so save your edits to a new file first.';
286
247
  saveAsDialog = true;
287
248
  return;
288
249
  }
@@ -300,10 +261,38 @@
300
261
  adoptFeedback = wasDirty
301
262
  ? `Saved "${adoptingName}" and adopted`
302
263
  : `Adopted "${adoptingName}"`;
303
- flashProductionStatus('done');
304
- } catch {
264
+ flashStatus(setProductionStatus, 'done', { onIdle: () => (adoptFeedback = '') });
265
+ } catch (err) {
266
+ const e = err as Error & { code?: string };
267
+ if (e.code === 'ACTIVE_IS_PROTECTED') {
268
+ adoptFeedback = '';
269
+ productionUpdateStatus = 'idle';
270
+ retryAdoptAfterManifestSave = true;
271
+ try {
272
+ manifests = await listManifests();
273
+ } catch {
274
+ manifests = [];
275
+ }
276
+ manifestSaveAsDialog = true;
277
+ return;
278
+ }
305
279
  adoptFeedback = '';
306
- flashProductionStatus('error');
280
+ flashStatus(setProductionStatus, 'error', { onIdle: () => (adoptFeedback = '') });
281
+ }
282
+ }
283
+
284
+ async function onManifestSaveAs(detail: { displayName: string; fileName: string }) {
285
+ manifestSaveAsDialog = false;
286
+ try {
287
+ await saveAsManifest(detail.fileName, detail.displayName);
288
+ } catch (err) {
289
+ window.alert(`Failed to create manifest: ${(err as Error).message}`);
290
+ retryAdoptAfterManifestSave = false;
291
+ return;
292
+ }
293
+ if (retryAdoptAfterManifestSave) {
294
+ retryAdoptAfterManifestSave = false;
295
+ await handleUpdateProduction();
307
296
  }
308
297
  }
309
298
 
@@ -324,14 +313,11 @@
324
313
  <h2 class="cfm-title">{title}</h2>
325
314
  {/if}
326
315
  {#if sourceFile && projectRoot}
327
- <a
328
- class="source-link"
316
+ <UIPillButton
317
+ icon="fa-code"
329
318
  href="vscode://file/{projectRoot}/{sourceFile}"
330
319
  title="Open {sourceFile} in VS Code"
331
- >
332
- <i class="fas fa-code"></i>
333
- <span>Show component source</span>
334
- </a>
320
+ >Show component source</UIPillButton>
335
321
  {/if}
336
322
  </div>
337
323
 
@@ -345,18 +331,19 @@
345
331
  <span>{compDirty ? 'unsaved' : isApplied ? 'live' : 'saved'}</span>
346
332
  </span>
347
333
  </div>
348
- <span
349
- class="cfm-pill"
350
- class:dirty={compDirty}
351
- class:applied={isApplied}
334
+ <FilePill
335
+ name={currentDisplayName}
336
+ isProtected={activeFileName === 'default'}
337
+ dirty={compDirty}
338
+ applied={isApplied}
339
+ protectedTitle="Protected system config"
352
340
  title={compDirty
353
341
  ? 'Unsaved changes'
354
342
  : isApplied
355
343
  ? 'Active config is applied to production'
356
344
  : ''}
357
- >
358
- <span class="cfm-pill-name">{currentDisplayName}</span>
359
- </span>
345
+ style="flex: 0 0 11.25rem; width: 11.25rem;"
346
+ />
360
347
  <div class="cfm-actions">
361
348
  <ComponentFileMenu
362
349
  {component}
@@ -369,15 +356,12 @@
369
356
  ondelete={handleDelete}
370
357
  />
371
358
  {#if resetVariables}
372
- <button
373
- class="cfm-btn reset-btn"
359
+ <UISquareButton
360
+ icon="fa-rotate-left"
374
361
  onclick={handleReset}
375
362
  disabled={!resetDirty}
376
363
  title="Revert unsaved changes to {currentDisplayName}"
377
- >
378
- <i class="fas fa-rotate-left"></i>
379
- <span>Reset</span>
380
- </button>
364
+ >Reset</UISquareButton>
381
365
  {/if}
382
366
  </div>
383
367
  </div>
@@ -395,15 +379,22 @@
395
379
  <span>live</span>
396
380
  </span>
397
381
  </div>
398
- <span class="cfm-pill production">
399
- <span class="cfm-pill-name">{productionInfo?.name ?? '—'}</span>
400
- </span>
382
+ <FilePill
383
+ name={productionInfo?.name ?? '—'}
384
+ isProtected={productionInfo?.fileName === 'default'}
385
+ protectedTitle="Protected system config"
386
+ style="flex: 0 0 11.25rem; width: 11.25rem;"
387
+ />
401
388
  <div class="cfm-actions">
402
- <button
403
- class="cfm-btn primary apply-btn"
404
- class:saving={productionUpdateStatus === 'updating'}
405
- class:saved={productionUpdateStatus === 'done'}
406
- class:error={productionUpdateStatus === 'error'}
389
+ <UISquareButton
390
+ variant="success"
391
+ icon={productionUpdateStatus === 'updating'
392
+ ? 'fa-spinner'
393
+ : productionUpdateStatus === 'done'
394
+ ? 'fa-check'
395
+ : productionUpdateStatus === 'error'
396
+ ? 'fa-xmark'
397
+ : 'fa-arrow-down'}
407
398
  onclick={handleUpdateProduction}
408
399
  disabled={productionUpdateStatus === 'updating' || !productionInfo || (productionInfo.fileName === activeFileName && !compDirty)}
409
400
  title={!productionInfo
@@ -416,55 +407,21 @@
416
407
  ? `Save "${currentDisplayName}" and adopt`
417
408
  : `Adopt "${currentDisplayName}" from editor`}
418
409
  >
419
- <i class="fas" class:fa-arrow-down={productionUpdateStatus === 'idle'} class:fa-spinner={productionUpdateStatus === 'updating'} class:fa-check={productionUpdateStatus === 'done'} class:fa-xmark={productionUpdateStatus === 'error'}></i>
420
- <span>
421
- {#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
422
- </span>
423
- </button>
424
- <button
425
- type="button"
426
- class="cfm-info-btn"
427
- aria-label="About Save and Adopt"
428
- aria-expanded={infoOpen}
429
- bind:this={infoBtnEl}
430
- onclick={() => (infoOpen = !infoOpen)}
431
- >
432
- <i class="fas fa-circle-info"></i>
433
- </button>
434
- {#if infoOpen}
435
- <div
436
- class="cfm-info-popover"
437
- class:ready={infoPopoverReady}
438
- role="dialog"
439
- aria-label="About Save and Adopt"
440
- bind:this={infoPopoverEl}
441
- >
442
- <header class="cfm-info-header">
443
- <span class="cfm-info-title">Component Configuration</span>
444
- <button
445
- type="button"
446
- class="cfm-info-close"
447
- aria-label="Close"
448
- onclick={() => (infoOpen = false)}
449
- >
450
- <i class="fas fa-xmark"></i>
451
- </button>
452
- </header>
453
- <div class="cfm-info-body">
454
- <p>
455
- Editor and Prod both use a saved file. When they share the
456
- <em>same</em> file, <strong>Saved changes</strong> go to into production
457
- immediately. They are sharing the configuration.
458
- </p>
459
- <p>
460
- To experiment without changing production,<strong>Save As</strong> a new file first.
461
- </p>
462
- <p>
463
- When ready, click <strong>Adopt</strong> to use the new file on prod.
464
- </p>
465
- </div>
466
- </div>
467
- {/if}
410
+ {#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
411
+ </UISquareButton>
412
+ <UIInfoPopover title="Component Configuration" ariaLabel="About Save and Adopt">
413
+ <p>
414
+ Editor and Prod both use a saved file. When they share the
415
+ <em>same</em> file, <strong>Saved changes</strong> go to into production
416
+ immediately. They are sharing the configuration.
417
+ </p>
418
+ <p>
419
+ To experiment without changing production,<strong>Save As</strong> a new file first.
420
+ </p>
421
+ <p>
422
+ When ready, click <strong>Adopt</strong> to use the new file on prod.
423
+ </p>
424
+ </UIInfoPopover>
468
425
  {#if adoptFeedback}
469
426
  <span class="cfm-feedback" aria-live="polite">{adoptFeedback}</span>
470
427
  {/if}
@@ -477,33 +434,47 @@
477
434
  bind:show={saveAsDialog}
478
435
  {currentDisplayName}
479
436
  {files}
437
+ currentFileName={activeFileName}
438
+ reservedDisplayNames={files.filter((f) => f.fileName === 'default').map((f) => f.name)}
439
+ title={saveAsTitle}
440
+ description={saveAsDescription}
441
+ placeholder="Component config name…"
442
+ branchFromDefaultName={`my-${(title || 'component').toLowerCase().trim().replace(/\s+/g, '-')}`}
480
443
  onsave={confirmSaveAs}
481
444
  />
482
445
 
446
+ <SaveAsDialog
447
+ bind:show={manifestSaveAsDialog}
448
+ currentDisplayName="my-manifest"
449
+ files={manifests}
450
+ reservedDisplayNames={manifests.filter((m) => m.fileName === 'default').map((m) => m.name)}
451
+ title="Save Manifest As"
452
+ placeholder="Manifest name…"
453
+ description="Adopting a component change updates the active manifest, The default manifest is locked. Name a new manifest for the site."
454
+ reservedNameMessage='That name is reserved for the protected default manifest.'
455
+ onsave={onManifestSaveAs}
456
+ />
457
+
483
458
  <style>
484
459
  .cfm-bar {
485
460
  --cfm-applied: #5aa85e;
486
- --cfm-rail-neutral: var(--ui-border-default);
461
+ --cfm-rail-neutral: var(--ui-border);
487
462
  --cfm-rail-dirty: var(--ui-highlight);
488
463
  --cfm-rail-applied: var(--cfm-applied);
489
464
 
490
- position: sticky;
491
- top: 0;
492
- z-index: 5;
493
465
  display: flex;
494
466
  flex-direction: column;
495
467
  gap: var(--ui-space-8);
496
- padding: var(--ui-space-12);
497
- background: var(--ui-surface-low);
498
- border: 1px solid var(--ui-border-faint);
499
- border-radius: var(--ui-radius-lg);
500
468
  }
501
469
 
502
470
  .cfm-title-row {
503
471
  display: flex;
504
472
  align-items: center;
505
- gap: var(--ui-space-12);
473
+ justify-content: space-between;
474
+ gap: var(--ui-space-16);
506
475
  flex-wrap: wrap;
476
+ max-width: 34rem;
477
+ margin-bottom: var(--ui-space-16);
507
478
  }
508
479
 
509
480
  .cfm-title {
@@ -516,33 +487,12 @@
516
487
  line-height: 1.1;
517
488
  }
518
489
 
519
- .source-link {
520
- display: inline-flex;
521
- align-items: center;
522
- gap: var(--ui-space-6);
523
- margin-left: 2.5rem;
524
- height: 26px;
525
- padding: 0 14px;
526
- font-size: var(--ui-font-size-xs);
527
- font-weight: 500;
528
- color: var(--ui-text-secondary);
529
- text-decoration: none;
530
- border: 1px solid var(--ui-border-default);
531
- border-radius: 999px;
532
- transition: all var(--ui-transition-fast);
533
- }
534
-
535
- .source-link:hover {
536
- color: var(--ui-text-primary);
537
- border-color: var(--ui-border-strong);
538
- background: var(--ui-hover);
539
- }
540
-
541
490
  /* ── two-row pipeline ─────────────────────────────────────── */
542
491
  .cfm-rows {
543
492
  display: flex;
544
493
  flex-direction: column;
545
494
  gap: var(--ui-space-6);
495
+ max-width: 34rem;
546
496
  }
547
497
 
548
498
  .cfm-row {
@@ -553,7 +503,7 @@
553
503
  gap: var(--ui-space-10);
554
504
  padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-8) var(--ui-space-16);
555
505
  background: var(--ui-surface-lower);
556
- border: 1px solid var(--ui-border-subtle);
506
+ border: 1px solid var(--ui-border-low);
557
507
  border-radius: var(--ui-radius-md);
558
508
  }
559
509
 
@@ -626,41 +576,6 @@
626
576
  opacity: 1;
627
577
  }
628
578
 
629
- /* filename pill — fixed width so editor and production pills
630
- stack with their left and right edges perfectly aligned. */
631
- .cfm-pill {
632
- display: inline-flex;
633
- align-items: center;
634
- gap: var(--ui-space-6);
635
- flex: 0 0 7.5rem;
636
- width: 7.5rem;
637
- padding: var(--ui-space-6) var(--ui-space-10);
638
- background: var(--ui-surface-lowest);
639
- border: 1px solid var(--ui-border-subtle);
640
- border-radius: var(--ui-radius-md);
641
- transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
642
- }
643
-
644
- .cfm-pill.dirty {
645
- border-color: color-mix(in srgb, var(--ui-highlight) 60%, var(--ui-border-subtle));
646
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-highlight) 35%, transparent);
647
- }
648
-
649
- .cfm-pill.applied {
650
- border-color: color-mix(in srgb, var(--cfm-applied) 50%, var(--ui-border-subtle));
651
- }
652
-
653
- .cfm-pill-name {
654
- flex: 1;
655
- min-width: 0;
656
- font-size: var(--ui-font-size-md);
657
- font-weight: var(--ui-font-weight-semibold);
658
- color: var(--ui-text-primary);
659
- white-space: nowrap;
660
- overflow: hidden;
661
- text-overflow: ellipsis;
662
- }
663
-
664
579
  /* actions cluster — sits directly next to the filename pill so the
665
580
  buttons stay near the input and don't drift under the open editor panel.
666
581
  position: relative anchors the info popover below the cluster. */
@@ -714,195 +629,8 @@
714
629
  line-height: 1;
715
630
  }
716
631
 
717
- /* buttons */
718
- .cfm-btn {
719
- display: inline-flex;
720
- align-items: center;
721
- gap: var(--ui-space-6);
722
- padding: var(--ui-space-6) var(--ui-space-12);
723
- background: var(--ui-surface);
724
- border: 1px solid var(--ui-border-subtle);
725
- border-radius: var(--ui-radius-md);
726
- color: var(--ui-text-secondary);
727
- font-size: var(--ui-font-size-md);
728
- font-weight: var(--ui-font-weight-medium);
729
- cursor: pointer;
730
- transition: all var(--ui-transition-fast);
731
- white-space: nowrap;
732
- }
733
-
734
- .cfm-btn i {
735
- width: 1rem;
736
- text-align: center;
737
- font-size: 0.85em;
738
- }
739
-
740
- .cfm-btn:hover:not(:disabled) {
741
- background: var(--ui-surface-high);
742
- color: var(--ui-text-primary);
743
- border-color: var(--ui-border-default);
744
- }
745
-
746
- .cfm-btn:disabled {
747
- opacity: 0.45;
748
- cursor: not-allowed;
749
- }
750
-
751
- .cfm-btn.primary {
752
- background: color-mix(in srgb, var(--cfm-applied) 18%, var(--ui-surface-high));
753
- border-color: color-mix(in srgb, var(--cfm-applied) 45%, var(--ui-border-medium));
754
- color: var(--ui-text-primary);
755
- }
756
-
757
- .cfm-btn.primary:hover:not(:disabled) {
758
- background: color-mix(in srgb, var(--cfm-applied) 30%, var(--ui-surface-higher));
759
- border-color: color-mix(in srgb, var(--cfm-applied) 70%, var(--ui-border-strong));
760
- }
761
-
762
- .cfm-btn.primary:disabled {
763
- background: var(--ui-surface);
764
- border-color: var(--ui-border-subtle);
765
- color: var(--ui-text-muted);
766
- opacity: 1;
767
- }
768
-
769
- .cfm-btn.primary.saving i { animation: cfm-spin 1s linear infinite; }
770
- .cfm-btn.primary.saved {
771
- background: color-mix(in srgb, var(--cfm-applied) 30%, var(--ui-surface-high));
772
- color: var(--cfm-applied);
773
- }
774
- .cfm-btn.primary.error {
775
- color: var(--ui-text-muted);
776
- }
777
-
778
- @keyframes cfm-spin {
779
- from { transform: rotate(0deg); }
780
- to { transform: rotate(360deg); }
781
- }
782
-
783
632
  @keyframes cfm-pulse {
784
633
  0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
785
634
  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
786
635
  }
787
-
788
- /* info button — naked icon, no chrome. The icon itself carries the
789
- affordance; hover/active simply brighten its color. */
790
- .cfm-info-btn {
791
- display: inline-flex;
792
- align-items: center;
793
- justify-content: center;
794
- padding: var(--ui-space-2) var(--ui-space-4);
795
- background: transparent;
796
- border: 0;
797
- color: var(--ui-text-tertiary);
798
- font-size: 1.15rem;
799
- line-height: 1;
800
- cursor: pointer;
801
- transition: color var(--ui-transition-fast);
802
- }
803
-
804
- .cfm-info-btn:hover,
805
- .cfm-info-btn[aria-expanded='true'] {
806
- color: var(--ui-text-primary);
807
- }
808
-
809
- .cfm-info-popover {
810
- /* Fixed positioning escapes the sticky header's stacking context,
811
- so the popover paints over the side panels. JS in this file
812
- anchors it centered below the info button. */
813
- position: fixed;
814
- top: 0;
815
- left: 0;
816
- width: 22rem;
817
- max-width: calc(100vw - var(--ui-space-24));
818
- padding: 0;
819
- background: var(--ui-surface-higher);
820
- border: 1px solid var(--ui-border-medium);
821
- border-radius: var(--ui-radius-lg);
822
- box-shadow: var(--ui-shadow-lg);
823
- z-index: 1000;
824
- color: var(--ui-text-secondary);
825
- font-family: var(--ui-font-family, system-ui, sans-serif);
826
- overflow: hidden;
827
- visibility: hidden;
828
- animation: cfm-info-in 140ms ease-out;
829
- }
830
-
831
- .cfm-info-popover.ready {
832
- visibility: visible;
833
- }
834
-
835
- .cfm-info-header {
836
- display: flex;
837
- align-items: center;
838
- justify-content: space-between;
839
- gap: var(--ui-space-8);
840
- padding: var(--ui-space-10) var(--ui-space-12) var(--ui-space-10) var(--ui-space-16);
841
- border-bottom: 1px solid var(--ui-border-subtle);
842
- }
843
-
844
- .cfm-info-title {
845
- color: var(--ui-text-primary);
846
- font-size: var(--ui-font-size-sm);
847
- font-weight: var(--ui-font-weight-semibold);
848
- letter-spacing: -0.01em;
849
- line-height: 1.2;
850
- }
851
-
852
- .cfm-info-close {
853
- display: inline-flex;
854
- align-items: center;
855
- justify-content: center;
856
- width: var(--ui-space-24);
857
- height: var(--ui-space-24);
858
- padding: 0;
859
- background: transparent;
860
- border: 0;
861
- border-radius: var(--ui-radius-sm);
862
- color: var(--ui-text-tertiary);
863
- font-size: var(--ui-font-size-xs);
864
- line-height: 1;
865
- cursor: pointer;
866
- transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
867
- }
868
-
869
- .cfm-info-close:hover {
870
- color: var(--ui-text-primary);
871
- background: var(--ui-hover);
872
- }
873
-
874
- .cfm-info-body {
875
- padding: var(--ui-space-16);
876
- }
877
-
878
- .cfm-info-popover p {
879
- margin: 0 0 var(--ui-space-12) 0;
880
- font-size: var(--ui-font-size-xs);
881
- line-height: 1.55;
882
- }
883
-
884
- .cfm-info-popover p:last-child {
885
- margin-bottom: 0;
886
- }
887
-
888
- .cfm-info-popover strong {
889
- color: var(--ui-text-primary);
890
- font-weight: var(--ui-font-weight-semibold);
891
- }
892
-
893
- .cfm-info-popover em {
894
- font-style: italic;
895
- color: var(--ui-text-primary);
896
- }
897
-
898
- @keyframes cfm-info-in {
899
- from { opacity: 0; transform: translateY(-3px); }
900
- to { opacity: 1; transform: translateY(0); }
901
- }
902
-
903
- /* narrow viewports: hide button text, keep icons visible */
904
- @media (max-width: 640px) {
905
- .cfm-btn span { display: none; }
906
- .cfm-btn { padding: var(--ui-space-6) var(--ui-space-10); }
907
- }
908
636
  </style>