@motion-proto/live-tokens 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist-plugin/index.cjs +147 -136
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +145 -135
- package/package.json +25 -40
- 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 +3 -3
- 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 +64 -37
- package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
- package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
- package/src/{component-editor → editor/component-editor}/SectionDividerEditor.svelte +57 -84
- package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +16 -20
- 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/{component-editor → editor/component-editor}/registry.ts +28 -18
- package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +144 -416
- 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/DividerEditor.svelte +1 -1
- package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
- package/src/{component-editor → editor/component-editor}/scaffolding/GradientCard.svelte +6 -6
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -11
- package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +72 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +257 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +9 -7
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +644 -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 +14 -0
- package/src/{lib → editor/core/components}/componentConfigService.ts +2 -2
- package/src/{lib → editor/core/components}/componentPersist.ts +5 -5
- 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 +116 -0
- package/src/{lib → editor/core/palettes}/paletteDerivation.ts +2 -2
- package/src/{lib → editor/core/palettes}/tokenRegistry.ts +5 -5
- 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 +17 -17
- package/src/{lib → editor/core/store}/editorTypes.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/components.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/gradients.ts +2 -2
- 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 +6 -6
- package/src/{lib → editor/core/themes}/themeService.ts +6 -6
- package/src/{lib → editor/core/themes}/themeTypes.ts +11 -7
- package/src/editor/index.ts +69 -0
- package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +79 -125
- 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 +41 -21
- package/src/{styles → editor/styles}/ui-form-controls.css +8 -8
- package/src/{ui → editor/ui}/BezierCurveEditor.svelte +8 -8
- package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
- package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +8 -6
- package/src/editor/ui/FileLoadList.svelte +350 -0
- package/src/editor/ui/FilePill.svelte +80 -0
- package/src/{ui → editor/ui}/FontStackEditor.svelte +7 -7
- package/src/{ui → editor/ui}/GradientEditor.svelte +11 -11
- package/src/{ui → editor/ui}/GradientStopPicker.svelte +1 -1
- package/src/editor/ui/ManifestFileManager.svelte +371 -0
- package/src/{ui → editor/ui}/PaletteEditor.svelte +132 -598
- package/src/{ui → editor/ui}/ProjectFontsSection.svelte +102 -144
- package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
- package/src/{ui → editor/ui}/TextTab.svelte +3 -3
- package/src/{ui → editor/ui}/ThemeFileManager.svelte +286 -519
- package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
- package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -6
- package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +1 -1
- package/src/editor/ui/UIInfoPopover.svelte +244 -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 +26 -26
- package/src/editor/ui/UIPillButton.svelte +138 -0
- package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
- package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
- package/src/editor/ui/UISquareButton.svelte +172 -0
- package/src/{ui → editor/ui}/UITokenSelector.svelte +10 -10
- package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
- package/src/{ui → editor/ui}/VariablesTab.svelte +31 -8
- package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
- package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +13 -13
- package/src/{ui → editor/ui}/palette/PaletteBase.svelte +8 -5
- 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 -17
- package/src/{ui → editor/ui}/sections/GradientsSection.svelte +7 -7
- package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +17 -17
- package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +22 -22
- package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
- package/src/{components → system/components}/Badge.svelte +0 -36
- package/src/{components → system/components}/Card.svelte +8 -62
- package/src/{components → system/components}/CornerBadge.svelte +8 -24
- package/src/{components → system/components}/Dialog.svelte +1 -1
- package/src/system/components/FloatingTokenTags.css +256 -0
- package/src/system/components/FloatingTokenTags.svelte +592 -0
- package/src/{components → system/components}/InlineEditActions.svelte +6 -4
- package/src/system/components/MenuSelect.svelte +229 -0
- package/src/{components → system/components}/ProgressBar.svelte +29 -11
- 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/{styles → system/styles}/fonts.css +6 -3
- package/src/{styles → system/styles}/tokens.css +149 -29
- package/src/component-editor/ImageEditor.svelte +0 -74
- 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/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/ui/PresetFileManager.svelte +0 -1116
- package/src/ui/UnsavedComponentsDialog.svelte +0 -315
- /package/src/{styles → app}/site.css +0 -0
- /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/TypeEditor.svelte +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +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/components}/componentConfigKeys.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}/migrations/index.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}/ColumnsOverlay.svelte +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}/Button.svelte +0 -0
- /package/src/{components → system/components}/Callout.svelte +0 -0
- /package/src/{components → system/components}/CollapsibleSection.svelte +0 -0
- /package/src/{components → system/components}/Image.svelte +0 -0
- /package/src/{components → system/components}/Notification.svelte +0 -0
- /package/src/{components → system/components}/RadioButton.svelte +0 -0
- /package/src/{components → system/components}/SectionDivider.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
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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';
|
|
11
13
|
import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
saveStatus?: 'idle' | 'saving' | 'saved' | 'error';
|
|
15
|
-
onsave?: (payload: { fileName: string; displayName: string }) => void
|
|
17
|
+
onsave?: (payload: { fileName: string; displayName: string }) => void | Promise<void>;
|
|
16
18
|
onload?: (payload: { fileName: string }) => void;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -23,12 +25,14 @@
|
|
|
23
25
|
let saveAsDialog = $state(false);
|
|
24
26
|
let currentDisplayName = $state('Default Theme');
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
type ProdApplyStatus = 'idle' | 'applying' | 'done' | 'error';
|
|
29
|
+
let prodApplyStatus: ProdApplyStatus = $state('idle');
|
|
30
|
+
const setProdApplyStatus = (s: ProdApplyStatus) => (prodApplyStatus = s);
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let
|
|
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;
|
|
32
36
|
|
|
33
37
|
let prodIsInSync = $derived($themeProductionInfo?.fileName === $activeFileName);
|
|
34
38
|
let editorIsApplied = $derived(prodIsInSync && !$dirty);
|
|
@@ -36,6 +40,14 @@
|
|
|
36
40
|
|
|
37
41
|
let isDefaultActive = $derived($activeFileName === 'default');
|
|
38
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
|
+
|
|
39
51
|
async function refreshFiles() {
|
|
40
52
|
try {
|
|
41
53
|
files = await listThemes();
|
|
@@ -61,76 +73,9 @@
|
|
|
61
73
|
onMount(async () => {
|
|
62
74
|
await refreshFiles();
|
|
63
75
|
await refreshProduction();
|
|
64
|
-
window.addEventListener('keydown', handleKeydown);
|
|
65
|
-
document.addEventListener('mousedown', handleDocumentMousedown, true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
onDestroy(() => {
|
|
69
|
-
window.removeEventListener('keydown', handleKeydown);
|
|
70
|
-
document.removeEventListener('mousedown', handleDocumentMousedown, true);
|
|
71
76
|
});
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
if (e.key === 'Escape' && infoOpen) {
|
|
75
|
-
infoOpen = false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function handleDocumentMousedown(e: MouseEvent) {
|
|
80
|
-
if (!infoOpen) return;
|
|
81
|
-
const target = e.target as Element | null;
|
|
82
|
-
if (target && !target.closest('.tfm-info-btn, .tfm-info-popover')) {
|
|
83
|
-
infoOpen = false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Anchor the fixed-position popover to the right of the info button. The
|
|
88
|
-
* sidebar lives on the left, so flow into the content area; flip up if the
|
|
89
|
-
* button is near the bottom of the viewport. Mirrors the preset popover so
|
|
90
|
-
* the two info surfaces feel identical. */
|
|
91
|
-
function positionInfoPopover(): void {
|
|
92
|
-
const btn = infoBtnEl;
|
|
93
|
-
const pop = infoPopoverEl;
|
|
94
|
-
if (!btn || !pop) return;
|
|
95
|
-
const br = btn.getBoundingClientRect();
|
|
96
|
-
const pr = pop.getBoundingClientRect();
|
|
97
|
-
const margin = 8;
|
|
98
|
-
const vw = window.innerWidth;
|
|
99
|
-
const vh = window.innerHeight;
|
|
100
|
-
let left = br.right + margin;
|
|
101
|
-
if (left + pr.width > vw - margin) {
|
|
102
|
-
left = br.left + br.width / 2 - pr.width / 2;
|
|
103
|
-
if (left < margin) left = margin;
|
|
104
|
-
if (left + pr.width > vw - margin) left = vw - margin - pr.width;
|
|
105
|
-
}
|
|
106
|
-
let top = br.bottom + margin;
|
|
107
|
-
if (top + pr.height > vh - margin) {
|
|
108
|
-
top = br.top - margin - pr.height;
|
|
109
|
-
if (top < margin) top = margin;
|
|
110
|
-
}
|
|
111
|
-
pop.style.left = `${left}px`;
|
|
112
|
-
pop.style.top = `${top}px`;
|
|
113
|
-
infoPopoverReady = true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
$effect(() => {
|
|
117
|
-
if (!infoOpen) {
|
|
118
|
-
infoPopoverReady = false;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
let raf1 = requestAnimationFrame(() => {
|
|
122
|
-
raf1 = requestAnimationFrame(positionInfoPopover);
|
|
123
|
-
});
|
|
124
|
-
window.addEventListener('scroll', positionInfoPopover, true);
|
|
125
|
-
window.addEventListener('resize', positionInfoPopover);
|
|
126
|
-
return () => {
|
|
127
|
-
cancelAnimationFrame(raf1);
|
|
128
|
-
window.removeEventListener('scroll', positionInfoPopover, true);
|
|
129
|
-
window.removeEventListener('resize', positionInfoPopover);
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Refresh production state when any production pointer flips (e.g. a preset
|
|
78
|
+
// Refresh production state when any production pointer flips (e.g. a manifest
|
|
134
79
|
// is adopted elsewhere). Skip the initial tick — onMount already loaded it.
|
|
135
80
|
let pulseInitialised = false;
|
|
136
81
|
$effect(() => {
|
|
@@ -143,35 +88,93 @@
|
|
|
143
88
|
});
|
|
144
89
|
|
|
145
90
|
function handleSave() {
|
|
146
|
-
|
|
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
|
+
}
|
|
147
98
|
onsave?.({ fileName: $activeFileName, displayName: currentDisplayName });
|
|
148
99
|
}
|
|
149
100
|
|
|
150
101
|
function openSaveAs() {
|
|
151
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;
|
|
152
106
|
saveAsDialog = true;
|
|
153
107
|
}
|
|
154
108
|
|
|
155
|
-
function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
109
|
+
async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
156
110
|
const { displayName, fileName } = detail;
|
|
157
|
-
onsave?.({ fileName, displayName });
|
|
111
|
+
await onsave?.({ fileName, displayName });
|
|
158
112
|
$activeFileName = fileName;
|
|
159
113
|
currentDisplayName = displayName;
|
|
160
|
-
|
|
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()}`;
|
|
161
139
|
}
|
|
162
140
|
|
|
163
141
|
async function handleApplyToProduction() {
|
|
164
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
|
+
|
|
165
153
|
prodApplyStatus = 'applying';
|
|
166
154
|
try {
|
|
167
155
|
await setProductionFile($activeFileName);
|
|
168
156
|
await refreshProduction();
|
|
169
157
|
bumpProductionRevision();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 });
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -189,12 +192,62 @@
|
|
|
189
192
|
onload?.({ fileName: file.fileName });
|
|
190
193
|
}
|
|
191
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
|
+
|
|
192
240
|
async function handleDelete(file: ThemeMeta) {
|
|
193
241
|
if (file.fileName === 'default') return;
|
|
242
|
+
if (file.fileName === $themeProductionInfo?.fileName) return;
|
|
194
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;
|
|
195
248
|
await deleteTheme(file.fileName);
|
|
196
249
|
await refreshFiles();
|
|
197
|
-
if (
|
|
250
|
+
if (wasActive) {
|
|
198
251
|
$activeFileName = 'default';
|
|
199
252
|
currentDisplayName = 'Default Theme';
|
|
200
253
|
onload?.({ fileName: 'default' });
|
|
@@ -209,57 +262,16 @@
|
|
|
209
262
|
if (showFileList) refreshFiles();
|
|
210
263
|
}
|
|
211
264
|
|
|
212
|
-
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
|
213
|
-
month: 'short',
|
|
214
|
-
day: 'numeric',
|
|
215
|
-
hour: 'numeric',
|
|
216
|
-
minute: '2-digit',
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
function formatUpdatedAt(iso: string): string {
|
|
220
|
-
if (!iso) return '';
|
|
221
|
-
const d = new Date(iso);
|
|
222
|
-
if (Number.isNaN(d.getTime())) return '';
|
|
223
|
-
return dateFormatter.format(d);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
type SortKey = 'name' | 'updatedAt';
|
|
227
|
-
let sortKey: SortKey = $state('updatedAt');
|
|
228
|
-
let sortDir: 'asc' | 'desc' = $state('desc');
|
|
229
|
-
|
|
230
|
-
function toggleSort(key: SortKey) {
|
|
231
|
-
if (sortKey === key) {
|
|
232
|
-
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
233
|
-
} else {
|
|
234
|
-
sortKey = key;
|
|
235
|
-
sortDir = key === 'name' ? 'asc' : 'desc';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
let sortedFiles = $derived([...files].sort((a, b) => {
|
|
240
|
-
let cmp = 0;
|
|
241
|
-
if (sortKey === 'name') {
|
|
242
|
-
cmp = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
243
|
-
} else {
|
|
244
|
-
cmp = (a.updatedAt || '').localeCompare(b.updatedAt || '');
|
|
245
|
-
}
|
|
246
|
-
return sortDir === 'asc' ? cmp : -cmp;
|
|
247
|
-
}));
|
|
248
265
|
</script>
|
|
249
266
|
|
|
250
267
|
<div class="theme-file-manager">
|
|
251
268
|
<div class="tfm-header">
|
|
252
269
|
<span class="tfm-header-label">Theme</span>
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
bind:this={infoBtnEl}
|
|
259
|
-
onclick={() => (infoOpen = !infoOpen)}
|
|
260
|
-
>
|
|
261
|
-
<i class="fas fa-circle-info"></i>
|
|
262
|
-
</button>
|
|
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>
|
|
263
275
|
</div>
|
|
264
276
|
|
|
265
277
|
<div class="tfm-cards" class:in-sync={prodIsInSync}>
|
|
@@ -278,11 +290,32 @@
|
|
|
278
290
|
>
|
|
279
291
|
<i class="tfm-status-dot" aria-hidden="true"></i>
|
|
280
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}
|
|
281
308
|
</span>
|
|
282
309
|
</div>
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
/>
|
|
286
319
|
<div class="tfm-card-actions tfm-card-actions-stack">
|
|
287
320
|
<button
|
|
288
321
|
class="tfm-btn tfm-btn-row save-btn"
|
|
@@ -290,9 +323,9 @@
|
|
|
290
323
|
class:saved={saveStatus === 'saved'}
|
|
291
324
|
class:error={saveStatus === 'error'}
|
|
292
325
|
onclick={handleSave}
|
|
293
|
-
disabled={saveStatus === 'saving'
|
|
326
|
+
disabled={saveStatus === 'saving'}
|
|
294
327
|
title={isDefaultActive
|
|
295
|
-
? '
|
|
328
|
+
? 'Save to a new theme file'
|
|
296
329
|
: 'Save to current file'}
|
|
297
330
|
>
|
|
298
331
|
<i
|
|
@@ -361,110 +394,49 @@
|
|
|
361
394
|
<span>{prodIsInSync ? 'live' : 'out of sync'}</span>
|
|
362
395
|
</span>
|
|
363
396
|
</div>
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
/>
|
|
367
405
|
</div>
|
|
368
406
|
</div>
|
|
369
407
|
</div>
|
|
370
408
|
|
|
371
|
-
|
|
372
|
-
<div
|
|
373
|
-
class="tfm-info-popover"
|
|
374
|
-
class:ready={infoPopoverReady}
|
|
375
|
-
role="dialog"
|
|
376
|
-
aria-label="About themes"
|
|
377
|
-
bind:this={infoPopoverEl}
|
|
378
|
-
>
|
|
379
|
-
<header class="tfm-info-header">
|
|
380
|
-
<span class="tfm-info-title">Themes</span>
|
|
381
|
-
<button
|
|
382
|
-
type="button"
|
|
383
|
-
class="tfm-info-close"
|
|
384
|
-
aria-label="Close"
|
|
385
|
-
onclick={() => (infoOpen = false)}
|
|
386
|
-
>
|
|
387
|
-
<i class="fas fa-xmark"></i>
|
|
388
|
-
</button>
|
|
389
|
-
</header>
|
|
390
|
-
<div class="tfm-info-body">
|
|
391
|
-
<p>
|
|
392
|
-
A <strong>theme</strong> saves the design tokens for a site, components use these tokens to define their appearance.
|
|
393
|
-
</p>
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
{/if}
|
|
397
|
-
|
|
398
|
-
<UIDialog
|
|
409
|
+
<FileLoadList
|
|
399
410
|
bind:show={showFileList}
|
|
400
411
|
title="Load Theme"
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<span>Name</span>
|
|
412
|
-
{#if sortKey === 'name'}
|
|
413
|
-
<i class="fas {sortDir === 'asc' ? 'fa-caret-up' : 'fa-caret-down'}"></i>
|
|
414
|
-
{/if}
|
|
415
|
-
</button>
|
|
416
|
-
<button
|
|
417
|
-
class="sort-btn date-col"
|
|
418
|
-
class:active-sort={sortKey === 'updatedAt'}
|
|
419
|
-
onclick={() => toggleSort('updatedAt')}
|
|
420
|
-
>
|
|
421
|
-
<span>Date</span>
|
|
422
|
-
{#if sortKey === 'updatedAt'}
|
|
423
|
-
<i class="fas {sortDir === 'asc' ? 'fa-caret-up' : 'fa-caret-down'}"></i>
|
|
424
|
-
{/if}
|
|
425
|
-
</button>
|
|
426
|
-
<span class="header-spacer"></span>
|
|
427
|
-
</div>
|
|
428
|
-
{#each sortedFiles as file}
|
|
429
|
-
<div class="load-item" class:active={file.fileName === $activeFileName}>
|
|
430
|
-
<button class="load-name-btn" onclick={() => handleLoad(file)}>
|
|
431
|
-
{file.name}
|
|
432
|
-
</button>
|
|
433
|
-
<span class="updated-at" title={file.updatedAt}>{formatUpdatedAt(file.updatedAt)}</span>
|
|
434
|
-
{#if file.fileName === $activeFileName}
|
|
435
|
-
<span class="active-badge">active</span>
|
|
436
|
-
{/if}
|
|
437
|
-
{#if file.fileName !== 'default'}
|
|
438
|
-
<button
|
|
439
|
-
class="file-delete-btn"
|
|
440
|
-
onclick={stopPropagation(() => handleDelete(file))}
|
|
441
|
-
title="Delete {file.name}"
|
|
442
|
-
>
|
|
443
|
-
<i class="fas fa-trash-alt"></i>
|
|
444
|
-
</button>
|
|
445
|
-
{/if}
|
|
446
|
-
</div>
|
|
447
|
-
{/each}
|
|
448
|
-
{#if files.length === 0}
|
|
449
|
-
<div class="load-item empty">No saved files</div>
|
|
450
|
-
{/if}
|
|
451
|
-
</div>
|
|
452
|
-
</UIDialog>
|
|
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
|
+
/>
|
|
453
422
|
|
|
454
423
|
<SaveAsDialog
|
|
455
424
|
bind:show={saveAsDialog}
|
|
456
425
|
{currentDisplayName}
|
|
457
426
|
{files}
|
|
427
|
+
currentFileName={$activeFileName}
|
|
428
|
+
reservedDisplayNames={protectedDisplayNames}
|
|
458
429
|
title="Save Theme As"
|
|
459
430
|
placeholder="Theme name…"
|
|
460
|
-
reservedNameMessage='
|
|
431
|
+
reservedNameMessage='That name is reserved for the protected default theme.'
|
|
432
|
+
branchFromDefaultName="My Theme"
|
|
461
433
|
onsave={confirmSaveAs}
|
|
462
434
|
/>
|
|
463
435
|
|
|
464
436
|
<style>
|
|
465
437
|
.theme-file-manager {
|
|
466
438
|
--tfm-applied: #5aa85e;
|
|
467
|
-
--tfm-rail-neutral: var(--ui-border
|
|
439
|
+
--tfm-rail-neutral: var(--ui-border);
|
|
468
440
|
--tfm-rail-dirty: var(--ui-highlight);
|
|
469
441
|
--tfm-rail-applied: var(--tfm-applied);
|
|
470
442
|
|
|
@@ -488,29 +460,9 @@
|
|
|
488
460
|
letter-spacing: 0.05em;
|
|
489
461
|
}
|
|
490
462
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
justify-content: center;
|
|
495
|
-
width: 22px;
|
|
496
|
-
height: 22px;
|
|
497
|
-
padding: 0;
|
|
498
|
-
background: transparent;
|
|
499
|
-
border: 0;
|
|
500
|
-
color: var(--ui-text-tertiary);
|
|
501
|
-
font-size: 0.95rem;
|
|
502
|
-
line-height: 1;
|
|
503
|
-
cursor: pointer;
|
|
504
|
-
transition: color var(--ui-transition-fast);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
.tfm-info-btn:hover,
|
|
508
|
-
.tfm-info-btn[aria-expanded='true'] {
|
|
509
|
-
color: var(--ui-text-primary);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/* Two-card pipeline (Editor → Production) — mirrors PresetFileManager so
|
|
513
|
-
theme and preset surfaces share one visual idiom. */
|
|
463
|
+
/* Two-card pipeline (Editor → Production) — theme card + production card
|
|
464
|
+
surface the per-artifact pipeline. The manifest panel sits one level up
|
|
465
|
+
and tracks active vs default rather than editor vs production. */
|
|
514
466
|
.tfm-cards {
|
|
515
467
|
display: flex;
|
|
516
468
|
flex-direction: column;
|
|
@@ -524,7 +476,7 @@
|
|
|
524
476
|
gap: var(--ui-space-6);
|
|
525
477
|
padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-10) var(--ui-space-16);
|
|
526
478
|
background: var(--ui-surface-lower);
|
|
527
|
-
border: 1px solid var(--ui-border-
|
|
479
|
+
border: 1px solid var(--ui-border-low);
|
|
528
480
|
border-radius: var(--ui-radius-md);
|
|
529
481
|
}
|
|
530
482
|
|
|
@@ -595,34 +547,87 @@
|
|
|
595
547
|
opacity: 1;
|
|
596
548
|
}
|
|
597
549
|
|
|
598
|
-
.
|
|
599
|
-
|
|
550
|
+
/* Revert button — paired with the "unsaved" status label. Lives only when
|
|
551
|
+
dirty, in the highlight color so it reads as part of the status group
|
|
552
|
+
rather than a peer of the action stack below. Two-step arm: first click
|
|
553
|
+
adds .armed (label appears, fills with highlight), second click commits. */
|
|
554
|
+
.tfm-revert-btn {
|
|
555
|
+
position: relative;
|
|
556
|
+
display: inline-flex;
|
|
600
557
|
align-items: center;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
558
|
+
justify-content: center;
|
|
559
|
+
gap: 4px;
|
|
560
|
+
height: 18px;
|
|
561
|
+
margin-left: 2px;
|
|
562
|
+
padding: 0 4px;
|
|
563
|
+
background: transparent;
|
|
564
|
+
border: 1px solid color-mix(in srgb, var(--ui-highlight) 35%, transparent);
|
|
565
|
+
border-radius: 4px;
|
|
566
|
+
color: var(--ui-highlight);
|
|
567
|
+
font-size: 0.65rem;
|
|
568
|
+
line-height: 1;
|
|
569
|
+
cursor: pointer;
|
|
570
|
+
opacity: 0.85;
|
|
571
|
+
overflow: hidden;
|
|
572
|
+
transition:
|
|
573
|
+
background var(--ui-transition-fast),
|
|
574
|
+
border-color var(--ui-transition-fast),
|
|
575
|
+
color var(--ui-transition-fast),
|
|
576
|
+
opacity var(--ui-transition-fast),
|
|
577
|
+
padding var(--ui-transition-fast);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.tfm-revert-btn:hover {
|
|
581
|
+
background: color-mix(in srgb, var(--ui-highlight) 18%, transparent);
|
|
582
|
+
border-color: color-mix(in srgb, var(--ui-highlight) 65%, transparent);
|
|
583
|
+
opacity: 1;
|
|
606
584
|
}
|
|
607
585
|
|
|
608
|
-
.tfm-
|
|
609
|
-
|
|
610
|
-
|
|
586
|
+
.tfm-revert-btn:focus-visible {
|
|
587
|
+
outline: 2px solid color-mix(in srgb, var(--ui-highlight) 60%, transparent);
|
|
588
|
+
outline-offset: 1px;
|
|
611
589
|
}
|
|
612
590
|
|
|
613
|
-
.tfm-
|
|
614
|
-
|
|
591
|
+
.tfm-revert-btn i {
|
|
592
|
+
font-size: 0.65rem;
|
|
593
|
+
transition: transform var(--ui-transition-fast);
|
|
615
594
|
}
|
|
616
595
|
|
|
617
|
-
.tfm-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
596
|
+
.tfm-revert-btn.armed {
|
|
597
|
+
background: var(--ui-highlight);
|
|
598
|
+
border-color: var(--ui-highlight);
|
|
599
|
+
color: var(--ui-surface-lowest, #111);
|
|
600
|
+
opacity: 1;
|
|
601
|
+
padding: 0 6px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.tfm-revert-btn.armed i {
|
|
605
|
+
transform: rotate(-35deg);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.tfm-revert-label {
|
|
609
|
+
font-weight: var(--ui-font-weight-semibold, 600);
|
|
610
|
+
letter-spacing: 0.02em;
|
|
623
611
|
white-space: nowrap;
|
|
624
|
-
|
|
625
|
-
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* Time-window cue — thin bar drains across the bottom edge as the 3s
|
|
615
|
+
auto-disarm window elapses. Pure CSS, runs once per arm cycle. */
|
|
616
|
+
.tfm-revert-btn.armed::after {
|
|
617
|
+
content: '';
|
|
618
|
+
position: absolute;
|
|
619
|
+
left: 0;
|
|
620
|
+
bottom: 0;
|
|
621
|
+
height: 2px;
|
|
622
|
+
width: 100%;
|
|
623
|
+
background: color-mix(in srgb, var(--ui-surface-lowest, #111) 70%, transparent);
|
|
624
|
+
transform-origin: left center;
|
|
625
|
+
animation: tfm-revert-drain 3s linear forwards;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
@keyframes tfm-revert-drain {
|
|
629
|
+
from { transform: scaleX(1); }
|
|
630
|
+
to { transform: scaleX(0); }
|
|
626
631
|
}
|
|
627
632
|
|
|
628
633
|
.tfm-card-actions {
|
|
@@ -666,7 +671,7 @@
|
|
|
666
671
|
margin: calc(var(--ui-space-2) * -1) 0;
|
|
667
672
|
padding: var(--ui-space-6) var(--ui-space-12);
|
|
668
673
|
background: color-mix(in srgb, var(--tfm-applied) 18%, var(--ui-surface-high));
|
|
669
|
-
border: 1px solid color-mix(in srgb, var(--tfm-applied) 45%, var(--ui-border-
|
|
674
|
+
border: 1px solid color-mix(in srgb, var(--tfm-applied) 45%, var(--ui-border-high));
|
|
670
675
|
border-radius: var(--ui-radius-md);
|
|
671
676
|
color: var(--ui-text-primary);
|
|
672
677
|
font-size: var(--ui-font-size-md);
|
|
@@ -686,7 +691,7 @@
|
|
|
686
691
|
|
|
687
692
|
.tfm-adopt-btn:hover:not(:disabled) {
|
|
688
693
|
background: color-mix(in srgb, var(--tfm-applied) 30%, var(--ui-surface-higher));
|
|
689
|
-
border-color: color-mix(in srgb, var(--tfm-applied) 70%, var(--ui-border-
|
|
694
|
+
border-color: color-mix(in srgb, var(--tfm-applied) 70%, var(--ui-border-higher));
|
|
690
695
|
}
|
|
691
696
|
|
|
692
697
|
.tfm-adopt-btn:disabled {
|
|
@@ -695,7 +700,7 @@
|
|
|
695
700
|
|
|
696
701
|
.tfm-adopt-btn.in-sync {
|
|
697
702
|
background: transparent;
|
|
698
|
-
border-color: var(--ui-border-
|
|
703
|
+
border-color: var(--ui-border-low);
|
|
699
704
|
color: var(--ui-text-muted);
|
|
700
705
|
opacity: 0.7;
|
|
701
706
|
}
|
|
@@ -714,7 +719,7 @@
|
|
|
714
719
|
gap: var(--ui-space-4);
|
|
715
720
|
padding: var(--ui-space-6) var(--ui-space-10);
|
|
716
721
|
background: var(--ui-surface);
|
|
717
|
-
border: 1px solid var(--ui-border-
|
|
722
|
+
border: 1px solid var(--ui-border-low);
|
|
718
723
|
border-radius: var(--ui-radius-md);
|
|
719
724
|
color: var(--ui-text-secondary);
|
|
720
725
|
font-size: var(--ui-font-size-md);
|
|
@@ -735,7 +740,7 @@
|
|
|
735
740
|
.tfm-btn:hover:not(:disabled) {
|
|
736
741
|
background: var(--ui-surface-high);
|
|
737
742
|
color: var(--ui-text-primary);
|
|
738
|
-
border-color: var(--ui-border
|
|
743
|
+
border-color: var(--ui-border);
|
|
739
744
|
}
|
|
740
745
|
|
|
741
746
|
.tfm-btn:disabled {
|
|
@@ -745,19 +750,19 @@
|
|
|
745
750
|
|
|
746
751
|
.tfm-btn.active {
|
|
747
752
|
background: var(--ui-surface);
|
|
748
|
-
border-color: var(--ui-border
|
|
753
|
+
border-color: var(--ui-border);
|
|
749
754
|
color: var(--ui-text-primary);
|
|
750
755
|
}
|
|
751
756
|
|
|
752
757
|
.save-btn {
|
|
753
758
|
background: var(--ui-surface-high);
|
|
754
|
-
border-color: var(--ui-border-
|
|
759
|
+
border-color: var(--ui-border-high);
|
|
755
760
|
color: var(--ui-text-primary);
|
|
756
761
|
}
|
|
757
762
|
|
|
758
763
|
.save-btn:hover:not(:disabled) {
|
|
759
764
|
background: var(--ui-surface-higher);
|
|
760
|
-
border-color: var(--ui-border-
|
|
765
|
+
border-color: var(--ui-border-higher);
|
|
761
766
|
}
|
|
762
767
|
|
|
763
768
|
.save-btn.saving i { animation: spin 1s linear infinite; }
|
|
@@ -770,244 +775,6 @@
|
|
|
770
775
|
color: var(--ui-text-muted);
|
|
771
776
|
}
|
|
772
777
|
|
|
773
|
-
.load-list {
|
|
774
|
-
display: flex;
|
|
775
|
-
flex-direction: column;
|
|
776
|
-
max-height: 60vh;
|
|
777
|
-
overflow-y: auto;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
.load-header {
|
|
781
|
-
display: flex;
|
|
782
|
-
align-items: center;
|
|
783
|
-
gap: 6px;
|
|
784
|
-
padding: 4px 6px;
|
|
785
|
-
border-bottom: 1px solid #3a3a3a;
|
|
786
|
-
position: sticky;
|
|
787
|
-
top: 0;
|
|
788
|
-
background: var(--ui-surface, #1a1a1a);
|
|
789
|
-
z-index: 1;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
.sort-btn {
|
|
793
|
-
display: inline-flex;
|
|
794
|
-
align-items: center;
|
|
795
|
-
gap: 4px;
|
|
796
|
-
padding: 4px 0;
|
|
797
|
-
background: none;
|
|
798
|
-
border: none;
|
|
799
|
-
color: #888;
|
|
800
|
-
font-size: 11px;
|
|
801
|
-
font-weight: 600;
|
|
802
|
-
text-transform: uppercase;
|
|
803
|
-
letter-spacing: 0.04em;
|
|
804
|
-
cursor: pointer;
|
|
805
|
-
text-align: left;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
.sort-btn:hover {
|
|
809
|
-
color: #ccc;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
.sort-btn.active-sort {
|
|
813
|
-
color: #e0e0e0;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
.sort-btn i {
|
|
817
|
-
font-size: 10px;
|
|
818
|
-
opacity: 0.85;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
.sort-btn.name-col {
|
|
822
|
-
flex: 1;
|
|
823
|
-
min-width: 0;
|
|
824
|
-
padding-left: 4px;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
.sort-btn.date-col {
|
|
828
|
-
flex-shrink: 0;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
.header-spacer {
|
|
832
|
-
flex-shrink: 0;
|
|
833
|
-
width: 24px;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
.load-item {
|
|
837
|
-
display: flex;
|
|
838
|
-
align-items: center;
|
|
839
|
-
gap: 6px;
|
|
840
|
-
padding: 4px 6px;
|
|
841
|
-
border-bottom: 1px solid #2a2a2a;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
.load-item:last-child {
|
|
845
|
-
border-bottom: none;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
.load-item.empty {
|
|
849
|
-
padding: 16px;
|
|
850
|
-
color: #888;
|
|
851
|
-
font-size: 14px;
|
|
852
|
-
text-align: center;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
.load-name-btn {
|
|
856
|
-
flex: 1;
|
|
857
|
-
min-width: 0;
|
|
858
|
-
overflow: hidden;
|
|
859
|
-
text-overflow: ellipsis;
|
|
860
|
-
white-space: nowrap;
|
|
861
|
-
padding: 6px 4px;
|
|
862
|
-
background: none;
|
|
863
|
-
border: none;
|
|
864
|
-
color: #aaa;
|
|
865
|
-
font-size: 14px;
|
|
866
|
-
cursor: pointer;
|
|
867
|
-
text-align: left;
|
|
868
|
-
border-radius: 3px;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
.load-name-btn:hover {
|
|
872
|
-
color: #e0e0e0;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
.load-item.active .load-name-btn {
|
|
876
|
-
color: #e0e0e0;
|
|
877
|
-
font-weight: 600;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
.updated-at {
|
|
881
|
-
flex-shrink: 0;
|
|
882
|
-
font-size: 12px;
|
|
883
|
-
color: #777;
|
|
884
|
-
font-variant-numeric: tabular-nums;
|
|
885
|
-
white-space: nowrap;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
.active-badge {
|
|
889
|
-
flex-shrink: 0;
|
|
890
|
-
font-size: 12px;
|
|
891
|
-
padding: 1px 6px;
|
|
892
|
-
border-radius: 3px;
|
|
893
|
-
background: #333;
|
|
894
|
-
color: #ccc;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
.file-delete-btn {
|
|
898
|
-
flex-shrink: 0;
|
|
899
|
-
display: flex;
|
|
900
|
-
align-items: center;
|
|
901
|
-
justify-content: center;
|
|
902
|
-
width: 24px;
|
|
903
|
-
height: 24px;
|
|
904
|
-
padding: 0;
|
|
905
|
-
background: none;
|
|
906
|
-
border: none;
|
|
907
|
-
color: #555;
|
|
908
|
-
font-size: 12px;
|
|
909
|
-
cursor: pointer;
|
|
910
|
-
opacity: 0;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
.load-item:hover .file-delete-btn {
|
|
914
|
-
opacity: 1;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
.file-delete-btn:hover {
|
|
918
|
-
color: #ccc;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/* Info popover — fixed positioning escapes the sidebar's overflow and any
|
|
922
|
-
parent stacking context. JS in this file anchors it to the right of the
|
|
923
|
-
info button (the sidebar is on the left, so there's room to flow into
|
|
924
|
-
the main content area without obscuring the button). */
|
|
925
|
-
.tfm-info-popover {
|
|
926
|
-
position: fixed;
|
|
927
|
-
top: 0;
|
|
928
|
-
left: 0;
|
|
929
|
-
width: 22rem;
|
|
930
|
-
max-width: calc(100vw - var(--ui-space-24));
|
|
931
|
-
padding: 0;
|
|
932
|
-
background: var(--ui-surface-higher);
|
|
933
|
-
border: 1px solid var(--ui-border-medium);
|
|
934
|
-
border-radius: var(--ui-radius-lg);
|
|
935
|
-
box-shadow: var(--ui-shadow-lg);
|
|
936
|
-
z-index: 1000;
|
|
937
|
-
color: var(--ui-text-secondary);
|
|
938
|
-
font-family: var(--ui-font-family, system-ui, sans-serif);
|
|
939
|
-
overflow: hidden;
|
|
940
|
-
visibility: hidden;
|
|
941
|
-
animation: tfm-info-in 140ms ease-out;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
.tfm-info-popover.ready {
|
|
945
|
-
visibility: visible;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
.tfm-info-header {
|
|
949
|
-
display: flex;
|
|
950
|
-
align-items: center;
|
|
951
|
-
justify-content: space-between;
|
|
952
|
-
gap: var(--ui-space-8);
|
|
953
|
-
padding: var(--ui-space-10) var(--ui-space-12) var(--ui-space-10) var(--ui-space-16);
|
|
954
|
-
border-bottom: 1px solid var(--ui-border-subtle);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
.tfm-info-title {
|
|
958
|
-
color: var(--ui-text-primary);
|
|
959
|
-
font-size: var(--ui-font-size-sm);
|
|
960
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
961
|
-
letter-spacing: -0.01em;
|
|
962
|
-
line-height: 1.2;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
.tfm-info-close {
|
|
966
|
-
display: inline-flex;
|
|
967
|
-
align-items: center;
|
|
968
|
-
justify-content: center;
|
|
969
|
-
width: var(--ui-space-24);
|
|
970
|
-
height: var(--ui-space-24);
|
|
971
|
-
padding: 0;
|
|
972
|
-
background: transparent;
|
|
973
|
-
border: 0;
|
|
974
|
-
border-radius: var(--ui-radius-sm);
|
|
975
|
-
color: var(--ui-text-tertiary);
|
|
976
|
-
font-size: var(--ui-font-size-xs);
|
|
977
|
-
line-height: 1;
|
|
978
|
-
cursor: pointer;
|
|
979
|
-
transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
.tfm-info-close:hover {
|
|
983
|
-
color: var(--ui-text-primary);
|
|
984
|
-
background: var(--ui-hover);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
.tfm-info-body {
|
|
988
|
-
padding: var(--ui-space-16);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
.tfm-info-popover p {
|
|
992
|
-
margin: 0 0 var(--ui-space-12) 0;
|
|
993
|
-
font-size: var(--ui-font-size-xs);
|
|
994
|
-
line-height: 1.55;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
.tfm-info-popover p:last-child {
|
|
998
|
-
margin-bottom: 0;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
.tfm-info-popover strong {
|
|
1002
|
-
color: var(--ui-text-primary);
|
|
1003
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
@keyframes tfm-info-in {
|
|
1007
|
-
from { opacity: 0; transform: translateY(-3px); }
|
|
1008
|
-
to { opacity: 1; transform: translateY(0); }
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
778
|
@keyframes tfm-pulse {
|
|
1012
779
|
0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
|
|
1013
780
|
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
|