@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.
- package/README.md +14 -13
- package/dist-plugin/index.cjs +854 -226
- package/dist-plugin/index.d.cts +2 -1
- package/dist-plugin/index.d.ts +2 -1
- package/dist-plugin/index.js +852 -225
- package/package.json +26 -40
- package/src/{styles → app}/site.css +1 -1
- package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
- package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
- package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
- package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
- package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
- package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
- package/src/editor/component-editor/ImageEditor.svelte +30 -0
- package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
- package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
- package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
- package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
- package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
- package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
- package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
- package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
- package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
- package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
- package/src/editor/component-editor/editors.d.ts +10 -0
- package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
- package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
- package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -12
- package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
- package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
- package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
- package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
- package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
- package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
- package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
- package/src/editor/core/flashStatus.ts +30 -0
- package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
- package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
- package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
- package/src/editor/core/manifests/manifestService.ts +171 -0
- package/src/editor/core/palettes/familySwap.ts +99 -0
- package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
- package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
- package/src/editor/core/productionPulse.ts +37 -0
- package/src/{lib → editor/core/routing}/router.ts +1 -1
- package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
- package/src/{lib → editor/core/store}/editorCore.ts +24 -8
- package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
- package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
- package/src/{lib → editor/core/store}/editorStore.ts +222 -28
- package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
- package/src/editor/core/store/gradientSource.ts +192 -0
- package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
- package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
- package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
- package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
- package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
- package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
- package/src/{lib → editor/core/themes}/themeService.ts +6 -6
- package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
- package/src/editor/index.ts +69 -0
- package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
- package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
- package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
- package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
- package/src/{pages → editor/pages}/Editor.svelte +4 -4
- package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
- package/src/{styles → editor/styles}/ui-editor.css +43 -22
- package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
- package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
- package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
- package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
- package/src/editor/ui/FileLoadList.svelte +367 -0
- package/src/editor/ui/FilePill.svelte +80 -0
- package/src/editor/ui/FontStackEditor.svelte +499 -0
- package/src/editor/ui/GradientEditor.svelte +690 -0
- package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
- package/src/editor/ui/ManifestFileManager.svelte +438 -0
- package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
- package/src/editor/ui/ProjectFontsSection.svelte +638 -0
- package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
- package/src/{ui → editor/ui}/TextTab.svelte +3 -3
- package/src/editor/ui/ThemeFileManager.svelte +783 -0
- package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
- package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
- package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
- package/src/editor/ui/UIInfoPopover.svelte +243 -0
- package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
- package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
- package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
- package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
- package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
- package/src/editor/ui/UIPillButton.svelte +168 -0
- package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
- package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
- package/src/editor/ui/UISegmentedControl.svelte +114 -0
- package/src/editor/ui/UISquareButton.svelte +172 -0
- package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
- package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
- package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
- package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
- package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
- package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
- package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
- package/src/editor/ui/palette/paletteMath.ts +275 -0
- package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
- package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
- package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
- package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
- package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
- package/src/{components → system/components}/Badge.svelte +0 -36
- package/src/{components → system/components}/Button.svelte +2 -2
- package/src/{components → system/components}/Card.svelte +34 -60
- package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
- package/src/{components → system/components}/CornerBadge.svelte +8 -24
- package/src/{components → system/components}/Dialog.svelte +1 -1
- package/src/system/components/FloatingTokenTags.css +275 -0
- package/src/system/components/FloatingTokenTags.svelte +543 -0
- package/src/{components → system/components}/InlineEditActions.svelte +6 -4
- package/src/system/components/MenuSelect.svelte +229 -0
- package/src/{components → system/components}/Notification.svelte +8 -1
- package/src/{components → system/components}/ProgressBar.svelte +29 -11
- package/src/system/components/SectionDivider.svelte +560 -0
- package/src/{components → system/components}/SegmentedControl.svelte +49 -43
- package/src/{components → system/components}/TabBar.svelte +81 -65
- package/src/{components → system/components}/Table.svelte +17 -3
- package/src/{components → system/components}/Tooltip.svelte +6 -4
- package/src/system/styles/CONVENTIONS.md +178 -0
- package/src/system/styles/fonts.css +20 -0
- package/src/system/styles/tokens.css +601 -0
- package/src/system/styles/tokens.generated.css +544 -0
- package/src/component-editor/ImageEditor.svelte +0 -74
- package/src/component-editor/SectionDividerEditor.svelte +0 -265
- package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
- package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
- package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
- package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
- package/src/components/SectionDivider.svelte +0 -483
- package/src/data/google-fonts.json +0 -75
- package/src/lib/index.ts +0 -68
- package/src/lib/presetService.ts +0 -214
- package/src/lib/productionPulse.ts +0 -32
- package/src/styles/fonts.css +0 -30
- package/src/styles/tokens.css +0 -1324
- package/src/ui/FontStackEditor.svelte +0 -361
- package/src/ui/GradientEditor.svelte +0 -470
- package/src/ui/PresetFileManager.svelte +0 -1116
- package/src/ui/ProjectFontsSection.svelte +0 -645
- package/src/ui/ThemeFileManager.svelte +0 -1020
- package/src/ui/UnsavedComponentsDialog.svelte +0 -315
- /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
- /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
- /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
- /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
- /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
- /package/src/{lib → editor/core/storage}/storage.ts +0 -0
- /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
- /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
- /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
- /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
- /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
- /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
- /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
- /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
- /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
- /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
- /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
- /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
- /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
- /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
- /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
- /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
- /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
- /package/src/{ui → editor/ui}/index.ts +0 -0
- /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
- /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
- /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
- /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
- /package/src/{ui → editor/ui}/variantScales.ts +0 -0
- /package/src/{assets → system/assets}/newspaper.webp +0 -0
- /package/src/{assets → system/assets}/offering.webp +0 -0
- /package/src/{components → system/components}/Callout.svelte +0 -0
- /package/src/{components → system/components}/Image.svelte +0 -0
- /package/src/{components → system/components}/RadioButton.svelte +0 -0
- /package/src/{components → system/components}/types.ts +0 -0
- /package/src/{styles → system/styles}/_padding.scss +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { ThemeMeta } from '../core/themes/themeTypes';
|
|
4
|
+
import { listThemes, deleteTheme, setActiveFile, getProductionInfo, setProductionFile, sanitizeFileName } from '../core/themes/themeService';
|
|
5
|
+
import { listManifests, saveAsManifest } from '../core/manifests/manifestService';
|
|
6
|
+
import { activeFileName } from '../core/store/editorConfigStore';
|
|
7
|
+
import { dirty } from '../core/store/editorStore';
|
|
8
|
+
import { productionRevision, bumpProductionRevision, themeProductionInfo } from '../core/productionPulse';
|
|
9
|
+
import { flashStatus } from '../core/flashStatus';
|
|
10
|
+
import UIInfoPopover from './UIInfoPopover.svelte';
|
|
11
|
+
import FileLoadList from './FileLoadList.svelte';
|
|
12
|
+
import FilePill from './FilePill.svelte';
|
|
13
|
+
import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
saveStatus?: 'idle' | 'saving' | 'saved' | 'error';
|
|
17
|
+
onsave?: (payload: { fileName: string; displayName: string }) => void | Promise<void>;
|
|
18
|
+
onload?: (payload: { fileName: string }) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { saveStatus = 'idle', onsave, onload }: Props = $props();
|
|
22
|
+
|
|
23
|
+
let files: ThemeMeta[] = $state([]);
|
|
24
|
+
let showFileList = $state(false);
|
|
25
|
+
let saveAsDialog = $state(false);
|
|
26
|
+
let currentDisplayName = $state('Default Theme');
|
|
27
|
+
|
|
28
|
+
type ProdApplyStatus = 'idle' | 'applying' | 'done' | 'error';
|
|
29
|
+
let prodApplyStatus: ProdApplyStatus = $state('idle');
|
|
30
|
+
const setProdApplyStatus = (s: ProdApplyStatus) => (prodApplyStatus = s);
|
|
31
|
+
|
|
32
|
+
// Set when Adopt is clicked on a dirty+default theme: the SaveAs dialog opens
|
|
33
|
+
// for the user to name their theme, and on confirm the Adopt resumes
|
|
34
|
+
// automatically so the user gets one flow ("save and adopt") from one click.
|
|
35
|
+
let adoptAfterSave = false;
|
|
36
|
+
|
|
37
|
+
let prodIsInSync = $derived($themeProductionInfo?.fileName === $activeFileName);
|
|
38
|
+
let editorIsApplied = $derived(prodIsInSync && !$dirty);
|
|
39
|
+
let prodName = $derived($themeProductionInfo?.name ?? '—');
|
|
40
|
+
|
|
41
|
+
let isDefaultActive = $derived($activeFileName === 'default');
|
|
42
|
+
|
|
43
|
+
// Display names already in use by protected files. Passed to SaveAsDialog so
|
|
44
|
+
// a user can't shadow the default by typing its label ("Default Theme"),
|
|
45
|
+
// which sanitizes to "default-theme" and would otherwise slip past the
|
|
46
|
+
// filename-only check.
|
|
47
|
+
let protectedDisplayNames = $derived(
|
|
48
|
+
files.filter((f) => f.fileName === 'default').map((f) => f.name),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
async function refreshFiles() {
|
|
52
|
+
try {
|
|
53
|
+
files = await listThemes();
|
|
54
|
+
const active = files.find((f) => f.isActive);
|
|
55
|
+
if (active) {
|
|
56
|
+
$activeFileName = active.fileName;
|
|
57
|
+
currentDisplayName = active.name;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// silent — will show empty list
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function refreshProduction() {
|
|
65
|
+
try {
|
|
66
|
+
const info = await getProductionInfo();
|
|
67
|
+
themeProductionInfo.set(info);
|
|
68
|
+
} catch {
|
|
69
|
+
// silent — leave cached value in place
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onMount(async () => {
|
|
74
|
+
await refreshFiles();
|
|
75
|
+
await refreshProduction();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Refresh production state when any production pointer flips (e.g. a manifest
|
|
79
|
+
// is adopted elsewhere). Skip the initial tick — onMount already loaded it.
|
|
80
|
+
let pulseInitialised = false;
|
|
81
|
+
$effect(() => {
|
|
82
|
+
void $productionRevision;
|
|
83
|
+
if (!pulseInitialised) {
|
|
84
|
+
pulseInitialised = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
refreshProduction();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function handleSave() {
|
|
91
|
+
// Default is read-only — quietly redirect the first Save into Save As so the
|
|
92
|
+
// user gets one motion ("name it, save it") instead of bumping into a
|
|
93
|
+
// disabled button and having to find Save As themselves.
|
|
94
|
+
if (isDefaultActive) {
|
|
95
|
+
openSaveAs();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
onsave?.({ fileName: $activeFileName, displayName: currentDisplayName });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function openSaveAs() {
|
|
102
|
+
showFileList = false;
|
|
103
|
+
// Standalone Save As must not piggyback on an Adopt that was cancelled
|
|
104
|
+
// earlier — clear the latch so confirmSaveAs doesn't fire a stale Adopt.
|
|
105
|
+
adoptAfterSave = false;
|
|
106
|
+
saveAsDialog = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
110
|
+
const { displayName, fileName } = detail;
|
|
111
|
+
await onsave?.({ fileName, displayName });
|
|
112
|
+
$activeFileName = fileName;
|
|
113
|
+
currentDisplayName = displayName;
|
|
114
|
+
await refreshFiles();
|
|
115
|
+
if (adoptAfterSave) {
|
|
116
|
+
adoptAfterSave = false;
|
|
117
|
+
await handleApplyToProduction();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Pick a manifest filename that doesn't collide with anything on disk so
|
|
122
|
+
// auto-creating doesn't clobber a manifest the user customized earlier.
|
|
123
|
+
async function pickFreshManifestName(base: string): Promise<string> {
|
|
124
|
+
const baseFile = sanitizeFileName(base);
|
|
125
|
+
let manifests: { fileName: string }[];
|
|
126
|
+
try {
|
|
127
|
+
manifests = await listManifests();
|
|
128
|
+
} catch {
|
|
129
|
+
return baseFile;
|
|
130
|
+
}
|
|
131
|
+
const taken = new Set(manifests.map((m) => m.fileName));
|
|
132
|
+
if (!taken.has(baseFile)) return baseFile;
|
|
133
|
+
for (let n = 1; n < 1000; n++) {
|
|
134
|
+
const suffix = String(n).padStart(2, '0');
|
|
135
|
+
const candidate = `${baseFile}_${suffix}`;
|
|
136
|
+
if (!taken.has(candidate)) return candidate;
|
|
137
|
+
}
|
|
138
|
+
return `${baseFile}_${Date.now()}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function handleApplyToProduction() {
|
|
142
|
+
if (prodIsInSync) return;
|
|
143
|
+
|
|
144
|
+
// Dirty edits on the protected default theme can't be saved to that file,
|
|
145
|
+
// and adopting the on-disk default would silently strand the user's
|
|
146
|
+
// changes. Open the theme SaveAs dialog and resume Adopt after save.
|
|
147
|
+
if (isDefaultActive && $dirty) {
|
|
148
|
+
adoptAfterSave = true;
|
|
149
|
+
saveAsDialog = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
prodApplyStatus = 'applying';
|
|
154
|
+
try {
|
|
155
|
+
await setProductionFile($activeFileName);
|
|
156
|
+
await refreshProduction();
|
|
157
|
+
bumpProductionRevision();
|
|
158
|
+
flashStatus(setProdApplyStatus, 'done');
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const e = err as Error & { code?: string };
|
|
161
|
+
if (e.code === 'ACTIVE_IS_PROTECTED') {
|
|
162
|
+
// Default manifest is active — auto-create a user manifest and retry.
|
|
163
|
+
// No second dialog: the user clicked one button (Adopt) and gave the
|
|
164
|
+
// theme a name; the manifest is bookkeeping they shouldn't have to
|
|
165
|
+
// think about.
|
|
166
|
+
prodApplyStatus = 'idle';
|
|
167
|
+
try {
|
|
168
|
+
const targetName = await pickFreshManifestName('my-manifest');
|
|
169
|
+
await saveAsManifest(targetName, targetName);
|
|
170
|
+
} catch {
|
|
171
|
+
flashStatus(setProdApplyStatus, 'error', { durationMs: 3000 });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await handleApplyToProduction();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
flashStatus(setProdApplyStatus, 'error', { durationMs: 3000 });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function handleLoad(file: ThemeMeta) {
|
|
182
|
+
if ($dirty) {
|
|
183
|
+
const ok = window.confirm(
|
|
184
|
+
'Loading a theme will discard unsaved changes. Continue?',
|
|
185
|
+
);
|
|
186
|
+
if (!ok) return;
|
|
187
|
+
}
|
|
188
|
+
showFileList = false;
|
|
189
|
+
await setActiveFile(file.fileName);
|
|
190
|
+
$activeFileName = file.fileName;
|
|
191
|
+
currentDisplayName = file.name;
|
|
192
|
+
onload?.({ fileName: file.fileName });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Two-step arm pattern. First click flips `revertArmed` so the button
|
|
196
|
+
// morphs into a "Discard?" affordance; second click commits. Click-out,
|
|
197
|
+
// Escape, a 3s timeout, or the dirty flag clearing all disarm.
|
|
198
|
+
let revertArmed = $state(false);
|
|
199
|
+
let revertTimer: ReturnType<typeof setTimeout> | null = null;
|
|
200
|
+
|
|
201
|
+
function disarmRevert() {
|
|
202
|
+
revertArmed = false;
|
|
203
|
+
if (revertTimer) {
|
|
204
|
+
clearTimeout(revertTimer);
|
|
205
|
+
revertTimer = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handleRevert() {
|
|
210
|
+
if (!$dirty) return;
|
|
211
|
+
if (!revertArmed) {
|
|
212
|
+
revertArmed = true;
|
|
213
|
+
revertTimer = setTimeout(disarmRevert, 3000);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
disarmRevert();
|
|
217
|
+
onload?.({ fileName: $activeFileName });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
$effect(() => {
|
|
221
|
+
if (!$dirty && revertArmed) disarmRevert();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
$effect(() => {
|
|
225
|
+
if (!revertArmed) return;
|
|
226
|
+
const onDocClick = (e: MouseEvent) => {
|
|
227
|
+
if (!(e.target as HTMLElement).closest('.tfm-revert-btn')) disarmRevert();
|
|
228
|
+
};
|
|
229
|
+
const onKey = (e: KeyboardEvent) => {
|
|
230
|
+
if (e.key === 'Escape') disarmRevert();
|
|
231
|
+
};
|
|
232
|
+
document.addEventListener('click', onDocClick, true);
|
|
233
|
+
document.addEventListener('keydown', onKey);
|
|
234
|
+
return () => {
|
|
235
|
+
document.removeEventListener('click', onDocClick, true);
|
|
236
|
+
document.removeEventListener('keydown', onKey);
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
async function handleDelete(file: ThemeMeta) {
|
|
241
|
+
if (file.fileName === 'default') return;
|
|
242
|
+
if (file.fileName === $themeProductionInfo?.fileName) return;
|
|
243
|
+
try {
|
|
244
|
+
// Capture before refreshFiles() reads the server's reverted active back
|
|
245
|
+
// into local state — otherwise the "was this the active file?" check
|
|
246
|
+
// below sees the post-revert value and skips the reload.
|
|
247
|
+
const wasActive = file.fileName === $activeFileName;
|
|
248
|
+
await deleteTheme(file.fileName);
|
|
249
|
+
await refreshFiles();
|
|
250
|
+
if (wasActive) {
|
|
251
|
+
$activeFileName = 'default';
|
|
252
|
+
currentDisplayName = 'Default Theme';
|
|
253
|
+
onload?.({ fileName: 'default' });
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// silent
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function toggleFileList() {
|
|
261
|
+
showFileList = !showFileList;
|
|
262
|
+
if (showFileList) refreshFiles();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<div class="theme-file-manager">
|
|
268
|
+
<div class="tfm-header">
|
|
269
|
+
<span class="tfm-header-label">Theme</span>
|
|
270
|
+
<UIInfoPopover title="Themes" ariaLabel="About themes">
|
|
271
|
+
<p>
|
|
272
|
+
A <strong>theme</strong> saves the design tokens for a site, components use these tokens to define their appearance.
|
|
273
|
+
</p>
|
|
274
|
+
</UIInfoPopover>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div class="tfm-cards" class:in-sync={prodIsInSync}>
|
|
278
|
+
<div
|
|
279
|
+
class="tfm-card tfm-card-editor"
|
|
280
|
+
class:dirty={$dirty}
|
|
281
|
+
class:applied={editorIsApplied}
|
|
282
|
+
>
|
|
283
|
+
<span class="tfm-rail" aria-hidden="true"></span>
|
|
284
|
+
<div class="tfm-card-head">
|
|
285
|
+
<span class="tfm-card-label">Editor</span>
|
|
286
|
+
<span
|
|
287
|
+
class="tfm-card-status"
|
|
288
|
+
class:dirty={$dirty}
|
|
289
|
+
class:applied={editorIsApplied}
|
|
290
|
+
>
|
|
291
|
+
<i class="tfm-status-dot" aria-hidden="true"></i>
|
|
292
|
+
<span>{$dirty ? 'unsaved' : editorIsApplied ? 'live' : 'saved'}</span>
|
|
293
|
+
{#if $dirty}
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
class="tfm-revert-btn"
|
|
297
|
+
class:armed={revertArmed}
|
|
298
|
+
onclick={handleRevert}
|
|
299
|
+
title={revertArmed
|
|
300
|
+
? 'Click again to discard unsaved changes'
|
|
301
|
+
: 'Revert to last saved version'}
|
|
302
|
+
aria-label={revertArmed ? 'Confirm discard unsaved changes' : 'Revert unsaved changes'}
|
|
303
|
+
>
|
|
304
|
+
<i class="fas fa-rotate-left" aria-hidden="true"></i>
|
|
305
|
+
{#if revertArmed}<span class="tfm-revert-label">Discard?</span>{/if}
|
|
306
|
+
</button>
|
|
307
|
+
{/if}
|
|
308
|
+
</span>
|
|
309
|
+
</div>
|
|
310
|
+
<FilePill
|
|
311
|
+
name={currentDisplayName}
|
|
312
|
+
isProtected={isDefaultActive}
|
|
313
|
+
dirty={$dirty}
|
|
314
|
+
applied={editorIsApplied}
|
|
315
|
+
protectedTitle="Protected system theme"
|
|
316
|
+
title={currentDisplayName}
|
|
317
|
+
style="display: flex;"
|
|
318
|
+
/>
|
|
319
|
+
<div class="tfm-card-actions tfm-card-actions-stack">
|
|
320
|
+
<button
|
|
321
|
+
class="tfm-btn tfm-btn-row save-btn"
|
|
322
|
+
class:saving={saveStatus === 'saving'}
|
|
323
|
+
class:saved={saveStatus === 'saved'}
|
|
324
|
+
class:error={saveStatus === 'error'}
|
|
325
|
+
onclick={handleSave}
|
|
326
|
+
disabled={saveStatus === 'saving'}
|
|
327
|
+
title={isDefaultActive
|
|
328
|
+
? 'Save to a new theme file'
|
|
329
|
+
: 'Save to current file'}
|
|
330
|
+
>
|
|
331
|
+
<i
|
|
332
|
+
class="fas"
|
|
333
|
+
class:fa-save={saveStatus === 'idle'}
|
|
334
|
+
class:fa-spinner={saveStatus === 'saving'}
|
|
335
|
+
class:fa-check={saveStatus === 'saved'}
|
|
336
|
+
class:fa-times={saveStatus === 'error'}
|
|
337
|
+
></i>
|
|
338
|
+
<span>
|
|
339
|
+
{#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
|
|
340
|
+
</span>
|
|
341
|
+
</button>
|
|
342
|
+
<button class="tfm-btn tfm-btn-row" onclick={openSaveAs} title="Save as new theme">
|
|
343
|
+
<i class="fas fa-copy"></i>
|
|
344
|
+
<span>Save As…</span>
|
|
345
|
+
</button>
|
|
346
|
+
<button
|
|
347
|
+
class="tfm-btn tfm-btn-row"
|
|
348
|
+
class:active={showFileList}
|
|
349
|
+
onclick={toggleFileList}
|
|
350
|
+
title="Load a theme"
|
|
351
|
+
>
|
|
352
|
+
<i class="fas fa-folder-open"></i>
|
|
353
|
+
<span>Load…</span>
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<button
|
|
359
|
+
class="tfm-adopt-btn"
|
|
360
|
+
class:saving={prodApplyStatus === 'applying'}
|
|
361
|
+
class:saved={prodApplyStatus === 'done'}
|
|
362
|
+
class:error={prodApplyStatus === 'error'}
|
|
363
|
+
class:in-sync={prodIsInSync}
|
|
364
|
+
onclick={handleApplyToProduction}
|
|
365
|
+
disabled={prodApplyStatus === 'applying' || prodIsInSync}
|
|
366
|
+
title={prodIsInSync
|
|
367
|
+
? 'This theme is already in production'
|
|
368
|
+
: `Adopt "${currentDisplayName}" as the production theme`}
|
|
369
|
+
>
|
|
370
|
+
<i
|
|
371
|
+
class="fas"
|
|
372
|
+
class:fa-arrow-down={prodApplyStatus === 'idle'}
|
|
373
|
+
class:fa-spinner={prodApplyStatus === 'applying'}
|
|
374
|
+
class:fa-check={prodApplyStatus === 'done'}
|
|
375
|
+
class:fa-xmark={prodApplyStatus === 'error'}
|
|
376
|
+
></i>
|
|
377
|
+
<span>
|
|
378
|
+
{#if prodApplyStatus === 'idle'}Adopt{:else if prodApplyStatus === 'applying'}Adopting{:else if prodApplyStatus === 'done'}Adopted{:else}Error{/if}
|
|
379
|
+
</span>
|
|
380
|
+
</button>
|
|
381
|
+
|
|
382
|
+
<div
|
|
383
|
+
class="tfm-card tfm-card-production"
|
|
384
|
+
class:in-sync={prodIsInSync}
|
|
385
|
+
>
|
|
386
|
+
<span class="tfm-rail" aria-hidden="true"></span>
|
|
387
|
+
<div class="tfm-card-head">
|
|
388
|
+
<span class="tfm-card-label">Production</span>
|
|
389
|
+
<span
|
|
390
|
+
class="tfm-card-status"
|
|
391
|
+
class:applied={prodIsInSync}
|
|
392
|
+
>
|
|
393
|
+
<i class="tfm-status-dot" aria-hidden="true"></i>
|
|
394
|
+
<span>{prodIsInSync ? 'live' : 'out of sync'}</span>
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
<FilePill
|
|
398
|
+
name={prodName}
|
|
399
|
+
isProtected={$themeProductionInfo?.fileName === 'default'}
|
|
400
|
+
applied={prodIsInSync}
|
|
401
|
+
protectedTitle="Protected system theme"
|
|
402
|
+
title={prodName}
|
|
403
|
+
style="display: flex;"
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<FileLoadList
|
|
410
|
+
bind:show={showFileList}
|
|
411
|
+
title="Load Theme"
|
|
412
|
+
{files}
|
|
413
|
+
activeFileName={$activeFileName}
|
|
414
|
+
sortable
|
|
415
|
+
showUpdatedAt
|
|
416
|
+
systemBadge={{ label: 'system', title: 'Protected system theme' }}
|
|
417
|
+
emptyMessage="No saved files"
|
|
418
|
+
canDelete={(file) => file.fileName !== 'default' && file.fileName !== $themeProductionInfo?.fileName}
|
|
419
|
+
onload={handleLoad}
|
|
420
|
+
ondelete={handleDelete}
|
|
421
|
+
/>
|
|
422
|
+
|
|
423
|
+
<SaveAsDialog
|
|
424
|
+
bind:show={saveAsDialog}
|
|
425
|
+
{currentDisplayName}
|
|
426
|
+
{files}
|
|
427
|
+
currentFileName={$activeFileName}
|
|
428
|
+
reservedDisplayNames={protectedDisplayNames}
|
|
429
|
+
title="Save Theme As"
|
|
430
|
+
placeholder="Theme name…"
|
|
431
|
+
reservedNameMessage='That name is reserved for the protected default theme.'
|
|
432
|
+
branchFromDefaultName="My Theme"
|
|
433
|
+
onsave={confirmSaveAs}
|
|
434
|
+
/>
|
|
435
|
+
|
|
436
|
+
<style>
|
|
437
|
+
.theme-file-manager {
|
|
438
|
+
--tfm-applied: #5aa85e;
|
|
439
|
+
--tfm-rail-neutral: var(--ui-border);
|
|
440
|
+
--tfm-rail-dirty: var(--ui-highlight);
|
|
441
|
+
--tfm-rail-applied: var(--tfm-applied);
|
|
442
|
+
|
|
443
|
+
display: flex;
|
|
444
|
+
flex-direction: column;
|
|
445
|
+
gap: var(--ui-space-8);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.tfm-header {
|
|
449
|
+
display: flex;
|
|
450
|
+
align-items: center;
|
|
451
|
+
justify-content: space-between;
|
|
452
|
+
gap: var(--ui-space-4);
|
|
453
|
+
padding: 0 var(--ui-space-4);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.tfm-header-label {
|
|
457
|
+
font-size: var(--ui-font-size-xs);
|
|
458
|
+
color: var(--ui-text-secondary);
|
|
459
|
+
text-transform: uppercase;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* Two-card pipeline (Editor → Production) — theme card + production card
|
|
463
|
+
surface the per-artifact pipeline. The manifest panel sits one level up
|
|
464
|
+
and tracks active vs default rather than editor vs production. */
|
|
465
|
+
.tfm-cards {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
gap: var(--ui-space-6);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.tfm-card {
|
|
472
|
+
position: relative;
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
gap: var(--ui-space-6);
|
|
476
|
+
padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-10) var(--ui-space-16);
|
|
477
|
+
background: var(--ui-surface-lower);
|
|
478
|
+
border: 1px solid var(--ui-border-low);
|
|
479
|
+
border-radius: var(--ui-radius-md);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.tfm-rail {
|
|
483
|
+
position: absolute;
|
|
484
|
+
left: 0;
|
|
485
|
+
top: 0;
|
|
486
|
+
bottom: 0;
|
|
487
|
+
width: 3px;
|
|
488
|
+
border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
|
|
489
|
+
background: var(--tfm-rail-neutral);
|
|
490
|
+
transition: background var(--ui-transition-base);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.tfm-card-editor.dirty .tfm-rail { background: var(--tfm-rail-dirty); }
|
|
494
|
+
.tfm-card-editor.applied .tfm-rail { background: var(--tfm-rail-applied); }
|
|
495
|
+
.tfm-card-production.in-sync .tfm-rail { background: var(--tfm-rail-applied); }
|
|
496
|
+
|
|
497
|
+
.tfm-card-head {
|
|
498
|
+
display: flex;
|
|
499
|
+
align-items: baseline;
|
|
500
|
+
justify-content: space-between;
|
|
501
|
+
gap: var(--ui-space-8);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.tfm-card-label {
|
|
505
|
+
font-size: var(--ui-font-size-xs);
|
|
506
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
507
|
+
text-transform: uppercase;
|
|
508
|
+
color: var(--ui-text-secondary);
|
|
509
|
+
line-height: 1.1;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.tfm-card-status {
|
|
513
|
+
display: inline-flex;
|
|
514
|
+
align-items: center;
|
|
515
|
+
gap: var(--ui-space-4);
|
|
516
|
+
font-size: 0.7rem;
|
|
517
|
+
color: var(--ui-text-muted);
|
|
518
|
+
line-height: 1;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.tfm-status-dot {
|
|
522
|
+
width: 5px;
|
|
523
|
+
height: 5px;
|
|
524
|
+
border-radius: 50%;
|
|
525
|
+
background: currentColor;
|
|
526
|
+
opacity: 0.7;
|
|
527
|
+
flex-shrink: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.tfm-card-status.dirty {
|
|
531
|
+
color: var(--ui-highlight);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.tfm-card-status.dirty .tfm-status-dot {
|
|
535
|
+
opacity: 1;
|
|
536
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent);
|
|
537
|
+
animation: tfm-pulse 1.6s ease-in-out infinite;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.tfm-card-status.applied {
|
|
541
|
+
color: var(--tfm-applied);
|
|
542
|
+
}
|
|
543
|
+
.tfm-card-status.applied .tfm-status-dot {
|
|
544
|
+
opacity: 1;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* Revert button — paired with the "unsaved" status label. Lives only when
|
|
548
|
+
dirty, in the highlight color so it reads as part of the status group
|
|
549
|
+
rather than a peer of the action stack below. Two-step arm: first click
|
|
550
|
+
adds .armed (label appears, fills with highlight), second click commits. */
|
|
551
|
+
.tfm-revert-btn {
|
|
552
|
+
position: relative;
|
|
553
|
+
display: inline-flex;
|
|
554
|
+
align-items: center;
|
|
555
|
+
justify-content: center;
|
|
556
|
+
gap: 4px;
|
|
557
|
+
height: 18px;
|
|
558
|
+
margin-left: 2px;
|
|
559
|
+
padding: 0 4px;
|
|
560
|
+
background: transparent;
|
|
561
|
+
border: 1px solid color-mix(in srgb, var(--ui-highlight) 35%, transparent);
|
|
562
|
+
border-radius: 4px;
|
|
563
|
+
color: var(--ui-highlight);
|
|
564
|
+
font-size: 0.65rem;
|
|
565
|
+
line-height: 1;
|
|
566
|
+
cursor: pointer;
|
|
567
|
+
opacity: 0.85;
|
|
568
|
+
overflow: hidden;
|
|
569
|
+
transition:
|
|
570
|
+
background var(--ui-transition-fast),
|
|
571
|
+
border-color var(--ui-transition-fast),
|
|
572
|
+
color var(--ui-transition-fast),
|
|
573
|
+
opacity var(--ui-transition-fast),
|
|
574
|
+
padding var(--ui-transition-fast);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.tfm-revert-btn:hover {
|
|
578
|
+
background: color-mix(in srgb, var(--ui-highlight) 18%, transparent);
|
|
579
|
+
border-color: color-mix(in srgb, var(--ui-highlight) 65%, transparent);
|
|
580
|
+
opacity: 1;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.tfm-revert-btn:focus-visible {
|
|
584
|
+
outline: 2px solid color-mix(in srgb, var(--ui-highlight) 60%, transparent);
|
|
585
|
+
outline-offset: 1px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.tfm-revert-btn i {
|
|
589
|
+
font-size: 0.65rem;
|
|
590
|
+
transition: transform var(--ui-transition-fast);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.tfm-revert-btn.armed {
|
|
594
|
+
background: var(--ui-highlight);
|
|
595
|
+
border-color: var(--ui-highlight);
|
|
596
|
+
color: var(--ui-surface-lowest, #111);
|
|
597
|
+
opacity: 1;
|
|
598
|
+
padding: 0 6px;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.tfm-revert-btn.armed i {
|
|
602
|
+
transform: rotate(-35deg);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.tfm-revert-label {
|
|
606
|
+
font-weight: var(--ui-font-weight-semibold, 600);
|
|
607
|
+
white-space: nowrap;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* Time-window cue — thin bar drains across the bottom edge as the 3s
|
|
611
|
+
auto-disarm window elapses. Pure CSS, runs once per arm cycle. */
|
|
612
|
+
.tfm-revert-btn.armed::after {
|
|
613
|
+
content: '';
|
|
614
|
+
position: absolute;
|
|
615
|
+
left: 0;
|
|
616
|
+
bottom: 0;
|
|
617
|
+
height: 2px;
|
|
618
|
+
width: 100%;
|
|
619
|
+
background: color-mix(in srgb, var(--ui-surface-lowest, #111) 70%, transparent);
|
|
620
|
+
transform-origin: left center;
|
|
621
|
+
animation: tfm-revert-drain 3s linear forwards;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@keyframes tfm-revert-drain {
|
|
625
|
+
from { transform: scaleX(1); }
|
|
626
|
+
to { transform: scaleX(0); }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.tfm-card-actions {
|
|
630
|
+
display: flex;
|
|
631
|
+
gap: var(--ui-space-4);
|
|
632
|
+
flex-wrap: wrap;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.tfm-card-actions-stack {
|
|
636
|
+
flex-direction: column;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.tfm-btn-row {
|
|
640
|
+
width: 100%;
|
|
641
|
+
justify-content: flex-start;
|
|
642
|
+
gap: var(--ui-space-8);
|
|
643
|
+
flex: 0 0 auto;
|
|
644
|
+
text-align: left;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.tfm-btn-row i {
|
|
648
|
+
width: 1rem;
|
|
649
|
+
text-align: center;
|
|
650
|
+
flex: 0 0 auto;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.tfm-btn-row span {
|
|
654
|
+
flex: 1 1 auto;
|
|
655
|
+
text-align: left;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* Bridge button — sits between Editor and Production cards as the arrow that
|
|
659
|
+
promotes the editor theme into production. */
|
|
660
|
+
.tfm-adopt-btn {
|
|
661
|
+
align-self: stretch;
|
|
662
|
+
width: 100%;
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
justify-content: center;
|
|
666
|
+
gap: var(--ui-space-6);
|
|
667
|
+
margin: calc(var(--ui-space-2) * -1) 0;
|
|
668
|
+
padding: var(--ui-space-6) var(--ui-space-12);
|
|
669
|
+
background: color-mix(in srgb, var(--tfm-applied) 18%, var(--ui-surface-high));
|
|
670
|
+
border: 1px solid color-mix(in srgb, var(--tfm-applied) 45%, var(--ui-border-high));
|
|
671
|
+
border-radius: var(--ui-radius-md);
|
|
672
|
+
color: var(--ui-text-primary);
|
|
673
|
+
font-size: var(--ui-font-size-md);
|
|
674
|
+
font-weight: var(--ui-font-weight-medium);
|
|
675
|
+
cursor: pointer;
|
|
676
|
+
transition: all var(--ui-transition-fast);
|
|
677
|
+
white-space: nowrap;
|
|
678
|
+
position: relative;
|
|
679
|
+
z-index: 1;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.tfm-adopt-btn i {
|
|
683
|
+
width: 1rem;
|
|
684
|
+
text-align: center;
|
|
685
|
+
font-size: 0.85em;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.tfm-adopt-btn:hover:not(:disabled) {
|
|
689
|
+
background: color-mix(in srgb, var(--tfm-applied) 30%, var(--ui-surface-higher));
|
|
690
|
+
border-color: color-mix(in srgb, var(--tfm-applied) 70%, var(--ui-border-higher));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.tfm-adopt-btn:disabled {
|
|
694
|
+
cursor: not-allowed;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.tfm-adopt-btn.in-sync {
|
|
698
|
+
background: transparent;
|
|
699
|
+
border-color: var(--ui-border-low);
|
|
700
|
+
color: var(--ui-text-muted);
|
|
701
|
+
opacity: 0.7;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.tfm-adopt-btn.saving i { animation: spin 1s linear infinite; }
|
|
705
|
+
.tfm-adopt-btn.saved {
|
|
706
|
+
background: color-mix(in srgb, var(--tfm-applied) 30%, var(--ui-surface-high));
|
|
707
|
+
color: var(--tfm-applied);
|
|
708
|
+
}
|
|
709
|
+
.tfm-adopt-btn.error { color: var(--ui-text-muted); }
|
|
710
|
+
|
|
711
|
+
.tfm-btn {
|
|
712
|
+
display: inline-flex;
|
|
713
|
+
align-items: center;
|
|
714
|
+
justify-content: center;
|
|
715
|
+
gap: var(--ui-space-4);
|
|
716
|
+
padding: var(--ui-space-6) var(--ui-space-10);
|
|
717
|
+
background: var(--ui-surface);
|
|
718
|
+
border: 1px solid var(--ui-border-low);
|
|
719
|
+
border-radius: var(--ui-radius-md);
|
|
720
|
+
color: var(--ui-text-secondary);
|
|
721
|
+
font-size: var(--ui-font-size-md);
|
|
722
|
+
font-weight: var(--ui-font-weight-medium);
|
|
723
|
+
cursor: pointer;
|
|
724
|
+
transition: all var(--ui-transition-fast);
|
|
725
|
+
white-space: nowrap;
|
|
726
|
+
flex: 1 1 0;
|
|
727
|
+
min-width: 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.tfm-btn i {
|
|
731
|
+
width: 1rem;
|
|
732
|
+
text-align: center;
|
|
733
|
+
font-size: 0.85em;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.tfm-btn:hover:not(:disabled) {
|
|
737
|
+
background: var(--ui-surface-high);
|
|
738
|
+
color: var(--ui-text-primary);
|
|
739
|
+
border-color: var(--ui-border);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.tfm-btn:disabled {
|
|
743
|
+
opacity: 0.45;
|
|
744
|
+
cursor: not-allowed;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.tfm-btn.active {
|
|
748
|
+
background: var(--ui-surface);
|
|
749
|
+
border-color: var(--ui-border);
|
|
750
|
+
color: var(--ui-text-primary);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.save-btn {
|
|
754
|
+
background: var(--ui-surface-high);
|
|
755
|
+
border-color: var(--ui-border-high);
|
|
756
|
+
color: var(--ui-text-primary);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.save-btn:hover:not(:disabled) {
|
|
760
|
+
background: var(--ui-surface-higher);
|
|
761
|
+
border-color: var(--ui-border-higher);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.save-btn.saving i { animation: spin 1s linear infinite; }
|
|
765
|
+
.save-btn.saved {
|
|
766
|
+
background: var(--ui-surface-highest);
|
|
767
|
+
color: var(--ui-text-success);
|
|
768
|
+
}
|
|
769
|
+
.save-btn.error {
|
|
770
|
+
background: var(--ui-surface-high);
|
|
771
|
+
color: var(--ui-text-muted);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
@keyframes tfm-pulse {
|
|
775
|
+
0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
|
|
776
|
+
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
@keyframes spin {
|
|
780
|
+
from { transform: rotate(0deg); }
|
|
781
|
+
to { transform: rotate(360deg); }
|
|
782
|
+
}
|
|
783
|
+
</style>
|