@motion-proto/live-tokens 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-plugin/index.cjs +80 -4
- package/dist-plugin/index.js +80 -4
- package/package.json +2 -2
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +3 -1
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +24 -7
- package/src/lib/ColumnsOverlay.svelte +1 -1
- package/src/lib/componentPersist.ts +65 -0
- package/src/lib/presetService.ts +121 -1
- package/src/lib/productionPulse.ts +32 -0
- package/src/pages/ComponentEditorPage.svelte +25 -0
- package/src/pages/EditorShell.svelte +25 -0
- package/src/ui/PresetFileManager.svelte +763 -216
- package/src/ui/ThemeFileManager.svelte +557 -307
- package/src/ui/UnsavedComponentsDialog.svelte +315 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { stopPropagation } from 'svelte/legacy';
|
|
3
3
|
|
|
4
|
-
import { onMount } from 'svelte';
|
|
4
|
+
import { onMount, onDestroy } from 'svelte';
|
|
5
5
|
import type { ThemeMeta } from '../lib/themeTypes';
|
|
6
|
-
import { listThemes, deleteTheme, setActiveFile,
|
|
7
|
-
import type { ProductionInfo } from '../lib/themeService';
|
|
6
|
+
import { listThemes, deleteTheme, setActiveFile, getProductionInfo, setProductionFile } from '../lib/themeService';
|
|
8
7
|
import { activeFileName } from '../lib/editorConfigStore';
|
|
9
8
|
import { dirty } from '../lib/editorStore';
|
|
9
|
+
import { productionRevision, bumpProductionRevision, themeProductionInfo } from '../lib/productionPulse';
|
|
10
10
|
import UIDialog from './UIDialog.svelte';
|
|
11
|
+
import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
saveStatus?: 'idle' | 'saving' | 'saved' | 'error';
|
|
@@ -19,19 +20,26 @@
|
|
|
19
20
|
|
|
20
21
|
let files: ThemeMeta[] = $state([]);
|
|
21
22
|
let showFileList = $state(false);
|
|
22
|
-
let
|
|
23
|
-
let
|
|
24
|
-
let saveAsInput: HTMLInputElement | undefined = $state();
|
|
25
|
-
let currentDisplayName = $state('Default');
|
|
23
|
+
let saveAsDialog = $state(false);
|
|
24
|
+
let currentDisplayName = $state('Default Theme');
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let
|
|
26
|
+
let prodApplyStatus: 'idle' | 'applying' | 'done' | 'error' = $state('idle');
|
|
27
|
+
|
|
28
|
+
let infoOpen = $state(false);
|
|
29
|
+
let infoBtnEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
30
|
+
let infoPopoverEl = $state<HTMLDivElement | undefined>(undefined);
|
|
31
|
+
let infoPopoverReady = $state(false);
|
|
32
|
+
|
|
33
|
+
let prodIsInSync = $derived($themeProductionInfo?.fileName === $activeFileName);
|
|
34
|
+
let editorIsApplied = $derived(prodIsInSync && !$dirty);
|
|
35
|
+
let prodName = $derived($themeProductionInfo?.name ?? '—');
|
|
36
|
+
|
|
37
|
+
let isDefaultActive = $derived($activeFileName === 'default');
|
|
30
38
|
|
|
31
39
|
async function refreshFiles() {
|
|
32
40
|
try {
|
|
33
41
|
files = await listThemes();
|
|
34
|
-
const active = files.find(f => f.isActive);
|
|
42
|
+
const active = files.find((f) => f.isActive);
|
|
35
43
|
if (active) {
|
|
36
44
|
$activeFileName = active.fileName;
|
|
37
45
|
currentDisplayName = active.name;
|
|
@@ -43,90 +51,142 @@
|
|
|
43
51
|
|
|
44
52
|
async function refreshProduction() {
|
|
45
53
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// silent
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function handleUpdateProduction() {
|
|
53
|
-
productionUpdateStatus = 'updating';
|
|
54
|
-
try {
|
|
55
|
-
await setProductionFile($activeFileName);
|
|
56
|
-
await refreshProduction();
|
|
57
|
-
productionUpdateStatus = 'done';
|
|
58
|
-
setTimeout(() => { productionUpdateStatus = 'idle'; }, 2000);
|
|
54
|
+
const info = await getProductionInfo();
|
|
55
|
+
themeProductionInfo.set(info);
|
|
59
56
|
} catch {
|
|
60
|
-
|
|
61
|
-
setTimeout(() => { productionUpdateStatus = 'idle'; }, 2000);
|
|
57
|
+
// silent — leave cached value in place
|
|
62
58
|
}
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
onMount(async () => {
|
|
66
62
|
await refreshFiles();
|
|
67
63
|
await refreshProduction();
|
|
64
|
+
window.addEventListener('keydown', handleKeydown);
|
|
65
|
+
document.addEventListener('mousedown', handleDocumentMousedown, true);
|
|
68
66
|
});
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
onDestroy(() => {
|
|
69
|
+
window.removeEventListener('keydown', handleKeydown);
|
|
70
|
+
document.removeEventListener('mousedown', handleDocumentMousedown, true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
74
|
+
if (e.key === 'Escape' && infoOpen) {
|
|
75
|
+
infoOpen = false;
|
|
76
|
+
}
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const m = f.fileName.match(/_(\d+)$/);
|
|
82
|
-
return m ? parseInt(m[1], 10) : 0;
|
|
83
|
-
});
|
|
84
|
-
const next = (existingNums.length > 0 ? Math.max(...existingNums) : 0) + 1;
|
|
85
|
-
const suffix = String(next).padStart(2, '0');
|
|
86
|
-
const displayName = `${baseName}_${suffix}`;
|
|
87
|
-
const fileName = `${baseFileName}_${suffix}`;
|
|
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
|
+
}
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
134
|
+
// is adopted elsewhere). Skip the initial tick — onMount already loaded it.
|
|
135
|
+
let pulseInitialised = false;
|
|
136
|
+
$effect(() => {
|
|
137
|
+
void $productionRevision;
|
|
138
|
+
if (!pulseInitialised) {
|
|
139
|
+
pulseInitialised = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
refreshProduction();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function handleSave() {
|
|
146
|
+
if (isDefaultActive) return;
|
|
147
|
+
onsave?.({ fileName: $activeFileName, displayName: currentDisplayName });
|
|
93
148
|
}
|
|
94
149
|
|
|
95
150
|
function openSaveAs() {
|
|
96
|
-
saveAsName = currentDisplayName;
|
|
97
|
-
saveAsEditing = true;
|
|
98
151
|
showFileList = false;
|
|
99
|
-
|
|
100
|
-
setTimeout(() => saveAsInput?.select(), 0);
|
|
152
|
+
saveAsDialog = true;
|
|
101
153
|
}
|
|
102
154
|
|
|
103
|
-
function confirmSaveAs() {
|
|
104
|
-
const displayName =
|
|
105
|
-
if (!displayName) return;
|
|
106
|
-
const fileName = sanitizeFileName(displayName);
|
|
107
|
-
if (fileName === 'default') {
|
|
108
|
-
saveAsName = '';
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
saveAsEditing = false;
|
|
155
|
+
function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
156
|
+
const { displayName, fileName } = detail;
|
|
112
157
|
onsave?.({ fileName, displayName });
|
|
113
158
|
$activeFileName = fileName;
|
|
114
159
|
currentDisplayName = displayName;
|
|
115
160
|
setTimeout(() => refreshFiles(), 500);
|
|
116
161
|
}
|
|
117
162
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
async function handleApplyToProduction() {
|
|
164
|
+
if (prodIsInSync) return;
|
|
165
|
+
prodApplyStatus = 'applying';
|
|
166
|
+
try {
|
|
167
|
+
await setProductionFile($activeFileName);
|
|
168
|
+
await refreshProduction();
|
|
169
|
+
bumpProductionRevision();
|
|
170
|
+
prodApplyStatus = 'done';
|
|
171
|
+
setTimeout(() => { prodApplyStatus = 'idle'; }, 2000);
|
|
172
|
+
} catch {
|
|
173
|
+
prodApplyStatus = 'error';
|
|
174
|
+
setTimeout(() => { prodApplyStatus = 'idle'; }, 3000);
|
|
175
|
+
}
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
async function handleLoad(file: ThemeMeta) {
|
|
179
|
+
if ($dirty) {
|
|
180
|
+
const ok = window.confirm(
|
|
181
|
+
'Loading a theme will discard unsaved changes. Continue?',
|
|
182
|
+
);
|
|
183
|
+
if (!ok) return;
|
|
184
|
+
}
|
|
124
185
|
showFileList = false;
|
|
125
186
|
await setActiveFile(file.fileName);
|
|
126
187
|
$activeFileName = file.fileName;
|
|
127
188
|
currentDisplayName = file.name;
|
|
128
189
|
onload?.({ fileName: file.fileName });
|
|
129
|
-
// editorStore.loadFromFile clears history and resets dirty — no snapshot needed here.
|
|
130
190
|
}
|
|
131
191
|
|
|
132
192
|
async function handleDelete(file: ThemeMeta) {
|
|
@@ -134,10 +194,9 @@
|
|
|
134
194
|
try {
|
|
135
195
|
await deleteTheme(file.fileName);
|
|
136
196
|
await refreshFiles();
|
|
137
|
-
// If we deleted the active file, it reverts to default on the server
|
|
138
197
|
if (file.fileName === $activeFileName) {
|
|
139
198
|
$activeFileName = 'default';
|
|
140
|
-
currentDisplayName = 'Default';
|
|
199
|
+
currentDisplayName = 'Default Theme';
|
|
141
200
|
onload?.({ fileName: 'default' });
|
|
142
201
|
}
|
|
143
202
|
} catch {
|
|
@@ -145,14 +204,8 @@
|
|
|
145
204
|
}
|
|
146
205
|
}
|
|
147
206
|
|
|
148
|
-
function handleSaveAsKeydown(e: KeyboardEvent) {
|
|
149
|
-
if (e.key === 'Enter') confirmSaveAs();
|
|
150
|
-
if (e.key === 'Escape') cancelSaveAs();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
207
|
function toggleFileList() {
|
|
154
208
|
showFileList = !showFileList;
|
|
155
|
-
saveAsEditing = false;
|
|
156
209
|
if (showFileList) refreshFiles();
|
|
157
210
|
}
|
|
158
211
|
|
|
@@ -179,7 +232,6 @@
|
|
|
179
232
|
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
180
233
|
} else {
|
|
181
234
|
sortKey = key;
|
|
182
|
-
// Default: names asc, dates desc (most-recent first)
|
|
183
235
|
sortDir = key === 'name' ? 'asc' : 'desc';
|
|
184
236
|
}
|
|
185
237
|
}
|
|
@@ -196,119 +248,153 @@
|
|
|
196
248
|
</script>
|
|
197
249
|
|
|
198
250
|
<div class="theme-file-manager">
|
|
199
|
-
<div class="
|
|
200
|
-
<span class="
|
|
201
|
-
<span class="active-name">{currentDisplayName}</span>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<div class="button-grid">
|
|
206
|
-
<div class="save-row">
|
|
207
|
-
<button
|
|
208
|
-
class="tfm-btn save-btn"
|
|
209
|
-
class:saving={saveStatus === 'saving'}
|
|
210
|
-
class:saved={saveStatus === 'saved'}
|
|
211
|
-
class:error={saveStatus === 'error'}
|
|
212
|
-
onclick={handleSave}
|
|
213
|
-
disabled={saveStatus === 'saving'}
|
|
214
|
-
title="Save to current file"
|
|
215
|
-
>
|
|
216
|
-
<i class="fas" class:fa-save={saveStatus === 'idle'} class:fa-spinner={saveStatus === 'saving'} class:fa-check={saveStatus === 'saved'} class:fa-times={saveStatus === 'error'}></i>
|
|
217
|
-
<span>
|
|
218
|
-
{#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
|
|
219
|
-
</span>
|
|
220
|
-
</button>
|
|
221
|
-
<button
|
|
222
|
-
class="tfm-btn increment-btn"
|
|
223
|
-
onclick={handleSaveIncrement}
|
|
224
|
-
disabled={saveStatus === 'saving'}
|
|
225
|
-
title="Save as incremented copy"
|
|
226
|
-
>
|
|
227
|
-
<i class="fas fa-plus"></i>
|
|
228
|
-
</button>
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
{#if saveAsEditing}
|
|
232
|
-
<div class="save-as-inline">
|
|
233
|
-
<input
|
|
234
|
-
class="save-as-input"
|
|
235
|
-
type="text"
|
|
236
|
-
bind:value={saveAsName}
|
|
237
|
-
bind:this={saveAsInput}
|
|
238
|
-
onkeydown={handleSaveAsKeydown}
|
|
239
|
-
placeholder="Theme name..."
|
|
240
|
-
/>
|
|
241
|
-
<div class="save-as-actions">
|
|
242
|
-
<button class="inline-btn confirm-btn" onclick={confirmSaveAs} disabled={!saveAsName.trim()} title="Save">
|
|
243
|
-
<i class="fas fa-check"></i>
|
|
244
|
-
</button>
|
|
245
|
-
<button class="inline-btn cancel-btn" onclick={cancelSaveAs} title="Cancel">
|
|
246
|
-
<i class="fas fa-times"></i>
|
|
247
|
-
</button>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
{:else}
|
|
251
|
-
<button
|
|
252
|
-
class="tfm-btn"
|
|
253
|
-
onclick={openSaveAs}
|
|
254
|
-
title="Save as new file"
|
|
255
|
-
>
|
|
256
|
-
<i class="fas fa-copy"></i>
|
|
257
|
-
<span>Save As</span>
|
|
258
|
-
</button>
|
|
259
|
-
{/if}
|
|
260
|
-
|
|
251
|
+
<div class="tfm-header">
|
|
252
|
+
<span class="tfm-header-label">Theme</span>
|
|
261
253
|
<button
|
|
262
|
-
|
|
263
|
-
class
|
|
264
|
-
|
|
265
|
-
|
|
254
|
+
type="button"
|
|
255
|
+
class="tfm-info-btn"
|
|
256
|
+
aria-label="About themes"
|
|
257
|
+
aria-expanded={infoOpen}
|
|
258
|
+
bind:this={infoBtnEl}
|
|
259
|
+
onclick={() => (infoOpen = !infoOpen)}
|
|
266
260
|
>
|
|
267
|
-
<i class="fas fa-
|
|
268
|
-
<span>Load</span>
|
|
261
|
+
<i class="fas fa-circle-info"></i>
|
|
269
262
|
</button>
|
|
270
|
-
|
|
271
|
-
|
|
272
263
|
</div>
|
|
273
264
|
|
|
274
|
-
<div class="
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
265
|
+
<div class="tfm-cards" class:in-sync={prodIsInSync}>
|
|
266
|
+
<div
|
|
267
|
+
class="tfm-card tfm-card-editor"
|
|
268
|
+
class:dirty={$dirty}
|
|
269
|
+
class:applied={editorIsApplied}
|
|
270
|
+
>
|
|
271
|
+
<span class="tfm-rail" aria-hidden="true"></span>
|
|
272
|
+
<div class="tfm-card-head">
|
|
273
|
+
<span class="tfm-card-label">Editor</span>
|
|
274
|
+
<span
|
|
275
|
+
class="tfm-card-status"
|
|
276
|
+
class:dirty={$dirty}
|
|
277
|
+
class:applied={editorIsApplied}
|
|
278
|
+
>
|
|
279
|
+
<i class="tfm-status-dot" aria-hidden="true"></i>
|
|
280
|
+
<span>{$dirty ? 'unsaved' : editorIsApplied ? 'live' : 'saved'}</span>
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="tfm-pill" class:dirty={$dirty} class:applied={editorIsApplied}>
|
|
284
|
+
<span class="tfm-pill-name" title={currentDisplayName}>{currentDisplayName}</span>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="tfm-card-actions tfm-card-actions-stack">
|
|
287
|
+
<button
|
|
288
|
+
class="tfm-btn tfm-btn-row save-btn"
|
|
289
|
+
class:saving={saveStatus === 'saving'}
|
|
290
|
+
class:saved={saveStatus === 'saved'}
|
|
291
|
+
class:error={saveStatus === 'error'}
|
|
292
|
+
onclick={handleSave}
|
|
293
|
+
disabled={saveStatus === 'saving' || isDefaultActive}
|
|
294
|
+
title={isDefaultActive
|
|
295
|
+
? 'Default is read-only — use Save As to capture under a new name'
|
|
296
|
+
: 'Save to current file'}
|
|
297
|
+
>
|
|
298
|
+
<i
|
|
299
|
+
class="fas"
|
|
300
|
+
class:fa-save={saveStatus === 'idle'}
|
|
301
|
+
class:fa-spinner={saveStatus === 'saving'}
|
|
302
|
+
class:fa-check={saveStatus === 'saved'}
|
|
303
|
+
class:fa-times={saveStatus === 'error'}
|
|
304
|
+
></i>
|
|
305
|
+
<span>
|
|
306
|
+
{#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
|
|
307
|
+
</span>
|
|
308
|
+
</button>
|
|
309
|
+
<button class="tfm-btn tfm-btn-row" onclick={openSaveAs} title="Save as new theme">
|
|
310
|
+
<i class="fas fa-copy"></i>
|
|
311
|
+
<span>Save As…</span>
|
|
312
|
+
</button>
|
|
313
|
+
<button
|
|
314
|
+
class="tfm-btn tfm-btn-row"
|
|
315
|
+
class:active={showFileList}
|
|
316
|
+
onclick={toggleFileList}
|
|
317
|
+
title="Load a theme"
|
|
318
|
+
>
|
|
319
|
+
<i class="fas fa-folder-open"></i>
|
|
320
|
+
<span>Load…</span>
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
286
323
|
</div>
|
|
287
324
|
|
|
288
325
|
<button
|
|
289
|
-
class="tfm-
|
|
290
|
-
class:
|
|
291
|
-
class:
|
|
292
|
-
class:error={
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
326
|
+
class="tfm-adopt-btn"
|
|
327
|
+
class:saving={prodApplyStatus === 'applying'}
|
|
328
|
+
class:saved={prodApplyStatus === 'done'}
|
|
329
|
+
class:error={prodApplyStatus === 'error'}
|
|
330
|
+
class:in-sync={prodIsInSync}
|
|
331
|
+
onclick={handleApplyToProduction}
|
|
332
|
+
disabled={prodApplyStatus === 'applying' || prodIsInSync}
|
|
333
|
+
title={prodIsInSync
|
|
334
|
+
? 'This theme is already in production'
|
|
335
|
+
: `Adopt "${currentDisplayName}" as the production theme`}
|
|
296
336
|
>
|
|
297
|
-
<i
|
|
337
|
+
<i
|
|
338
|
+
class="fas"
|
|
339
|
+
class:fa-arrow-down={prodApplyStatus === 'idle'}
|
|
340
|
+
class:fa-spinner={prodApplyStatus === 'applying'}
|
|
341
|
+
class:fa-check={prodApplyStatus === 'done'}
|
|
342
|
+
class:fa-xmark={prodApplyStatus === 'error'}
|
|
343
|
+
></i>
|
|
298
344
|
<span>
|
|
299
|
-
{#if
|
|
345
|
+
{#if prodApplyStatus === 'idle'}Adopt{:else if prodApplyStatus === 'applying'}Adopting{:else if prodApplyStatus === 'done'}Adopted{:else}Error{/if}
|
|
300
346
|
</span>
|
|
301
347
|
</button>
|
|
302
348
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
349
|
+
<div
|
|
350
|
+
class="tfm-card tfm-card-production"
|
|
351
|
+
class:in-sync={prodIsInSync}
|
|
352
|
+
>
|
|
353
|
+
<span class="tfm-rail" aria-hidden="true"></span>
|
|
354
|
+
<div class="tfm-card-head">
|
|
355
|
+
<span class="tfm-card-label">Production</span>
|
|
356
|
+
<span
|
|
357
|
+
class="tfm-card-status"
|
|
358
|
+
class:applied={prodIsInSync}
|
|
359
|
+
>
|
|
360
|
+
<i class="tfm-status-dot" aria-hidden="true"></i>
|
|
361
|
+
<span>{prodIsInSync ? 'live' : 'out of sync'}</span>
|
|
362
|
+
</span>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="tfm-pill" class:applied={prodIsInSync}>
|
|
365
|
+
<span class="tfm-pill-name" title={prodName}>{prodName}</span>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
308
368
|
</div>
|
|
309
|
-
|
|
310
369
|
</div>
|
|
311
370
|
|
|
371
|
+
{#if infoOpen}
|
|
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
|
+
|
|
312
398
|
<UIDialog
|
|
313
399
|
bind:show={showFileList}
|
|
314
400
|
title="Load Theme"
|
|
@@ -365,178 +451,325 @@
|
|
|
365
451
|
</div>
|
|
366
452
|
</UIDialog>
|
|
367
453
|
|
|
454
|
+
<SaveAsDialog
|
|
455
|
+
bind:show={saveAsDialog}
|
|
456
|
+
{currentDisplayName}
|
|
457
|
+
{files}
|
|
458
|
+
title="Save Theme As"
|
|
459
|
+
placeholder="Theme name…"
|
|
460
|
+
reservedNameMessage='The name "default" is reserved for the initial distribution.'
|
|
461
|
+
onsave={confirmSaveAs}
|
|
462
|
+
/>
|
|
368
463
|
|
|
369
464
|
<style>
|
|
370
465
|
.theme-file-manager {
|
|
466
|
+
--tfm-applied: #5aa85e;
|
|
467
|
+
--tfm-rail-neutral: var(--ui-border-default);
|
|
468
|
+
--tfm-rail-dirty: var(--ui-highlight);
|
|
469
|
+
--tfm-rail-applied: var(--tfm-applied);
|
|
470
|
+
|
|
371
471
|
display: flex;
|
|
372
472
|
flex-direction: column;
|
|
373
473
|
gap: var(--ui-space-8);
|
|
374
474
|
}
|
|
375
475
|
|
|
376
|
-
.
|
|
476
|
+
.tfm-header {
|
|
377
477
|
display: flex;
|
|
378
|
-
|
|
379
|
-
|
|
478
|
+
align-items: center;
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
gap: var(--ui-space-4);
|
|
380
481
|
padding: 0 var(--ui-space-4);
|
|
381
482
|
}
|
|
382
483
|
|
|
383
|
-
.
|
|
484
|
+
.tfm-header-label {
|
|
384
485
|
font-size: var(--ui-font-size-xs);
|
|
385
486
|
color: var(--ui-text-secondary);
|
|
386
487
|
text-transform: uppercase;
|
|
387
488
|
letter-spacing: 0.05em;
|
|
388
489
|
}
|
|
389
490
|
|
|
390
|
-
.
|
|
391
|
-
|
|
392
|
-
|
|
491
|
+
.tfm-info-btn {
|
|
492
|
+
display: inline-flex;
|
|
493
|
+
align-items: center;
|
|
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'] {
|
|
393
509
|
color: var(--ui-text-primary);
|
|
394
510
|
}
|
|
395
511
|
|
|
396
|
-
|
|
512
|
+
/* Two-card pipeline (Editor → Production) — mirrors PresetFileManager so
|
|
513
|
+
theme and preset surfaces share one visual idiom. */
|
|
514
|
+
.tfm-cards {
|
|
397
515
|
display: flex;
|
|
398
516
|
flex-direction: column;
|
|
399
|
-
gap: var(--ui-space-
|
|
517
|
+
gap: var(--ui-space-6);
|
|
400
518
|
}
|
|
401
519
|
|
|
402
|
-
.tfm-
|
|
520
|
+
.tfm-card {
|
|
521
|
+
position: relative;
|
|
403
522
|
display: flex;
|
|
404
|
-
|
|
405
|
-
gap: var(--ui-space-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
background: var(--ui-surface-low);
|
|
523
|
+
flex-direction: column;
|
|
524
|
+
gap: var(--ui-space-6);
|
|
525
|
+
padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-10) var(--ui-space-16);
|
|
526
|
+
background: var(--ui-surface-lower);
|
|
409
527
|
border: 1px solid var(--ui-border-subtle);
|
|
410
528
|
border-radius: var(--ui-radius-md);
|
|
411
|
-
color: var(--ui-text-secondary);
|
|
412
|
-
font-size: var(--ui-font-size-md);
|
|
413
|
-
cursor: pointer;
|
|
414
|
-
transition: all var(--ui-transition-fast);
|
|
415
|
-
white-space: nowrap;
|
|
416
529
|
}
|
|
417
530
|
|
|
418
|
-
.tfm-
|
|
419
|
-
|
|
420
|
-
|
|
531
|
+
.tfm-rail {
|
|
532
|
+
position: absolute;
|
|
533
|
+
left: 0;
|
|
534
|
+
top: 0;
|
|
535
|
+
bottom: 0;
|
|
536
|
+
width: 3px;
|
|
537
|
+
border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
|
|
538
|
+
background: var(--tfm-rail-neutral);
|
|
539
|
+
transition: background var(--ui-transition-base);
|
|
421
540
|
}
|
|
422
541
|
|
|
423
|
-
.tfm-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
border-color: var(--ui-border-default);
|
|
427
|
-
}
|
|
542
|
+
.tfm-card-editor.dirty .tfm-rail { background: var(--tfm-rail-dirty); }
|
|
543
|
+
.tfm-card-editor.applied .tfm-rail { background: var(--tfm-rail-applied); }
|
|
544
|
+
.tfm-card-production.in-sync .tfm-rail { background: var(--tfm-rail-applied); }
|
|
428
545
|
|
|
429
|
-
.tfm-
|
|
430
|
-
|
|
431
|
-
|
|
546
|
+
.tfm-card-head {
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: baseline;
|
|
549
|
+
justify-content: space-between;
|
|
550
|
+
gap: var(--ui-space-8);
|
|
432
551
|
}
|
|
433
552
|
|
|
434
|
-
.tfm-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
553
|
+
.tfm-card-label {
|
|
554
|
+
font-size: var(--ui-font-size-xs);
|
|
555
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
556
|
+
text-transform: uppercase;
|
|
557
|
+
letter-spacing: 0.08em;
|
|
558
|
+
color: var(--ui-text-secondary);
|
|
559
|
+
line-height: 1.1;
|
|
438
560
|
}
|
|
439
561
|
|
|
440
|
-
.
|
|
441
|
-
display: flex;
|
|
562
|
+
.tfm-card-status {
|
|
563
|
+
display: inline-flex;
|
|
564
|
+
align-items: center;
|
|
442
565
|
gap: var(--ui-space-4);
|
|
566
|
+
font-size: 0.7rem;
|
|
567
|
+
letter-spacing: 0.02em;
|
|
568
|
+
color: var(--ui-text-muted);
|
|
569
|
+
line-height: 1;
|
|
443
570
|
}
|
|
444
571
|
|
|
445
|
-
.
|
|
446
|
-
|
|
447
|
-
|
|
572
|
+
.tfm-status-dot {
|
|
573
|
+
width: 5px;
|
|
574
|
+
height: 5px;
|
|
575
|
+
border-radius: 50%;
|
|
576
|
+
background: currentColor;
|
|
577
|
+
opacity: 0.7;
|
|
578
|
+
flex-shrink: 0;
|
|
448
579
|
}
|
|
449
580
|
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
581
|
+
.tfm-card-status.dirty {
|
|
582
|
+
color: var(--ui-highlight);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.tfm-card-status.dirty .tfm-status-dot {
|
|
586
|
+
opacity: 1;
|
|
587
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent);
|
|
588
|
+
animation: tfm-pulse 1.6s ease-in-out infinite;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.tfm-card-status.applied {
|
|
592
|
+
color: var(--tfm-applied);
|
|
593
|
+
}
|
|
594
|
+
.tfm-card-status.applied .tfm-status-dot {
|
|
595
|
+
opacity: 1;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.tfm-pill {
|
|
454
599
|
display: flex;
|
|
455
600
|
align-items: center;
|
|
456
|
-
|
|
601
|
+
padding: var(--ui-space-6) var(--ui-space-10);
|
|
602
|
+
background: var(--ui-surface-lowest);
|
|
603
|
+
border: 1px solid var(--ui-border-subtle);
|
|
604
|
+
border-radius: var(--ui-radius-md);
|
|
605
|
+
transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
|
|
457
606
|
}
|
|
458
607
|
|
|
459
|
-
.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
color: var(--ui-text-primary);
|
|
608
|
+
.tfm-pill.dirty {
|
|
609
|
+
border-color: color-mix(in srgb, var(--ui-highlight) 60%, var(--ui-border-subtle));
|
|
610
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-highlight) 35%, transparent);
|
|
463
611
|
}
|
|
464
612
|
|
|
465
|
-
.
|
|
466
|
-
|
|
467
|
-
border-color: var(--ui-border-strong);
|
|
613
|
+
.tfm-pill.applied {
|
|
614
|
+
border-color: color-mix(in srgb, var(--tfm-applied) 50%, var(--ui-border-subtle));
|
|
468
615
|
}
|
|
469
616
|
|
|
470
|
-
.
|
|
471
|
-
|
|
472
|
-
|
|
617
|
+
.tfm-pill-name {
|
|
618
|
+
flex: 1;
|
|
619
|
+
min-width: 0;
|
|
620
|
+
font-size: var(--ui-font-size-md);
|
|
621
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
622
|
+
color: var(--ui-text-primary);
|
|
623
|
+
white-space: nowrap;
|
|
624
|
+
overflow: hidden;
|
|
625
|
+
text-overflow: ellipsis;
|
|
626
|
+
}
|
|
473
627
|
|
|
474
|
-
.
|
|
628
|
+
.tfm-card-actions {
|
|
475
629
|
display: flex;
|
|
476
|
-
flex-direction: column;
|
|
477
630
|
gap: var(--ui-space-4);
|
|
631
|
+
flex-wrap: wrap;
|
|
478
632
|
}
|
|
479
633
|
|
|
480
|
-
.
|
|
481
|
-
|
|
482
|
-
gap: var(--ui-space-4);
|
|
483
|
-
justify-content: flex-end;
|
|
634
|
+
.tfm-card-actions-stack {
|
|
635
|
+
flex-direction: column;
|
|
484
636
|
}
|
|
485
637
|
|
|
486
|
-
.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
638
|
+
.tfm-btn-row {
|
|
639
|
+
width: 100%;
|
|
640
|
+
justify-content: flex-start;
|
|
641
|
+
gap: var(--ui-space-8);
|
|
642
|
+
flex: 0 0 auto;
|
|
643
|
+
text-align: left;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.tfm-btn-row i {
|
|
647
|
+
width: 1rem;
|
|
648
|
+
text-align: center;
|
|
649
|
+
flex: 0 0 auto;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.tfm-btn-row span {
|
|
653
|
+
flex: 1 1 auto;
|
|
654
|
+
text-align: left;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* Bridge button — sits between Editor and Production cards as the arrow that
|
|
658
|
+
promotes the editor theme into production. */
|
|
659
|
+
.tfm-adopt-btn {
|
|
660
|
+
align-self: stretch;
|
|
661
|
+
width: 100%;
|
|
662
|
+
display: flex;
|
|
663
|
+
align-items: center;
|
|
664
|
+
justify-content: center;
|
|
665
|
+
gap: var(--ui-space-6);
|
|
666
|
+
margin: calc(var(--ui-space-2) * -1) 0;
|
|
667
|
+
padding: var(--ui-space-6) var(--ui-space-12);
|
|
668
|
+
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-medium));
|
|
492
670
|
border-radius: var(--ui-radius-md);
|
|
493
671
|
color: var(--ui-text-primary);
|
|
494
672
|
font-size: var(--ui-font-size-md);
|
|
495
|
-
|
|
673
|
+
font-weight: var(--ui-font-weight-medium);
|
|
674
|
+
cursor: pointer;
|
|
675
|
+
transition: all var(--ui-transition-fast);
|
|
676
|
+
white-space: nowrap;
|
|
677
|
+
position: relative;
|
|
678
|
+
z-index: 1;
|
|
496
679
|
}
|
|
497
680
|
|
|
498
|
-
.
|
|
499
|
-
|
|
681
|
+
.tfm-adopt-btn i {
|
|
682
|
+
width: 1rem;
|
|
683
|
+
text-align: center;
|
|
684
|
+
font-size: 0.85em;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.tfm-adopt-btn:hover:not(:disabled) {
|
|
688
|
+
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-strong));
|
|
500
690
|
}
|
|
501
691
|
|
|
502
|
-
.
|
|
692
|
+
.tfm-adopt-btn:disabled {
|
|
693
|
+
cursor: not-allowed;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.tfm-adopt-btn.in-sync {
|
|
697
|
+
background: transparent;
|
|
698
|
+
border-color: var(--ui-border-subtle);
|
|
503
699
|
color: var(--ui-text-muted);
|
|
700
|
+
opacity: 0.7;
|
|
504
701
|
}
|
|
505
702
|
|
|
506
|
-
.
|
|
507
|
-
|
|
508
|
-
|
|
703
|
+
.tfm-adopt-btn.saving i { animation: spin 1s linear infinite; }
|
|
704
|
+
.tfm-adopt-btn.saved {
|
|
705
|
+
background: color-mix(in srgb, var(--tfm-applied) 30%, var(--ui-surface-high));
|
|
706
|
+
color: var(--tfm-applied);
|
|
707
|
+
}
|
|
708
|
+
.tfm-adopt-btn.error { color: var(--ui-text-muted); }
|
|
709
|
+
|
|
710
|
+
.tfm-btn {
|
|
711
|
+
display: inline-flex;
|
|
509
712
|
align-items: center;
|
|
510
713
|
justify-content: center;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
background: var(--ui-surface-low);
|
|
714
|
+
gap: var(--ui-space-4);
|
|
715
|
+
padding: var(--ui-space-6) var(--ui-space-10);
|
|
716
|
+
background: var(--ui-surface);
|
|
515
717
|
border: 1px solid var(--ui-border-subtle);
|
|
516
718
|
border-radius: var(--ui-radius-md);
|
|
517
719
|
color: var(--ui-text-secondary);
|
|
518
|
-
font-size: var(--ui-font-size-
|
|
720
|
+
font-size: var(--ui-font-size-md);
|
|
721
|
+
font-weight: var(--ui-font-weight-medium);
|
|
519
722
|
cursor: pointer;
|
|
520
723
|
transition: all var(--ui-transition-fast);
|
|
724
|
+
white-space: nowrap;
|
|
725
|
+
flex: 1 1 0;
|
|
726
|
+
min-width: 0;
|
|
521
727
|
}
|
|
522
728
|
|
|
523
|
-
.
|
|
524
|
-
|
|
729
|
+
.tfm-btn i {
|
|
730
|
+
width: 1rem;
|
|
731
|
+
text-align: center;
|
|
732
|
+
font-size: 0.85em;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.tfm-btn:hover:not(:disabled) {
|
|
736
|
+
background: var(--ui-surface-high);
|
|
525
737
|
color: var(--ui-text-primary);
|
|
526
738
|
border-color: var(--ui-border-default);
|
|
527
739
|
}
|
|
528
740
|
|
|
529
|
-
.
|
|
530
|
-
opacity: 0.
|
|
741
|
+
.tfm-btn:disabled {
|
|
742
|
+
opacity: 0.45;
|
|
531
743
|
cursor: not-allowed;
|
|
532
744
|
}
|
|
533
745
|
|
|
534
|
-
.
|
|
746
|
+
.tfm-btn.active {
|
|
747
|
+
background: var(--ui-surface);
|
|
748
|
+
border-color: var(--ui-border-default);
|
|
749
|
+
color: var(--ui-text-primary);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.save-btn {
|
|
535
753
|
background: var(--ui-surface-high);
|
|
536
754
|
border-color: var(--ui-border-medium);
|
|
537
755
|
color: var(--ui-text-primary);
|
|
538
756
|
}
|
|
539
757
|
|
|
758
|
+
.save-btn:hover:not(:disabled) {
|
|
759
|
+
background: var(--ui-surface-higher);
|
|
760
|
+
border-color: var(--ui-border-strong);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.save-btn.saving i { animation: spin 1s linear infinite; }
|
|
764
|
+
.save-btn.saved {
|
|
765
|
+
background: var(--ui-surface-highest);
|
|
766
|
+
color: var(--ui-text-success);
|
|
767
|
+
}
|
|
768
|
+
.save-btn.error {
|
|
769
|
+
background: var(--ui-surface-high);
|
|
770
|
+
color: var(--ui-text-muted);
|
|
771
|
+
}
|
|
772
|
+
|
|
540
773
|
.load-list {
|
|
541
774
|
display: flex;
|
|
542
775
|
flex-direction: column;
|
|
@@ -597,7 +830,7 @@
|
|
|
597
830
|
|
|
598
831
|
.header-spacer {
|
|
599
832
|
flex-shrink: 0;
|
|
600
|
-
width: 24px;
|
|
833
|
+
width: 24px;
|
|
601
834
|
}
|
|
602
835
|
|
|
603
836
|
.load-item {
|
|
@@ -685,82 +918,99 @@
|
|
|
685
918
|
color: #ccc;
|
|
686
919
|
}
|
|
687
920
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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;
|
|
700
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;
|
|
701
942
|
}
|
|
702
943
|
|
|
703
|
-
.
|
|
704
|
-
|
|
944
|
+
.tfm-info-popover.ready {
|
|
945
|
+
visibility: visible;
|
|
705
946
|
}
|
|
706
947
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
.production-section {
|
|
948
|
+
.tfm-info-header {
|
|
710
949
|
display: flex;
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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);
|
|
715
955
|
}
|
|
716
956
|
|
|
717
|
-
.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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;
|
|
722
963
|
}
|
|
723
964
|
|
|
724
|
-
.
|
|
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);
|
|
725
976
|
font-size: var(--ui-font-size-xs);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
977
|
+
line-height: 1;
|
|
978
|
+
cursor: pointer;
|
|
979
|
+
transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
|
|
729
980
|
}
|
|
730
981
|
|
|
731
|
-
.
|
|
732
|
-
font-size: var(--ui-font-size-md);
|
|
733
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
982
|
+
.tfm-info-close:hover {
|
|
734
983
|
color: var(--ui-text-primary);
|
|
984
|
+
background: var(--ui-hover);
|
|
735
985
|
}
|
|
736
986
|
|
|
737
|
-
.
|
|
738
|
-
|
|
739
|
-
border-color: var(--ui-border-medium);
|
|
740
|
-
color: var(--ui-text-primary);
|
|
987
|
+
.tfm-info-body {
|
|
988
|
+
padding: var(--ui-space-16);
|
|
741
989
|
}
|
|
742
990
|
|
|
743
|
-
.
|
|
744
|
-
|
|
745
|
-
|
|
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;
|
|
746
995
|
}
|
|
747
996
|
|
|
748
|
-
.
|
|
749
|
-
|
|
750
|
-
|
|
997
|
+
.tfm-info-popover p:last-child {
|
|
998
|
+
margin-bottom: 0;
|
|
999
|
+
}
|
|
751
1000
|
|
|
752
|
-
.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
padding: 0 var(--ui-space-4);
|
|
756
|
-
letter-spacing: 0.02em;
|
|
1001
|
+
.tfm-info-popover strong {
|
|
1002
|
+
color: var(--ui-text-primary);
|
|
1003
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
757
1004
|
}
|
|
758
1005
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1006
|
+
@keyframes tfm-info-in {
|
|
1007
|
+
from { opacity: 0; transform: translateY(-3px); }
|
|
1008
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
@keyframes tfm-pulse {
|
|
1012
|
+
0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent); }
|
|
1013
|
+
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
|
|
764
1014
|
}
|
|
765
1015
|
|
|
766
1016
|
@keyframes spin {
|