@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
@@ -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 { AliasDiskValue, 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,40 +112,30 @@
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
 
178
- function refToString(ref: CssVarRef): string {
179
- return ref.kind === 'token' ? ref.name : ref.value;
128
+ function refToDiskValue(ref: CssVarRef): AliasDiskValue {
129
+ if (ref.kind === 'token') return ref.name;
130
+ if (ref.kind === 'literal') return ref.value;
131
+ return { kind: 'gradient', value: ref.value };
180
132
  }
181
133
 
182
- function currentAliases(): Record<string, string> {
134
+ function currentAliases(): Record<string, AliasDiskValue> {
183
135
  const slice = get(editorState).components[component];
184
136
  if (!slice) return {};
185
- const out: Record<string, string> = {};
186
- for (const [k, ref] of Object.entries(slice.aliases)) out[k] = refToString(ref);
137
+ const out: Record<string, AliasDiskValue> = {};
138
+ for (const [k, ref] of Object.entries(slice.aliases)) out[k] = refToDiskValue(ref);
187
139
  return out;
188
140
  }
189
141
 
@@ -211,21 +163,24 @@
211
163
 
212
164
  async function handleSave() {
213
165
  if (activeFileName === 'default') {
214
- // Default is regenerated from source can't overwrite directly.
215
- saveAsDialog = true;
166
+ // Default is regenerated from source, so the first Save quietly opens
167
+ // Save As — same dialog as the Save As button, no scolding description.
168
+ openSaveAs();
216
169
  return;
217
170
  }
218
171
  saveStatus = 'saving';
219
172
  try {
220
173
  await persist(activeFileName, currentDisplayName);
221
- flashStatus('saved');
174
+ flashStatus(setSaveStatus, 'saved');
222
175
  await refreshFiles();
223
176
  } catch {
224
- flashStatus('error');
177
+ flashStatus(setSaveStatus, 'error');
225
178
  }
226
179
  }
227
180
 
228
181
  function openSaveAs() {
182
+ saveAsTitle = 'Save Component As';
183
+ saveAsDescription = '';
229
184
  saveAsDialog = true;
230
185
  }
231
186
 
@@ -234,10 +189,10 @@
234
189
  saveStatus = 'saving';
235
190
  try {
236
191
  await persist(fileName, displayName);
237
- flashStatus('saved');
192
+ flashStatus(setSaveStatus, 'saved');
238
193
  await refreshFiles();
239
194
  } catch {
240
- flashStatus('error');
195
+ flashStatus(setSaveStatus, 'error');
241
196
  }
242
197
  }
243
198
 
@@ -262,11 +217,16 @@
262
217
  // Multi-step service flow (delete + reload-default-on-active-removal).
263
218
  // Silent by design — see handleLoad.
264
219
  try {
220
+ // Capture before refreshFiles() reads the server's reverted active back
221
+ // into local state — otherwise the "was this the active file?" check
222
+ // below sees the post-revert value and skips the reload.
223
+ const wasActive = file.fileName === activeFileName;
265
224
  await deleteComponentConfig(component, file.fileName);
266
225
  await refreshFiles();
267
226
  await refreshProduction();
268
- if (file.fileName === activeFileName) {
269
- // Server reverts active to default; reload default aliases into the store.
227
+ if (wasActive) {
228
+ // Server reverts active to default; reload default aliases into the store
229
+ // so the deleted file's CSS vars are replaced by default's.
270
230
  const defaultCfg = await loadComponentConfig(component, 'default');
271
231
  loadComponentActive(component, 'default', defaultCfg.aliases, defaultCfg.config, defaultCfg.schemaVersion ?? 0);
272
232
  activeFileName = 'default';
@@ -283,6 +243,9 @@
283
243
  // regenerated from source and can't be overwritten, so route to Save As
284
244
  // and bail; the user can re-trigger Adopt after the new file is saved.
285
245
  if (compDirty && activeFileName === 'default') {
246
+ saveAsTitle = 'Save Component As';
247
+ saveAsDescription =
248
+ 'Adopting pushes this component to production. The default config is read-only, so save your edits to a new file first.';
286
249
  saveAsDialog = true;
287
250
  return;
288
251
  }
@@ -300,10 +263,38 @@
300
263
  adoptFeedback = wasDirty
301
264
  ? `Saved "${adoptingName}" and adopted`
302
265
  : `Adopted "${adoptingName}"`;
303
- flashProductionStatus('done');
304
- } catch {
266
+ flashStatus(setProductionStatus, 'done', { onIdle: () => (adoptFeedback = '') });
267
+ } catch (err) {
268
+ const e = err as Error & { code?: string };
269
+ if (e.code === 'ACTIVE_IS_PROTECTED') {
270
+ adoptFeedback = '';
271
+ productionUpdateStatus = 'idle';
272
+ retryAdoptAfterManifestSave = true;
273
+ try {
274
+ manifests = await listManifests();
275
+ } catch {
276
+ manifests = [];
277
+ }
278
+ manifestSaveAsDialog = true;
279
+ return;
280
+ }
305
281
  adoptFeedback = '';
306
- flashProductionStatus('error');
282
+ flashStatus(setProductionStatus, 'error', { onIdle: () => (adoptFeedback = '') });
283
+ }
284
+ }
285
+
286
+ async function onManifestSaveAs(detail: { displayName: string; fileName: string }) {
287
+ manifestSaveAsDialog = false;
288
+ try {
289
+ await saveAsManifest(detail.fileName, detail.displayName);
290
+ } catch (err) {
291
+ window.alert(`Failed to create manifest: ${(err as Error).message}`);
292
+ retryAdoptAfterManifestSave = false;
293
+ return;
294
+ }
295
+ if (retryAdoptAfterManifestSave) {
296
+ retryAdoptAfterManifestSave = false;
297
+ await handleUpdateProduction();
307
298
  }
308
299
  }
309
300
 
@@ -324,14 +315,11 @@
324
315
  <h2 class="cfm-title">{title}</h2>
325
316
  {/if}
326
317
  {#if sourceFile && projectRoot}
327
- <a
328
- class="source-link"
318
+ <UIPillButton
319
+ icon="fa-code"
329
320
  href="vscode://file/{projectRoot}/{sourceFile}"
330
321
  title="Open {sourceFile} in VS Code"
331
- >
332
- <i class="fas fa-code"></i>
333
- <span>Show component source</span>
334
- </a>
322
+ >Show component source</UIPillButton>
335
323
  {/if}
336
324
  </div>
337
325
 
@@ -345,18 +333,19 @@
345
333
  <span>{compDirty ? 'unsaved' : isApplied ? 'live' : 'saved'}</span>
346
334
  </span>
347
335
  </div>
348
- <span
349
- class="cfm-pill"
350
- class:dirty={compDirty}
351
- class:applied={isApplied}
336
+ <FilePill
337
+ name={currentDisplayName}
338
+ isProtected={activeFileName === 'default'}
339
+ dirty={compDirty}
340
+ applied={isApplied}
341
+ protectedTitle="Protected system config"
352
342
  title={compDirty
353
343
  ? 'Unsaved changes'
354
344
  : isApplied
355
345
  ? 'Active config is applied to production'
356
346
  : ''}
357
- >
358
- <span class="cfm-pill-name">{currentDisplayName}</span>
359
- </span>
347
+ style="flex: 0 1 11.25rem; min-width: 0; max-width: 11.25rem;"
348
+ />
360
349
  <div class="cfm-actions">
361
350
  <ComponentFileMenu
362
351
  {component}
@@ -369,15 +358,12 @@
369
358
  ondelete={handleDelete}
370
359
  />
371
360
  {#if resetVariables}
372
- <button
373
- class="cfm-btn reset-btn"
361
+ <UISquareButton
362
+ icon="fa-rotate-left"
374
363
  onclick={handleReset}
375
364
  disabled={!resetDirty}
376
365
  title="Revert unsaved changes to {currentDisplayName}"
377
- >
378
- <i class="fas fa-rotate-left"></i>
379
- <span>Reset</span>
380
- </button>
366
+ >Reset</UISquareButton>
381
367
  {/if}
382
368
  </div>
383
369
  </div>
@@ -395,15 +381,22 @@
395
381
  <span>live</span>
396
382
  </span>
397
383
  </div>
398
- <span class="cfm-pill production">
399
- <span class="cfm-pill-name">{productionInfo?.name ?? '—'}</span>
400
- </span>
384
+ <FilePill
385
+ name={productionInfo?.name ?? '—'}
386
+ isProtected={productionInfo?.fileName === 'default'}
387
+ protectedTitle="Protected system config"
388
+ style="flex: 0 1 11.25rem; min-width: 0; max-width: 11.25rem;"
389
+ />
401
390
  <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'}
391
+ <UISquareButton
392
+ variant="success"
393
+ icon={productionUpdateStatus === 'updating'
394
+ ? 'fa-spinner'
395
+ : productionUpdateStatus === 'done'
396
+ ? 'fa-check'
397
+ : productionUpdateStatus === 'error'
398
+ ? 'fa-xmark'
399
+ : 'fa-arrow-down'}
407
400
  onclick={handleUpdateProduction}
408
401
  disabled={productionUpdateStatus === 'updating' || !productionInfo || (productionInfo.fileName === activeFileName && !compDirty)}
409
402
  title={!productionInfo
@@ -416,55 +409,21 @@
416
409
  ? `Save "${currentDisplayName}" and adopt`
417
410
  : `Adopt "${currentDisplayName}" from editor`}
418
411
  >
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}
412
+ {#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
413
+ </UISquareButton>
414
+ <UIInfoPopover title="Component Configuration" ariaLabel="About Save and Adopt">
415
+ <p>
416
+ Editor and Prod both use a saved file. When they share the
417
+ <em>same</em> file, <strong>Saved changes</strong> go to into production
418
+ immediately. They are sharing the configuration.
419
+ </p>
420
+ <p>
421
+ To experiment without changing production,<strong>Save As</strong> a new file first.
422
+ </p>
423
+ <p>
424
+ When ready, click <strong>Adopt</strong> to use the new file on prod.
425
+ </p>
426
+ </UIInfoPopover>
468
427
  {#if adoptFeedback}
469
428
  <span class="cfm-feedback" aria-live="polite">{adoptFeedback}</span>
470
429
  {/if}
@@ -477,33 +436,47 @@
477
436
  bind:show={saveAsDialog}
478
437
  {currentDisplayName}
479
438
  {files}
439
+ currentFileName={activeFileName}
440
+ reservedDisplayNames={files.filter((f) => f.fileName === 'default').map((f) => f.name)}
441
+ title={saveAsTitle}
442
+ description={saveAsDescription}
443
+ placeholder="Component config name…"
444
+ branchFromDefaultName={`my-${(title || 'component').toLowerCase().trim().replace(/\s+/g, '-')}`}
480
445
  onsave={confirmSaveAs}
481
446
  />
482
447
 
448
+ <SaveAsDialog
449
+ bind:show={manifestSaveAsDialog}
450
+ currentDisplayName="my-manifest"
451
+ files={manifests}
452
+ reservedDisplayNames={manifests.filter((m) => m.fileName === 'default').map((m) => m.name)}
453
+ title="Save Manifest As"
454
+ placeholder="Manifest name…"
455
+ description="Adopting a component change updates the active manifest, The default manifest is locked. Name a new manifest for the site."
456
+ reservedNameMessage='That name is reserved for the protected default manifest.'
457
+ onsave={onManifestSaveAs}
458
+ />
459
+
483
460
  <style>
484
461
  .cfm-bar {
485
462
  --cfm-applied: #5aa85e;
486
- --cfm-rail-neutral: var(--ui-border-default);
463
+ --cfm-rail-neutral: var(--ui-border);
487
464
  --cfm-rail-dirty: var(--ui-highlight);
488
465
  --cfm-rail-applied: var(--cfm-applied);
489
466
 
490
- position: sticky;
491
- top: 0;
492
- z-index: 5;
493
467
  display: flex;
494
468
  flex-direction: column;
495
469
  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
470
  }
501
471
 
502
472
  .cfm-title-row {
503
473
  display: flex;
504
474
  align-items: center;
505
- gap: var(--ui-space-12);
475
+ justify-content: space-between;
476
+ gap: var(--ui-space-16);
506
477
  flex-wrap: wrap;
478
+ max-width: 34rem;
479
+ margin-bottom: var(--ui-space-16);
507
480
  }
508
481
 
509
482
  .cfm-title {
@@ -512,37 +485,15 @@
512
485
  font-size: var(--ui-font-size-3xl);
513
486
  font-weight: var(--ui-font-weight-semibold);
514
487
  color: var(--ui-text-primary);
515
- letter-spacing: -0.015em;
516
488
  line-height: 1.1;
517
489
  }
518
490
 
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
491
  /* ── two-row pipeline ─────────────────────────────────────── */
542
492
  .cfm-rows {
543
493
  display: flex;
544
494
  flex-direction: column;
545
495
  gap: var(--ui-space-6);
496
+ max-width: 34rem;
546
497
  }
547
498
 
548
499
  .cfm-row {
@@ -553,7 +504,7 @@
553
504
  gap: var(--ui-space-10);
554
505
  padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-8) var(--ui-space-16);
555
506
  background: var(--ui-surface-lower);
556
- border: 1px solid var(--ui-border-subtle);
507
+ border: 1px solid var(--ui-border-low);
557
508
  border-radius: var(--ui-radius-md);
558
509
  }
559
510
 
@@ -586,7 +537,6 @@
586
537
  font-size: var(--ui-font-size-xs);
587
538
  font-weight: var(--ui-font-weight-semibold);
588
539
  text-transform: uppercase;
589
- letter-spacing: 0.08em;
590
540
  color: var(--ui-text-secondary);
591
541
  line-height: 1.1;
592
542
  }
@@ -596,7 +546,6 @@
596
546
  align-items: center;
597
547
  gap: var(--ui-space-4);
598
548
  font-size: 0.75rem;
599
- letter-spacing: 0.02em;
600
549
  color: var(--ui-text-muted);
601
550
  line-height: 1;
602
551
  }
@@ -626,41 +575,6 @@
626
575
  opacity: 1;
627
576
  }
628
577
 
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
578
  /* actions cluster — sits directly next to the filename pill so the
665
579
  buttons stay near the input and don't drift under the open editor panel.
666
580
  position: relative anchors the info popover below the cluster. */
@@ -714,195 +628,8 @@
714
628
  line-height: 1;
715
629
  }
716
630
 
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
631
  @keyframes cfm-pulse {
784
632
  0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
785
633
  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
786
634
  }
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
635
  </style>