@motion-proto/live-tokens 0.1.1 → 0.3.2
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.
- package/README.md +168 -21
- package/dist-plugin/index.cjs +823 -336
- package/dist-plugin/index.d.cts +9 -7
- package/dist-plugin/index.d.ts +9 -7
- package/dist-plugin/index.js +822 -335
- package/package.json +46 -20
- package/src/assets/newspaper.webp +0 -0
- package/src/assets/offering.webp +0 -0
- package/src/component-editor/BadgeEditor.svelte +170 -0
- package/src/component-editor/CalloutEditor.svelte +103 -0
- package/src/component-editor/CardEditor.svelte +184 -0
- package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
- package/src/component-editor/CornerBadgeEditor.svelte +207 -0
- package/src/component-editor/DialogEditor.svelte +172 -0
- package/src/component-editor/ImageEditor.svelte +72 -0
- package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
- package/src/component-editor/NotificationEditor.svelte +160 -0
- package/src/component-editor/ProgressBarEditor.svelte +124 -0
- package/src/component-editor/RadioButtonEditor.svelte +140 -0
- package/src/component-editor/SectionDividerEditor.svelte +263 -0
- package/src/component-editor/SegmentedControlEditor.svelte +154 -0
- package/src/component-editor/StandardButtonsEditor.svelte +178 -0
- package/src/component-editor/TabBarEditor.svelte +137 -0
- package/src/component-editor/TableEditor.svelte +128 -0
- package/src/component-editor/TooltipEditor.svelte +122 -0
- package/src/component-editor/editorTokens.test.ts +93 -0
- package/src/component-editor/groupKeySlots.test.ts +67 -0
- package/src/component-editor/groupKeySnapshot.test.ts +52 -0
- package/src/component-editor/index.ts +5 -0
- package/src/component-editor/registry.ts +246 -0
- package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
- package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
- package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
- package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
- package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
- package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
- package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
- package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
- package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
- package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
- package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
- package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
- package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
- package/src/component-editor/scaffolding/componentSources.ts +9 -0
- package/src/component-editor/scaffolding/defaultSections.ts +16 -0
- package/src/component-editor/scaffolding/editorContext.ts +44 -0
- package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
- package/src/component-editor/scaffolding/siblings.ts +33 -0
- package/src/component-editor/scaffolding/types.ts +39 -0
- package/src/components/Badge.svelte +231 -42
- package/src/components/Button.svelte +324 -124
- package/src/components/Callout.svelte +145 -0
- package/src/components/Card.svelte +123 -25
- package/src/components/CollapsibleSection.svelte +213 -35
- package/src/components/CornerBadge.svelte +224 -0
- package/src/components/Dialog.svelte +137 -114
- package/src/components/Image.svelte +43 -0
- package/src/components/InlineEditActions.svelte +74 -14
- package/src/components/Notification.svelte +184 -163
- package/src/components/ProgressBar.svelte +216 -22
- package/src/components/RadioButton.svelte +110 -40
- package/src/components/SectionDivider.svelte +428 -74
- package/src/components/SegmentedControl.svelte +203 -0
- package/src/components/TabBar.svelte +146 -21
- package/src/components/Table.svelte +102 -0
- package/src/components/Tooltip.svelte +45 -19
- package/src/components/types.ts +51 -0
- package/src/data/google-fonts.json +75 -0
- package/src/lib/ColumnsOverlay.svelte +20 -7
- package/src/lib/LiveEditorOverlay.svelte +257 -78
- package/src/lib/columnsOverlay.ts +21 -17
- package/src/lib/componentConfig.test.ts +204 -0
- package/src/lib/componentConfigKeys.ts +19 -0
- package/src/lib/componentConfigService.ts +88 -0
- package/src/lib/copyPopover.ts +30 -0
- package/src/lib/cssVarSync.ts +59 -7
- package/src/lib/editorConfigStore.ts +0 -10
- package/src/lib/editorCore.ts +402 -0
- package/src/lib/editorKeybindings.ts +52 -0
- package/src/lib/editorPersistence.ts +106 -0
- package/src/lib/editorRenderer.ts +74 -0
- package/src/lib/editorStore.test.ts +328 -0
- package/src/lib/editorStore.ts +412 -0
- package/src/lib/editorTypes.ts +100 -0
- package/src/lib/editorViewStore.ts +55 -0
- package/src/lib/files/versionedFileResource.ts +140 -0
- package/src/lib/fontLoader.ts +130 -0
- package/src/lib/fontMigration.ts +140 -0
- package/src/lib/fontParse.ts +168 -0
- package/src/lib/index.ts +48 -30
- package/src/lib/lazyConfig.test.ts +54 -0
- package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
- package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
- package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
- package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
- package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
- package/src/lib/migrations/index.ts +93 -0
- package/src/lib/migrations/migrations.test.ts +341 -0
- package/src/lib/navLinkTypes.ts +1 -0
- package/src/lib/overlayState.ts +3 -0
- package/src/lib/paletteDerivation.ts +300 -0
- package/src/lib/parentRouteStore.ts +42 -0
- package/src/lib/parsers/globalRootBlock.ts +32 -0
- package/src/lib/presetService.ts +94 -0
- package/src/lib/router.ts +42 -10
- package/src/lib/scrollSection.ts +45 -0
- package/src/lib/slices/columns.ts +59 -0
- package/src/lib/slices/components.ts +362 -0
- package/src/lib/slices/domainVars.ts +15 -0
- package/src/lib/slices/fonts.ts +30 -0
- package/src/lib/slices/gradients.ts +153 -0
- package/src/lib/slices/overlays.ts +132 -0
- package/src/lib/slices/palettes.ts +26 -0
- package/src/lib/slices/shadows.ts +123 -0
- package/src/lib/storage.ts +88 -0
- package/src/lib/themeInit.ts +74 -0
- package/src/lib/themeService.ts +101 -0
- package/src/lib/themeTypes.ts +146 -0
- package/src/lib/tokenRegistry.ts +148 -0
- package/src/pages/ComponentEditorPage.svelte +384 -0
- package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
- package/src/pages/Editor.svelte +98 -0
- package/src/pages/Editor.svelte.d.ts +2 -0
- package/src/pages/EditorShell.svelte +348 -0
- package/src/styles/_padding.scss +34 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
- package/src/styles/fonts.css +22 -10
- package/src/styles/form-controls.css +14 -16
- package/src/styles/tokens.css +1322 -0
- package/src/styles/ui-editor.css +126 -0
- package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
- package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
- package/src/ui/EditorViewSwitcher.svelte +180 -0
- package/src/ui/FontStackEditor.svelte +360 -0
- package/src/ui/GradientEditor.svelte +461 -0
- package/src/ui/GradientStopPicker.svelte +74 -0
- package/src/ui/PaletteEditor.svelte +1590 -0
- package/src/ui/PaletteEditor.test.ts +108 -0
- package/src/ui/PresetFileManager.svelte +567 -0
- package/src/ui/ProjectFontsSection.svelte +645 -0
- package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
- package/src/{showcase → ui}/TextTab.svelte +27 -27
- package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
- package/src/ui/Toggle.svelte +108 -0
- package/src/ui/UICopyPopover.svelte +78 -0
- package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
- package/src/ui/UIFontFamilySelector.svelte +309 -0
- package/src/ui/UIFontSizeSelector.svelte +165 -0
- package/src/ui/UIFontWeightSelector.svelte +52 -0
- package/src/ui/UILineHeightSelector.svelte +47 -0
- package/src/ui/UILinkToggle.svelte +60 -0
- package/src/ui/UIOptionItem.svelte +74 -0
- package/src/ui/UIOptionList.svelte +27 -0
- package/src/ui/UIPaddingSelector.svelte +661 -0
- package/src/ui/UIPaletteSelector.svelte +1084 -0
- package/src/ui/UIRadio.svelte +72 -0
- package/src/ui/UIRadioGroup.svelte +59 -0
- package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
- package/src/ui/UITokenSelector.svelte +509 -0
- package/src/ui/UIVariantSelector.svelte +145 -0
- package/src/ui/VariablesTab.svelte +252 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/keepInViewport.ts +84 -0
- package/src/ui/palette/GradientStopEditor.svelte +482 -0
- package/src/ui/palette/OverridesPanel.svelte +526 -0
- package/src/ui/palette/PaletteBase.svelte +165 -0
- package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
- package/src/ui/palette/paletteEditorState.ts +89 -0
- package/src/ui/sections/ColumnsSection.svelte +273 -0
- package/src/ui/sections/GradientsSection.svelte +147 -0
- package/src/ui/sections/OverlaysSection.svelte +670 -0
- package/src/ui/sections/ShadowsSection.svelte +1250 -0
- package/src/ui/sections/TokenScaleTable.svelte +332 -0
- package/src/ui/sections/tokenScales.ts +81 -0
- package/src/ui/variantScales.ts +108 -0
- package/src/components/DetailNav.svelte +0 -78
- package/src/components/Toggle.svelte +0 -86
- package/src/lib/tokenInit.ts +0 -29
- package/src/lib/tokenService.ts +0 -144
- package/src/lib/tokenTypes.ts +0 -45
- package/src/pages/Admin.svelte +0 -100
- package/src/pages/ShowcasePage.svelte +0 -144
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -105
- package/src/showcase/PaletteEditor.svelte +0 -2579
- package/src/showcase/PaletteSelector.svelte +0 -627
- package/src/showcase/TokenMap.svelte +0 -54
- package/src/showcase/VariablesTab.svelte +0 -2655
- package/src/showcase/VisualsTab.svelte +0 -231
- package/src/showcase/demos/BadgeDemo.svelte +0 -56
- package/src/showcase/demos/CardDemo.svelte +0 -50
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
- package/src/showcase/demos/NotificationDemo.svelte +0 -147
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
- package/src/showcase/demos/TabBarDemo.svelte +0 -58
- package/src/showcase/demos/TooltipDemo.svelte +0 -52
- package/src/showcase/editor.css +0 -93
- package/src/showcase/index.ts +0 -17
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +0 -97
- package/src/styles/fonts/Domine/README.txt +0 -66
- /package/src/{showcase → ui}/curveEngine.ts +0 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
declare const __PROJECT_ROOT__: string | undefined;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { onMount, onDestroy } from 'svelte';
|
|
7
|
+
import { get } from 'svelte/store';
|
|
8
|
+
import type { ComponentConfig, ComponentConfigMeta } from '../../lib/themeTypes';
|
|
9
|
+
import { componentSourceFile } from './componentSources';
|
|
10
|
+
import {
|
|
11
|
+
loadComponentConfig,
|
|
12
|
+
saveComponentConfig,
|
|
13
|
+
deleteComponentConfig,
|
|
14
|
+
setActiveComponentFile,
|
|
15
|
+
setComponentProductionFile,
|
|
16
|
+
type ComponentProductionInfo,
|
|
17
|
+
} from '../../lib/componentConfigService';
|
|
18
|
+
import {
|
|
19
|
+
editorState,
|
|
20
|
+
componentDirty,
|
|
21
|
+
loadComponentActive,
|
|
22
|
+
markComponentSaved,
|
|
23
|
+
} from '../../lib/editorStore';
|
|
24
|
+
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../lib/migrations';
|
|
25
|
+
import type { CssVarRef } from '../../lib/editorTypes';
|
|
26
|
+
import { safeFetch } from '../../lib/storage';
|
|
27
|
+
import ComponentFileMenu from './ComponentFileMenu.svelte';
|
|
28
|
+
import SaveAsDialog from './SaveAsDialog.svelte';
|
|
29
|
+
|
|
30
|
+
/** Which component this manager controls (e.g. "button"). */
|
|
31
|
+
export let component: string;
|
|
32
|
+
/** Display name shown at the start of the bar (e.g. "Segmented Control"). */
|
|
33
|
+
export let title: string = '';
|
|
34
|
+
/** When provided, renders a Reset button that reverts the component to its
|
|
35
|
+
currently-loaded config file (discarding unsaved edits). To switch
|
|
36
|
+
configs or return to default, use the File menu. */
|
|
37
|
+
export let resetVariables: string[] | null = null;
|
|
38
|
+
|
|
39
|
+
const projectRoot: string =
|
|
40
|
+
typeof __PROJECT_ROOT__ !== 'undefined' ? (__PROJECT_ROOT__ ?? '') : '';
|
|
41
|
+
$: sourceFile = componentSourceFile(component);
|
|
42
|
+
|
|
43
|
+
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
|
44
|
+
let saveStatus: SaveStatus = 'idle';
|
|
45
|
+
|
|
46
|
+
/** Show a transient saveStatus (saved/error) and revert to idle after 2s.
|
|
47
|
+
Centralises the timing so all flash sites stay in sync. */
|
|
48
|
+
function flashStatus(state: Exclude<SaveStatus, 'idle'>) {
|
|
49
|
+
saveStatus = state;
|
|
50
|
+
setTimeout(() => (saveStatus = 'idle'), 2000);
|
|
51
|
+
}
|
|
52
|
+
let files: ComponentConfigMeta[] = [];
|
|
53
|
+
let activeFileName = 'default';
|
|
54
|
+
let currentDisplayName = 'Default';
|
|
55
|
+
let saveAsDialog = false;
|
|
56
|
+
|
|
57
|
+
let productionInfo: ComponentProductionInfo | null = null;
|
|
58
|
+
type ProductionStatus = 'idle' | 'updating' | 'done' | 'error';
|
|
59
|
+
let productionUpdateStatus: ProductionStatus = 'idle';
|
|
60
|
+
let adoptFeedback = '';
|
|
61
|
+
|
|
62
|
+
/** Same idle-after-2s pattern for the production-update flash. */
|
|
63
|
+
function flashProductionStatus(state: Exclude<ProductionStatus, 'idle'>) {
|
|
64
|
+
productionUpdateStatus = state;
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
productionUpdateStatus = 'idle';
|
|
67
|
+
adoptFeedback = '';
|
|
68
|
+
}, 2000);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$: compDirty = $componentDirty[component] ?? false;
|
|
72
|
+
$: isApplied = !!productionInfo && productionInfo.fileName === activeFileName && !compDirty;
|
|
73
|
+
$: resetDirty = !!resetVariables && compDirty;
|
|
74
|
+
|
|
75
|
+
async function refreshFiles() {
|
|
76
|
+
// safeFetch returns null on dev-server unavailable / non-2xx — silently
|
|
77
|
+
// leave the file list empty in that case.
|
|
78
|
+
const data = await safeFetch<{
|
|
79
|
+
files: ComponentConfigMeta[];
|
|
80
|
+
activeFile: string;
|
|
81
|
+
}>(`/api/component-configs/${encodeURIComponent(component)}`);
|
|
82
|
+
if (!data) return;
|
|
83
|
+
files = data.files;
|
|
84
|
+
activeFileName = data.activeFile;
|
|
85
|
+
const active = files.find((f) => f.fileName === activeFileName);
|
|
86
|
+
if (active) currentDisplayName = active.name;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function refreshProduction() {
|
|
90
|
+
// Preserve existing productionInfo on transient fetch failure rather than
|
|
91
|
+
// clobbering it to null — same behaviour as the previous empty catch.
|
|
92
|
+
const info = await safeFetch<ComponentProductionInfo>(
|
|
93
|
+
`/api/component-configs/${encodeURIComponent(component)}/production`,
|
|
94
|
+
);
|
|
95
|
+
if (info) productionInfo = info;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onMount(async () => {
|
|
99
|
+
await refreshFiles();
|
|
100
|
+
await refreshProduction();
|
|
101
|
+
window.addEventListener('keydown', handleKeydown);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
onDestroy(() => {
|
|
105
|
+
window.removeEventListener('keydown', handleKeydown);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
109
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
handleSave();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function refToString(ref: CssVarRef): string {
|
|
116
|
+
return ref.kind === 'token' ? ref.name : ref.value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function currentAliases(): Record<string, string> {
|
|
120
|
+
const slice = get(editorState).components[component];
|
|
121
|
+
if (!slice) return {};
|
|
122
|
+
const out: Record<string, string> = {};
|
|
123
|
+
for (const [k, ref] of Object.entries(slice.aliases)) out[k] = refToString(ref);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function currentConfig(): Record<string, unknown> {
|
|
128
|
+
return get(editorState).components[component]?.config ?? {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function persist(fileName: string, displayName: string): Promise<void> {
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
const data: ComponentConfig = {
|
|
134
|
+
name: displayName,
|
|
135
|
+
component,
|
|
136
|
+
createdAt: now,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
aliases: { ...currentAliases() },
|
|
139
|
+
config: { ...currentConfig() },
|
|
140
|
+
schemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
141
|
+
};
|
|
142
|
+
await saveComponentConfig(component, fileName, data);
|
|
143
|
+
await setActiveComponentFile(component, fileName);
|
|
144
|
+
activeFileName = fileName;
|
|
145
|
+
currentDisplayName = displayName;
|
|
146
|
+
markComponentSaved(component);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleSave() {
|
|
150
|
+
if (activeFileName === 'default') {
|
|
151
|
+
// Default is regenerated from source — can't overwrite directly.
|
|
152
|
+
saveAsDialog = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
saveStatus = 'saving';
|
|
156
|
+
try {
|
|
157
|
+
await persist(activeFileName, currentDisplayName);
|
|
158
|
+
flashStatus('saved');
|
|
159
|
+
await refreshFiles();
|
|
160
|
+
} catch {
|
|
161
|
+
flashStatus('error');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function openSaveAs() {
|
|
166
|
+
saveAsDialog = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function confirmSaveAs(e: CustomEvent<{ displayName: string; fileName: string }>) {
|
|
170
|
+
const { displayName, fileName } = e.detail;
|
|
171
|
+
saveStatus = 'saving';
|
|
172
|
+
try {
|
|
173
|
+
await persist(fileName, displayName);
|
|
174
|
+
flashStatus('saved');
|
|
175
|
+
await refreshFiles();
|
|
176
|
+
} catch {
|
|
177
|
+
flashStatus('error');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function handleLoad(e: CustomEvent<ComponentConfigMeta>) {
|
|
182
|
+
const file = e.detail;
|
|
183
|
+
// Multi-step service flow (load + set-active) — if any network call
|
|
184
|
+
// fails, the dialog is already closed and the local state stays on the
|
|
185
|
+
// previous selection. Silent by design; the same boot resilience that
|
|
186
|
+
// keeps the editor working without a dev-server applies here.
|
|
187
|
+
try {
|
|
188
|
+
const cfg = await loadComponentConfig(component, file.fileName);
|
|
189
|
+
await setActiveComponentFile(component, file.fileName);
|
|
190
|
+
loadComponentActive(component, file.fileName, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
|
|
191
|
+
activeFileName = file.fileName;
|
|
192
|
+
currentDisplayName = file.name;
|
|
193
|
+
} catch {
|
|
194
|
+
// intentional: see comment above
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function handleDelete(e: CustomEvent<ComponentConfigMeta>) {
|
|
199
|
+
const file = e.detail;
|
|
200
|
+
if (file.fileName === 'default') return;
|
|
201
|
+
// Multi-step service flow (delete + reload-default-on-active-removal).
|
|
202
|
+
// Silent by design — see handleLoad.
|
|
203
|
+
try {
|
|
204
|
+
await deleteComponentConfig(component, file.fileName);
|
|
205
|
+
await refreshFiles();
|
|
206
|
+
await refreshProduction();
|
|
207
|
+
if (file.fileName === activeFileName) {
|
|
208
|
+
// Server reverts active to default; reload default aliases into the store.
|
|
209
|
+
const defaultCfg = await loadComponentConfig(component, 'default');
|
|
210
|
+
loadComponentActive(component, 'default', defaultCfg.aliases, defaultCfg.config, defaultCfg.schemaVersion ?? 0);
|
|
211
|
+
activeFileName = 'default';
|
|
212
|
+
currentDisplayName = 'Default';
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// intentional: see comment above
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function handleUpdateProduction() {
|
|
220
|
+
// If the editor has unsaved edits, persist them first so production adopts
|
|
221
|
+
// the actual current state — not a stale snapshot. The `default` file is
|
|
222
|
+
// regenerated from source and can't be overwritten, so route to Save As
|
|
223
|
+
// and bail; the user can re-trigger Adopt after the new file is saved.
|
|
224
|
+
if (compDirty && activeFileName === 'default') {
|
|
225
|
+
saveAsDialog = true;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const wasDirty = compDirty;
|
|
229
|
+
const adoptingName = currentDisplayName;
|
|
230
|
+
productionUpdateStatus = 'updating';
|
|
231
|
+
try {
|
|
232
|
+
if (wasDirty) {
|
|
233
|
+
await persist(activeFileName, currentDisplayName);
|
|
234
|
+
await refreshFiles();
|
|
235
|
+
}
|
|
236
|
+
await setComponentProductionFile(component, activeFileName);
|
|
237
|
+
await refreshProduction();
|
|
238
|
+
adoptFeedback = wasDirty
|
|
239
|
+
? `Saved "${adoptingName}" and adopted`
|
|
240
|
+
: `Adopted "${adoptingName}"`;
|
|
241
|
+
flashProductionStatus('done');
|
|
242
|
+
} catch {
|
|
243
|
+
adoptFeedback = '';
|
|
244
|
+
flashProductionStatus('error');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function handleReset() {
|
|
249
|
+
if (!resetVariables) return;
|
|
250
|
+
try {
|
|
251
|
+
const cfg = await loadComponentConfig(component, activeFileName);
|
|
252
|
+
loadComponentActive(component, activeFileName, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
|
|
253
|
+
} catch {
|
|
254
|
+
// intentional: dev-server unavailable — leave state untouched
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
</script>
|
|
258
|
+
|
|
259
|
+
<header class="cfm-bar">
|
|
260
|
+
<div class="cfm-title-row">
|
|
261
|
+
{#if title}
|
|
262
|
+
<h2 class="cfm-title">{title}</h2>
|
|
263
|
+
{/if}
|
|
264
|
+
{#if sourceFile && projectRoot}
|
|
265
|
+
<a
|
|
266
|
+
class="source-link"
|
|
267
|
+
href="vscode://file/{projectRoot}/{sourceFile}"
|
|
268
|
+
title="Open {sourceFile} in VS Code"
|
|
269
|
+
>
|
|
270
|
+
<i class="fas fa-code"></i>
|
|
271
|
+
<span>Source</span>
|
|
272
|
+
</a>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="cfm-rows" class:in-sync={isApplied} class:promotable={compDirty || (!!productionInfo && productionInfo.fileName !== activeFileName)}>
|
|
277
|
+
<div class="cfm-row cfm-row-editor" class:dirty={compDirty} class:applied={isApplied}>
|
|
278
|
+
<span class="cfm-rail" aria-hidden="true"></span>
|
|
279
|
+
<div class="cfm-row-head">
|
|
280
|
+
<span class="cfm-row-label">Editor</span>
|
|
281
|
+
<span class="cfm-row-status" class:dirty={compDirty} class:applied={isApplied}>
|
|
282
|
+
<i class="cfm-status-dot" aria-hidden="true"></i>
|
|
283
|
+
<span>{compDirty ? 'unsaved' : isApplied ? 'live' : 'saved'}</span>
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
<span
|
|
287
|
+
class="cfm-pill"
|
|
288
|
+
class:dirty={compDirty}
|
|
289
|
+
class:applied={isApplied}
|
|
290
|
+
title={compDirty
|
|
291
|
+
? 'Unsaved changes'
|
|
292
|
+
: isApplied
|
|
293
|
+
? 'Active config is applied to production'
|
|
294
|
+
: ''}
|
|
295
|
+
>
|
|
296
|
+
<span class="cfm-pill-name">{currentDisplayName}</span>
|
|
297
|
+
</span>
|
|
298
|
+
<div class="cfm-actions">
|
|
299
|
+
<ComponentFileMenu
|
|
300
|
+
{component}
|
|
301
|
+
{files}
|
|
302
|
+
{activeFileName}
|
|
303
|
+
on:save={handleSave}
|
|
304
|
+
on:saveAs={openSaveAs}
|
|
305
|
+
on:openLoad={refreshFiles}
|
|
306
|
+
on:load={handleLoad}
|
|
307
|
+
on:delete={handleDelete}
|
|
308
|
+
/>
|
|
309
|
+
{#if resetVariables}
|
|
310
|
+
<button
|
|
311
|
+
class="cfm-btn reset-btn"
|
|
312
|
+
on:click={handleReset}
|
|
313
|
+
disabled={!resetDirty}
|
|
314
|
+
title="Revert unsaved changes to {currentDisplayName}"
|
|
315
|
+
>
|
|
316
|
+
<i class="fas fa-rotate-left"></i>
|
|
317
|
+
<span>Reset</span>
|
|
318
|
+
</button>
|
|
319
|
+
{/if}
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="cfm-promote" aria-hidden="true">
|
|
324
|
+
<i class="fas fa-arrow-down cfm-promote-icon"></i>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div class="cfm-row cfm-row-production" class:applied={isApplied}>
|
|
328
|
+
<span class="cfm-rail" aria-hidden="true"></span>
|
|
329
|
+
<div class="cfm-row-head">
|
|
330
|
+
<span class="cfm-row-label">Prod</span>
|
|
331
|
+
<span class="cfm-row-status applied">
|
|
332
|
+
<i class="cfm-status-dot" aria-hidden="true"></i>
|
|
333
|
+
<span>live</span>
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
<span class="cfm-pill production">
|
|
337
|
+
<span class="cfm-pill-name">{productionInfo?.name ?? '—'}</span>
|
|
338
|
+
</span>
|
|
339
|
+
<div class="cfm-actions">
|
|
340
|
+
<button
|
|
341
|
+
class="cfm-btn primary apply-btn"
|
|
342
|
+
class:saving={productionUpdateStatus === 'updating'}
|
|
343
|
+
class:saved={productionUpdateStatus === 'done'}
|
|
344
|
+
class:error={productionUpdateStatus === 'error'}
|
|
345
|
+
on:click={handleUpdateProduction}
|
|
346
|
+
disabled={productionUpdateStatus === 'updating' || !productionInfo || (productionInfo.fileName === activeFileName && !compDirty)}
|
|
347
|
+
title={!productionInfo
|
|
348
|
+
? ''
|
|
349
|
+
: productionInfo.fileName === activeFileName && !compDirty
|
|
350
|
+
? 'Already in sync with editor'
|
|
351
|
+
: compDirty && activeFileName === 'default'
|
|
352
|
+
? 'Save edits as a new file, then adopt'
|
|
353
|
+
: compDirty
|
|
354
|
+
? `Save "${currentDisplayName}" and adopt`
|
|
355
|
+
: `Adopt "${currentDisplayName}" from editor`}
|
|
356
|
+
>
|
|
357
|
+
<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>
|
|
358
|
+
<span>
|
|
359
|
+
{#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
|
|
360
|
+
</span>
|
|
361
|
+
</button>
|
|
362
|
+
{#if adoptFeedback}
|
|
363
|
+
<span class="cfm-feedback" aria-live="polite">{adoptFeedback}</span>
|
|
364
|
+
{/if}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</header>
|
|
369
|
+
|
|
370
|
+
<SaveAsDialog
|
|
371
|
+
bind:show={saveAsDialog}
|
|
372
|
+
{currentDisplayName}
|
|
373
|
+
{files}
|
|
374
|
+
on:save={confirmSaveAs}
|
|
375
|
+
/>
|
|
376
|
+
|
|
377
|
+
<style>
|
|
378
|
+
.cfm-bar {
|
|
379
|
+
--cfm-applied: #5aa85e;
|
|
380
|
+
--cfm-rail-neutral: var(--ui-border-default);
|
|
381
|
+
--cfm-rail-dirty: var(--ui-highlight);
|
|
382
|
+
--cfm-rail-applied: var(--cfm-applied);
|
|
383
|
+
|
|
384
|
+
position: sticky;
|
|
385
|
+
top: 0;
|
|
386
|
+
z-index: 5;
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-direction: column;
|
|
389
|
+
gap: var(--ui-space-8);
|
|
390
|
+
padding: var(--ui-space-12);
|
|
391
|
+
background: var(--ui-surface-low);
|
|
392
|
+
border: 1px solid var(--ui-border-faint);
|
|
393
|
+
border-radius: var(--ui-radius-lg);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.cfm-title-row {
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: baseline;
|
|
399
|
+
gap: var(--ui-space-12);
|
|
400
|
+
flex-wrap: wrap;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.cfm-title {
|
|
404
|
+
margin: 0;
|
|
405
|
+
min-width: 0;
|
|
406
|
+
font-size: var(--ui-font-size-3xl);
|
|
407
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
408
|
+
color: var(--ui-text-primary);
|
|
409
|
+
letter-spacing: -0.015em;
|
|
410
|
+
line-height: 1.1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.source-link {
|
|
414
|
+
display: inline-flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
gap: var(--ui-space-4);
|
|
417
|
+
padding: var(--ui-space-2) var(--ui-space-6);
|
|
418
|
+
font-size: var(--ui-font-size-xs);
|
|
419
|
+
color: var(--ui-text-secondary);
|
|
420
|
+
text-decoration: none;
|
|
421
|
+
border: 1px solid var(--ui-border-default);
|
|
422
|
+
border-radius: var(--ui-radius-sm);
|
|
423
|
+
transition: all var(--ui-transition-fast);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.source-link:hover {
|
|
427
|
+
color: var(--ui-text-primary);
|
|
428
|
+
border-color: var(--ui-border-strong);
|
|
429
|
+
background: var(--ui-hover);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* ── two-row pipeline ─────────────────────────────────────── */
|
|
433
|
+
.cfm-rows {
|
|
434
|
+
display: flex;
|
|
435
|
+
flex-direction: column;
|
|
436
|
+
gap: var(--ui-space-6);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.cfm-row {
|
|
440
|
+
position: relative;
|
|
441
|
+
display: flex;
|
|
442
|
+
align-items: center;
|
|
443
|
+
flex-wrap: wrap;
|
|
444
|
+
gap: var(--ui-space-10);
|
|
445
|
+
padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-8) var(--ui-space-16);
|
|
446
|
+
background: var(--ui-surface-lower);
|
|
447
|
+
border: 1px solid var(--ui-border-subtle);
|
|
448
|
+
border-radius: var(--ui-radius-md);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.cfm-rail {
|
|
452
|
+
position: absolute;
|
|
453
|
+
left: 0;
|
|
454
|
+
top: 0;
|
|
455
|
+
bottom: 0;
|
|
456
|
+
width: 3px;
|
|
457
|
+
border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
|
|
458
|
+
background: var(--cfm-rail-neutral);
|
|
459
|
+
transition: background var(--ui-transition-base);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.cfm-row-editor.dirty .cfm-rail { background: var(--cfm-rail-dirty); }
|
|
463
|
+
.cfm-row-editor.applied .cfm-rail { background: var(--cfm-rail-applied); }
|
|
464
|
+
.cfm-row-production.applied .cfm-rail { background: var(--cfm-rail-applied); }
|
|
465
|
+
|
|
466
|
+
/* row head — label stacked over a small status sub-label so the
|
|
467
|
+
filename pill in each row starts at the same x and width */
|
|
468
|
+
.cfm-row-head {
|
|
469
|
+
flex-shrink: 0;
|
|
470
|
+
width: 4.5rem;
|
|
471
|
+
display: flex;
|
|
472
|
+
flex-direction: column;
|
|
473
|
+
gap: var(--ui-space-2);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.cfm-row-label {
|
|
477
|
+
font-size: var(--ui-font-size-xs);
|
|
478
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
479
|
+
text-transform: uppercase;
|
|
480
|
+
letter-spacing: 0.08em;
|
|
481
|
+
color: var(--ui-text-secondary);
|
|
482
|
+
line-height: 1.1;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.cfm-row-status {
|
|
486
|
+
display: inline-flex;
|
|
487
|
+
align-items: center;
|
|
488
|
+
gap: var(--ui-space-4);
|
|
489
|
+
font-size: 0.75rem;
|
|
490
|
+
letter-spacing: 0.02em;
|
|
491
|
+
color: var(--ui-text-muted);
|
|
492
|
+
line-height: 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.cfm-status-dot {
|
|
496
|
+
width: 5px;
|
|
497
|
+
height: 5px;
|
|
498
|
+
border-radius: 50%;
|
|
499
|
+
background: currentColor;
|
|
500
|
+
opacity: 0.7;
|
|
501
|
+
flex-shrink: 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.cfm-row-status.dirty {
|
|
505
|
+
color: var(--ui-highlight);
|
|
506
|
+
}
|
|
507
|
+
.cfm-row-status.dirty .cfm-status-dot {
|
|
508
|
+
opacity: 1;
|
|
509
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent);
|
|
510
|
+
animation: cfm-pulse 1.6s ease-in-out infinite;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.cfm-row-status.applied {
|
|
514
|
+
color: var(--cfm-applied);
|
|
515
|
+
}
|
|
516
|
+
.cfm-row-status.applied .cfm-status-dot {
|
|
517
|
+
opacity: 1;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* filename pill — fixed width so editor and production pills
|
|
521
|
+
stack with their left and right edges perfectly aligned. */
|
|
522
|
+
.cfm-pill {
|
|
523
|
+
display: inline-flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
gap: var(--ui-space-6);
|
|
526
|
+
flex: 0 0 7.5rem;
|
|
527
|
+
width: 7.5rem;
|
|
528
|
+
padding: var(--ui-space-6) var(--ui-space-10);
|
|
529
|
+
background: var(--ui-surface-lowest);
|
|
530
|
+
border: 1px solid var(--ui-border-subtle);
|
|
531
|
+
border-radius: var(--ui-radius-md);
|
|
532
|
+
transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.cfm-pill.dirty {
|
|
536
|
+
border-color: color-mix(in srgb, var(--ui-highlight) 60%, var(--ui-border-subtle));
|
|
537
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-highlight) 35%, transparent);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.cfm-pill.applied {
|
|
541
|
+
border-color: color-mix(in srgb, var(--cfm-applied) 50%, var(--ui-border-subtle));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.cfm-pill-name {
|
|
545
|
+
flex: 1;
|
|
546
|
+
min-width: 0;
|
|
547
|
+
font-size: var(--ui-font-size-md);
|
|
548
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
549
|
+
color: var(--ui-text-primary);
|
|
550
|
+
white-space: nowrap;
|
|
551
|
+
overflow: hidden;
|
|
552
|
+
text-overflow: ellipsis;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* actions cluster — sits directly next to the filename pill so the
|
|
556
|
+
buttons stay near the input and don't drift under the open editor panel */
|
|
557
|
+
.cfm-actions {
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
gap: var(--ui-space-6);
|
|
561
|
+
flex-wrap: wrap;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.cfm-feedback {
|
|
565
|
+
font-size: var(--ui-font-size-xs);
|
|
566
|
+
color: var(--cfm-applied);
|
|
567
|
+
white-space: nowrap;
|
|
568
|
+
animation: cfm-feedback-in 180ms ease;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
@keyframes cfm-feedback-in {
|
|
572
|
+
from { opacity: 0; transform: translateX(-2px); }
|
|
573
|
+
to { opacity: 1; transform: translateX(0); }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* promote connector between rows */
|
|
577
|
+
.cfm-promote {
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
justify-content: flex-start;
|
|
581
|
+
/* aligns under the pill column: row left-padding + head width + row gap */
|
|
582
|
+
padding-left: calc(var(--ui-space-16) + 4.5rem + var(--ui-space-10));
|
|
583
|
+
height: 0.25rem;
|
|
584
|
+
color: var(--ui-text-tertiary);
|
|
585
|
+
font-size: 0.7rem;
|
|
586
|
+
opacity: 0;
|
|
587
|
+
transform: translateY(-2px);
|
|
588
|
+
transition: opacity var(--ui-transition-base), transform var(--ui-transition-base), color var(--ui-transition-base);
|
|
589
|
+
pointer-events: none;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.cfm-rows.promotable .cfm-promote {
|
|
593
|
+
opacity: 0.85;
|
|
594
|
+
transform: translateY(0);
|
|
595
|
+
color: var(--ui-highlight);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.cfm-rows.in-sync .cfm-promote {
|
|
599
|
+
opacity: 0;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.cfm-promote-icon {
|
|
603
|
+
line-height: 1;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* buttons */
|
|
607
|
+
.cfm-btn {
|
|
608
|
+
display: inline-flex;
|
|
609
|
+
align-items: center;
|
|
610
|
+
gap: var(--ui-space-6);
|
|
611
|
+
padding: var(--ui-space-6) var(--ui-space-12);
|
|
612
|
+
background: var(--ui-surface);
|
|
613
|
+
border: 1px solid var(--ui-border-subtle);
|
|
614
|
+
border-radius: var(--ui-radius-md);
|
|
615
|
+
color: var(--ui-text-secondary);
|
|
616
|
+
font-size: var(--ui-font-size-md);
|
|
617
|
+
font-weight: var(--ui-font-weight-medium);
|
|
618
|
+
cursor: pointer;
|
|
619
|
+
transition: all var(--ui-transition-fast);
|
|
620
|
+
white-space: nowrap;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.cfm-btn i {
|
|
624
|
+
width: 1rem;
|
|
625
|
+
text-align: center;
|
|
626
|
+
font-size: 0.85em;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.cfm-btn:hover:not(:disabled) {
|
|
630
|
+
background: var(--ui-surface-high);
|
|
631
|
+
color: var(--ui-text-primary);
|
|
632
|
+
border-color: var(--ui-border-default);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.cfm-btn:disabled {
|
|
636
|
+
opacity: 0.45;
|
|
637
|
+
cursor: not-allowed;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.cfm-btn.primary {
|
|
641
|
+
background: color-mix(in srgb, var(--cfm-applied) 18%, var(--ui-surface-high));
|
|
642
|
+
border-color: color-mix(in srgb, var(--cfm-applied) 45%, var(--ui-border-medium));
|
|
643
|
+
color: var(--ui-text-primary);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.cfm-btn.primary:hover:not(:disabled) {
|
|
647
|
+
background: color-mix(in srgb, var(--cfm-applied) 30%, var(--ui-surface-higher));
|
|
648
|
+
border-color: color-mix(in srgb, var(--cfm-applied) 70%, var(--ui-border-strong));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.cfm-btn.primary:disabled {
|
|
652
|
+
background: var(--ui-surface);
|
|
653
|
+
border-color: var(--ui-border-subtle);
|
|
654
|
+
color: var(--ui-text-muted);
|
|
655
|
+
opacity: 1;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.cfm-btn.primary.saving i { animation: cfm-spin 1s linear infinite; }
|
|
659
|
+
.cfm-btn.primary.saved {
|
|
660
|
+
background: color-mix(in srgb, var(--cfm-applied) 30%, var(--ui-surface-high));
|
|
661
|
+
color: var(--cfm-applied);
|
|
662
|
+
}
|
|
663
|
+
.cfm-btn.primary.error {
|
|
664
|
+
color: var(--ui-text-muted);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
@keyframes cfm-spin {
|
|
668
|
+
from { transform: rotate(0deg); }
|
|
669
|
+
to { transform: rotate(360deg); }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
@keyframes cfm-pulse {
|
|
673
|
+
0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
|
|
674
|
+
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* narrow viewports: hide button text, keep icons visible */
|
|
678
|
+
@media (max-width: 640px) {
|
|
679
|
+
.cfm-btn span { display: none; }
|
|
680
|
+
.cfm-btn { padding: var(--ui-space-6) var(--ui-space-10); }
|
|
681
|
+
}
|
|
682
|
+
</style>
|