@motion-proto/live-tokens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +41 -0
  2. package/dist-plugin/index.cjs +444 -0
  3. package/dist-plugin/index.d.cts +12 -0
  4. package/dist-plugin/index.d.ts +12 -0
  5. package/dist-plugin/index.js +407 -0
  6. package/package.json +86 -0
  7. package/src/components/Badge.svelte +82 -0
  8. package/src/components/Button.svelte +333 -0
  9. package/src/components/Card.svelte +83 -0
  10. package/src/components/CollapsibleSection.svelte +82 -0
  11. package/src/components/DetailNav.svelte +78 -0
  12. package/src/components/Dialog.svelte +269 -0
  13. package/src/components/InlineEditActions.svelte +73 -0
  14. package/src/components/Notification.svelte +308 -0
  15. package/src/components/ProgressBar.svelte +99 -0
  16. package/src/components/RadioButton.svelte +87 -0
  17. package/src/components/SectionDivider.svelte +121 -0
  18. package/src/components/TabBar.svelte +92 -0
  19. package/src/components/Toggle.svelte +86 -0
  20. package/src/components/Tooltip.svelte +64 -0
  21. package/src/lib/ColumnsOverlay.svelte +120 -0
  22. package/src/lib/LiveEditorOverlay.svelte +467 -0
  23. package/src/lib/columnsOverlay.ts +26 -0
  24. package/src/lib/cssVarSync.ts +72 -0
  25. package/src/lib/editorConfig.ts +9 -0
  26. package/src/lib/editorConfigStore.ts +14 -0
  27. package/src/lib/index.ts +51 -0
  28. package/src/lib/oklch.ts +129 -0
  29. package/src/lib/pageSource.ts +6 -0
  30. package/src/lib/tokenInit.ts +29 -0
  31. package/src/lib/tokenService.ts +144 -0
  32. package/src/lib/tokenTypes.ts +45 -0
  33. package/src/pages/Admin.svelte +100 -0
  34. package/src/pages/ShowcasePage.svelte +146 -0
  35. package/src/showcase/BackupBrowser.svelte +617 -0
  36. package/src/showcase/BezierCurveEditor.svelte +648 -0
  37. package/src/showcase/ColorEditPanel.svelte +498 -0
  38. package/src/showcase/ComponentsTab.svelte +107 -0
  39. package/src/showcase/EditorDialog.svelte +137 -0
  40. package/src/showcase/PaletteEditor.svelte +2579 -0
  41. package/src/showcase/PaletteSelector.svelte +627 -0
  42. package/src/showcase/SurfacesTab.svelte +409 -0
  43. package/src/showcase/TextTab.svelte +205 -0
  44. package/src/showcase/TokenFileManager.svelte +683 -0
  45. package/src/showcase/TokenMap.svelte +54 -0
  46. package/src/showcase/VariablesTab.svelte +2657 -0
  47. package/src/showcase/VisualsTab.svelte +233 -0
  48. package/src/showcase/curveEngine.ts +190 -0
  49. package/src/showcase/demos/BadgeDemo.svelte +58 -0
  50. package/src/showcase/demos/CardDemo.svelte +52 -0
  51. package/src/showcase/demos/ChoiceButtonsDemo.svelte +194 -0
  52. package/src/showcase/demos/CollapsibleSectionDemo.svelte +56 -0
  53. package/src/showcase/demos/DialogDemo.svelte +42 -0
  54. package/src/showcase/demos/InlineEditActionsDemo.svelte +27 -0
  55. package/src/showcase/demos/NotificationDemo.svelte +149 -0
  56. package/src/showcase/demos/ProgressBarDemo.svelte +56 -0
  57. package/src/showcase/demos/RadioButtonDemo.svelte +58 -0
  58. package/src/showcase/demos/SectionDividerDemo.svelte +79 -0
  59. package/src/showcase/demos/StandardButtonsDemo.svelte +457 -0
  60. package/src/showcase/demos/TabBarDemo.svelte +60 -0
  61. package/src/showcase/demos/TooltipDemo.svelte +54 -0
  62. package/src/showcase/editor.css +93 -0
  63. package/src/showcase/index.ts +17 -0
  64. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  65. package/src/styles/fonts/Domine/OFL.txt +97 -0
  66. package/src/styles/fonts/Domine/README.txt +66 -0
  67. package/src/styles/fonts.css +18 -0
  68. package/src/styles/form-controls.css +190 -0
@@ -0,0 +1,683 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onMount } from 'svelte';
3
+ import type { TokenFileMeta } from '../lib/tokenTypes';
4
+ import { listTokenFiles, deleteTokenFile, setActiveFile, sanitizeFileName, getProductionInfo, setProductionFile } from '../lib/tokenService';
5
+ import type { ProductionInfo } from '../lib/tokenService';
6
+ import { activeFileName, editorConfigs, configsLoadedFromFile } from '../lib/editorConfigStore';
7
+ import BackupBrowser from './BackupBrowser.svelte';
8
+ import EditorDialog from './EditorDialog.svelte';
9
+
10
+ const dispatch = createEventDispatcher<{
11
+ save: { fileName: string; displayName: string };
12
+ load: { fileName: string };
13
+ }>();
14
+
15
+ export let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = 'idle';
16
+
17
+ let files: TokenFileMeta[] = [];
18
+ let showFileList = false;
19
+ let saveAsEditing = false;
20
+ let saveAsName = '';
21
+ let saveAsInput: HTMLInputElement;
22
+ let currentDisplayName = 'Default';
23
+ let showBackups = false;
24
+
25
+ // --- Production state ---
26
+ let productionInfo: ProductionInfo | null = null;
27
+ let productionUpdateStatus: 'idle' | 'updating' | 'done' | 'error' = 'idle';
28
+
29
+ // --- Dirty state tracking ---
30
+ let savedConfigHash = '';
31
+ let initialized = false;
32
+
33
+ $: configHash = JSON.stringify($editorConfigs);
34
+ $: unsaved = initialized && configHash !== savedConfigHash;
35
+
36
+ async function refreshFiles() {
37
+ try {
38
+ files = await listTokenFiles();
39
+ const active = files.find(f => f.isActive);
40
+ if (active) {
41
+ $activeFileName = active.fileName;
42
+ currentDisplayName = active.name;
43
+ }
44
+ } catch {
45
+ // silent — will show empty list
46
+ }
47
+ }
48
+
49
+ async function refreshProduction() {
50
+ try {
51
+ productionInfo = await getProductionInfo();
52
+ } catch {
53
+ // silent
54
+ }
55
+ }
56
+
57
+ async function handleUpdateProduction() {
58
+ productionUpdateStatus = 'updating';
59
+ try {
60
+ await setProductionFile($activeFileName);
61
+ await refreshProduction();
62
+ productionUpdateStatus = 'done';
63
+ setTimeout(() => { productionUpdateStatus = 'idle'; }, 2000);
64
+ } catch {
65
+ productionUpdateStatus = 'error';
66
+ setTimeout(() => { productionUpdateStatus = 'idle'; }, 2000);
67
+ }
68
+ }
69
+
70
+ onMount(async () => {
71
+ await refreshFiles();
72
+ await refreshProduction();
73
+ // Delay snapshot until editors have initialized
74
+ setTimeout(() => {
75
+ savedConfigHash = JSON.stringify($editorConfigs);
76
+ initialized = true;
77
+ }, 500);
78
+ });
79
+
80
+ function handleSave() {
81
+ dispatch('save', { fileName: $activeFileName, displayName: currentDisplayName });
82
+ setTimeout(() => { savedConfigHash = JSON.stringify($editorConfigs); }, 300);
83
+ }
84
+
85
+ function handleSaveIncrement() {
86
+ // Strip any existing _NN suffix, then find the next available number
87
+ const baseName = currentDisplayName.replace(/_\d+$/, '');
88
+ const baseFileName = sanitizeFileName(baseName);
89
+ const existingNums = files
90
+ .filter(f => f.fileName === baseFileName || f.fileName.match(new RegExp(`^${baseFileName}_\\d+$`)))
91
+ .map(f => {
92
+ const m = f.fileName.match(/_(\d+)$/);
93
+ return m ? parseInt(m[1], 10) : 0;
94
+ });
95
+ const next = (existingNums.length > 0 ? Math.max(...existingNums) : 0) + 1;
96
+ const suffix = String(next).padStart(2, '0');
97
+ const displayName = `${baseName}_${suffix}`;
98
+ const fileName = `${baseFileName}_${suffix}`;
99
+
100
+ dispatch('save', { fileName, displayName });
101
+ $activeFileName = fileName;
102
+ currentDisplayName = displayName;
103
+ setTimeout(() => {
104
+ refreshFiles();
105
+ savedConfigHash = JSON.stringify($editorConfigs);
106
+ }, 500);
107
+ }
108
+
109
+ function openSaveAs() {
110
+ saveAsName = currentDisplayName;
111
+ saveAsEditing = true;
112
+ showFileList = false;
113
+ // Focus the input after Svelte renders it
114
+ setTimeout(() => saveAsInput?.select(), 0);
115
+ }
116
+
117
+ function confirmSaveAs() {
118
+ const displayName = saveAsName.trim();
119
+ if (!displayName) return;
120
+ const fileName = sanitizeFileName(displayName);
121
+ if (fileName === 'default') {
122
+ saveAsName = '';
123
+ return;
124
+ }
125
+ saveAsEditing = false;
126
+ dispatch('save', { fileName, displayName });
127
+ $activeFileName = fileName;
128
+ currentDisplayName = displayName;
129
+ setTimeout(() => {
130
+ refreshFiles();
131
+ savedConfigHash = JSON.stringify($editorConfigs);
132
+ }, 500);
133
+ }
134
+
135
+ function cancelSaveAs() {
136
+ saveAsEditing = false;
137
+ saveAsName = '';
138
+ }
139
+
140
+ async function handleLoad(file: TokenFileMeta) {
141
+ showFileList = false;
142
+ await setActiveFile(file.fileName);
143
+ $activeFileName = file.fileName;
144
+ currentDisplayName = file.name;
145
+ dispatch('load', { fileName: file.fileName });
146
+ // Snapshot after load completes
147
+ setTimeout(() => {
148
+ savedConfigHash = JSON.stringify($editorConfigs);
149
+ }, 500);
150
+ }
151
+
152
+ async function handleDelete(file: TokenFileMeta) {
153
+ if (file.fileName === 'default') return;
154
+ try {
155
+ await deleteTokenFile(file.fileName);
156
+ await refreshFiles();
157
+ // If we deleted the active file, it reverts to default on the server
158
+ if (file.fileName === $activeFileName) {
159
+ $activeFileName = 'default';
160
+ currentDisplayName = 'Default';
161
+ dispatch('load', { fileName: 'default' });
162
+ }
163
+ } catch {
164
+ // silent
165
+ }
166
+ }
167
+
168
+ function handleSaveAsKeydown(e: KeyboardEvent) {
169
+ if (e.key === 'Enter') confirmSaveAs();
170
+ if (e.key === 'Escape') cancelSaveAs();
171
+ }
172
+
173
+ function toggleFileList() {
174
+ showFileList = !showFileList;
175
+ saveAsEditing = false;
176
+ if (showFileList) refreshFiles();
177
+ }
178
+
179
+ </script>
180
+
181
+ <div class="token-file-manager">
182
+ <div class="active-file">
183
+ <span class="active-label">Theme</span>
184
+ <span class="active-name">{currentDisplayName}</span>
185
+ </div>
186
+
187
+
188
+ <div class="button-grid">
189
+ <div class="save-row">
190
+ <button
191
+ class="tfm-btn save-btn"
192
+ class:saving={saveStatus === 'saving'}
193
+ class:saved={saveStatus === 'saved'}
194
+ class:error={saveStatus === 'error'}
195
+ on:click={handleSave}
196
+ disabled={saveStatus === 'saving' || !$configsLoadedFromFile}
197
+ title={$configsLoadedFromFile ? "Save to current file" : "Load a token file first"}
198
+ >
199
+ <i class="fas" class:fa-save={saveStatus === 'idle'} class:fa-spinner={saveStatus === 'saving'} class:fa-check={saveStatus === 'saved'} class:fa-times={saveStatus === 'error'}></i>
200
+ <span>
201
+ {#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
202
+ </span>
203
+ </button>
204
+ <button
205
+ class="tfm-btn increment-btn"
206
+ on:click={handleSaveIncrement}
207
+ disabled={saveStatus === 'saving' || !$configsLoadedFromFile}
208
+ title="Save as incremented copy"
209
+ >
210
+ <i class="fas fa-plus"></i>
211
+ </button>
212
+ </div>
213
+
214
+ {#if saveAsEditing}
215
+ <div class="save-as-inline">
216
+ <input
217
+ class="save-as-input"
218
+ type="text"
219
+ bind:value={saveAsName}
220
+ bind:this={saveAsInput}
221
+ on:keydown={handleSaveAsKeydown}
222
+ placeholder="Theme name..."
223
+ />
224
+ <div class="save-as-actions">
225
+ <button class="inline-btn confirm-btn" on:click={confirmSaveAs} disabled={!saveAsName.trim()} title="Save">
226
+ <i class="fas fa-check"></i>
227
+ </button>
228
+ <button class="inline-btn cancel-btn" on:click={cancelSaveAs} title="Cancel">
229
+ <i class="fas fa-times"></i>
230
+ </button>
231
+ </div>
232
+ </div>
233
+ {:else}
234
+ <button
235
+ class="tfm-btn"
236
+ on:click={openSaveAs}
237
+ title="Save as new file"
238
+ >
239
+ <i class="fas fa-copy"></i>
240
+ <span>Save As</span>
241
+ </button>
242
+ {/if}
243
+
244
+ <button
245
+ class="tfm-btn"
246
+ class:active={showFileList}
247
+ on:click={toggleFileList}
248
+ title="Load a token file"
249
+ >
250
+ <i class="fas fa-folder-open"></i>
251
+ <span>Load</span>
252
+ </button>
253
+
254
+ <button
255
+ class="tfm-btn history-btn"
256
+ on:click={() => showBackups = true}
257
+ title="View backup history and restore"
258
+ >
259
+ <i class="fas fa-history"></i>
260
+ <span>History</span>
261
+ </button>
262
+
263
+ </div>
264
+
265
+ <div class="status-labels">
266
+ <span class="status-label" class:dirty={unsaved} class:clean={!unsaved}>
267
+ {unsaved ? 'Unsaved changes' : 'Saved'}
268
+ </span>
269
+ </div>
270
+
271
+ <div class="production-section">
272
+ <div class="production-header">
273
+ <span class="production-label">Production</span>
274
+ {#if productionInfo}
275
+ <span class="production-name">{productionInfo.name}</span>
276
+ {/if}
277
+ </div>
278
+
279
+ <button
280
+ class="tfm-btn production-update-btn"
281
+ class:updating={productionUpdateStatus === 'updating'}
282
+ class:done={productionUpdateStatus === 'done'}
283
+ class:error={productionUpdateStatus === 'error'}
284
+ on:click={handleUpdateProduction}
285
+ disabled={productionUpdateStatus === 'updating' || !$configsLoadedFromFile || (productionInfo?.fileName === $activeFileName)}
286
+ title={productionInfo?.fileName === $activeFileName ? 'Already in production' : `Set "${currentDisplayName}" as production`}
287
+ >
288
+ <i class="fas" class:fa-sync-alt={productionUpdateStatus === 'idle'} class:fa-spinner={productionUpdateStatus === 'updating'} class:fa-check={productionUpdateStatus === 'done'} class:fa-times={productionUpdateStatus === 'error'}></i>
289
+ <span>
290
+ {#if productionUpdateStatus === 'idle'}Sync Theme{:else if productionUpdateStatus === 'updating'}Syncing{:else if productionUpdateStatus === 'done'}Synced{:else}Error{/if}
291
+ </span>
292
+ </button>
293
+
294
+ {#if productionInfo?.fileName === $activeFileName}
295
+ <span class="production-match">Active theme matches production</span>
296
+ {:else}
297
+ <span class="production-diff">Active theme differs from production</span>
298
+ {/if}
299
+ </div>
300
+
301
+ </div>
302
+
303
+ <EditorDialog
304
+ bind:show={showFileList}
305
+ title="Load Theme"
306
+ cancelLabel="Close"
307
+ width="420px"
308
+ >
309
+ <div class="load-list">
310
+ {#each files as file}
311
+ <div class="load-item" class:active={file.fileName === $activeFileName}>
312
+ <button class="load-name-btn" on:click={() => handleLoad(file)}>
313
+ {file.name}
314
+ </button>
315
+ {#if file.fileName === $activeFileName}
316
+ <span class="active-badge">active</span>
317
+ {/if}
318
+ {#if file.fileName !== 'default'}
319
+ <button
320
+ class="file-delete-btn"
321
+ on:click|stopPropagation={() => handleDelete(file)}
322
+ title="Delete {file.name}"
323
+ >
324
+ <i class="fas fa-trash-alt"></i>
325
+ </button>
326
+ {/if}
327
+ </div>
328
+ {/each}
329
+ {#if files.length === 0}
330
+ <div class="load-item empty">No saved files</div>
331
+ {/if}
332
+ </div>
333
+ </EditorDialog>
334
+
335
+ <BackupBrowser
336
+ bind:open={showBackups}
337
+ on:restored={(e) => {
338
+ if (e.detail.type === 'tokens') {
339
+ dispatch('load', { fileName: $activeFileName });
340
+ }
341
+ refreshFiles();
342
+ }}
343
+ />
344
+
345
+
346
+ <style>
347
+ .token-file-manager {
348
+ display: flex;
349
+ flex-direction: column;
350
+ gap: var(--space-8);
351
+ }
352
+
353
+ .active-file {
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: var(--space-2);
357
+ padding: 0 var(--space-4);
358
+ }
359
+
360
+ .active-label {
361
+ font-size: var(--font-xs);
362
+ color: var(--ui-text-muted);
363
+ text-transform: uppercase;
364
+ letter-spacing: 0.05em;
365
+ }
366
+
367
+ .active-name {
368
+ font-size: var(--font-md);
369
+ font-weight: var(--font-weight-semibold);
370
+ color: var(--ui-text-primary);
371
+ }
372
+
373
+ .button-grid {
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: var(--space-4);
377
+ }
378
+
379
+ .tfm-btn {
380
+ display: flex;
381
+ align-items: center;
382
+ gap: var(--space-4);
383
+ width: 100%;
384
+ padding: var(--space-6) var(--space-8);
385
+ background: var(--ui-surface-low);
386
+ border: 1px solid var(--ui-border-subtle);
387
+ border-radius: var(--radius-md);
388
+ color: var(--ui-text-secondary);
389
+ font-size: var(--font-md);
390
+ cursor: pointer;
391
+ transition: all var(--transition-fast);
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .tfm-btn i {
396
+ width: 1rem;
397
+ text-align: center;
398
+ }
399
+
400
+ .tfm-btn:hover:not(:disabled) {
401
+ background: var(--ui-surface);
402
+ color: var(--ui-text-primary);
403
+ border-color: var(--border);
404
+ }
405
+
406
+ .tfm-btn:disabled {
407
+ opacity: 0.5;
408
+ cursor: not-allowed;
409
+ }
410
+
411
+ .tfm-btn.active {
412
+ background: var(--ui-surface);
413
+ border-color: var(--ui-border-default);
414
+ color: var(--ui-text-primary);
415
+ }
416
+
417
+ .save-row {
418
+ display: flex;
419
+ gap: var(--space-4);
420
+ }
421
+
422
+ .save-row .save-btn {
423
+ flex: 1;
424
+ min-width: 0;
425
+ }
426
+
427
+ .increment-btn {
428
+ flex: 0 0 auto;
429
+ width: 34px;
430
+ padding: 0;
431
+ display: flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ }
435
+
436
+ .save-btn {
437
+ background: var(--ui-surface-high);
438
+ border-color: var(--ui-border-medium);
439
+ color: var(--ui-text-primary);
440
+ }
441
+
442
+ .save-btn:hover:not(:disabled) {
443
+ background: var(--ui-surface-higher);
444
+ border-color: var(--ui-border-strong);
445
+ }
446
+
447
+ .save-btn.saving i { animation: spin 1s linear infinite; }
448
+ .save-btn.saved { background: var(--ui-surface-highest); color: var(--ui-text-success); }
449
+ .save-btn.error { background: var(--ui-surface-high); color: var(--ui-text-muted); }
450
+
451
+ .save-as-inline {
452
+ display: flex;
453
+ flex-direction: column;
454
+ gap: var(--space-4);
455
+ }
456
+
457
+ .save-as-actions {
458
+ display: flex;
459
+ gap: var(--space-4);
460
+ justify-content: flex-end;
461
+ }
462
+
463
+ .save-as-input {
464
+ flex: 1;
465
+ min-width: 0;
466
+ padding: var(--space-6) var(--space-8);
467
+ background: var(--ui-surface-lowest);
468
+ border: 1px solid var(--ui-border-subtle);
469
+ border-radius: var(--radius-md);
470
+ color: var(--ui-text-primary);
471
+ font-size: var(--font-md);
472
+ outline: none;
473
+ }
474
+
475
+ .save-as-input:focus {
476
+ border-color: var(--ui-border-medium);
477
+ }
478
+
479
+ .save-as-input::placeholder {
480
+ color: var(--ui-text-muted);
481
+ }
482
+
483
+ .inline-btn {
484
+ flex: 0 0 auto;
485
+ display: flex;
486
+ align-items: center;
487
+ justify-content: center;
488
+ width: 30px;
489
+ height: 30px;
490
+ padding: 0;
491
+ background: var(--ui-surface-low);
492
+ border: 1px solid var(--ui-border-subtle);
493
+ border-radius: var(--radius-md);
494
+ color: var(--ui-text-secondary);
495
+ font-size: var(--font-sm);
496
+ cursor: pointer;
497
+ transition: all var(--transition-fast);
498
+ }
499
+
500
+ .inline-btn:hover:not(:disabled) {
501
+ background: var(--ui-surface);
502
+ color: var(--ui-text-primary);
503
+ border-color: var(--border);
504
+ }
505
+
506
+ .inline-btn:disabled {
507
+ opacity: 0.5;
508
+ cursor: not-allowed;
509
+ }
510
+
511
+ .inline-btn.confirm-btn {
512
+ background: var(--ui-surface-high);
513
+ border-color: var(--ui-border-medium);
514
+ color: var(--ui-text-primary);
515
+ }
516
+
517
+ .load-list {
518
+ display: flex;
519
+ flex-direction: column;
520
+ max-height: 60vh;
521
+ overflow-y: auto;
522
+ }
523
+
524
+ .load-item {
525
+ display: flex;
526
+ align-items: center;
527
+ gap: 6px;
528
+ padding: 4px 6px;
529
+ border-bottom: 1px solid #2a2a2a;
530
+ }
531
+
532
+ .load-item:last-child {
533
+ border-bottom: none;
534
+ }
535
+
536
+ .load-item.empty {
537
+ padding: 16px;
538
+ color: #888;
539
+ font-size: 14px;
540
+ text-align: center;
541
+ }
542
+
543
+ .load-name-btn {
544
+ flex: 1;
545
+ min-width: 0;
546
+ overflow: hidden;
547
+ text-overflow: ellipsis;
548
+ white-space: nowrap;
549
+ padding: 6px 4px;
550
+ background: none;
551
+ border: none;
552
+ color: #aaa;
553
+ font-size: 14px;
554
+ cursor: pointer;
555
+ text-align: left;
556
+ border-radius: 3px;
557
+ }
558
+
559
+ .load-name-btn:hover {
560
+ color: #e0e0e0;
561
+ }
562
+
563
+ .load-item.active .load-name-btn {
564
+ color: #e0e0e0;
565
+ font-weight: 600;
566
+ }
567
+
568
+ .active-badge {
569
+ flex-shrink: 0;
570
+ font-size: 12px;
571
+ padding: 1px 6px;
572
+ border-radius: 3px;
573
+ background: #333;
574
+ color: #ccc;
575
+ }
576
+
577
+ .file-delete-btn {
578
+ flex-shrink: 0;
579
+ display: flex;
580
+ align-items: center;
581
+ justify-content: center;
582
+ width: 24px;
583
+ height: 24px;
584
+ padding: 0;
585
+ background: none;
586
+ border: none;
587
+ color: #555;
588
+ font-size: 12px;
589
+ cursor: pointer;
590
+ opacity: 0;
591
+ }
592
+
593
+ .load-item:hover .file-delete-btn {
594
+ opacity: 1;
595
+ }
596
+
597
+ .file-delete-btn:hover {
598
+ color: #ccc;
599
+ }
600
+
601
+ .status-labels {
602
+ display: flex;
603
+ gap: var(--space-8);
604
+ padding: 0 var(--space-4);
605
+ }
606
+
607
+ .status-label {
608
+ font-size: var(--font-xs);
609
+ letter-spacing: 0.02em;
610
+ }
611
+
612
+ .status-label.clean {
613
+ color: var(--ui-text-muted);
614
+ }
615
+
616
+ .status-label.dirty {
617
+ color: var(--ui-text-warning, #e6a030);
618
+ }
619
+
620
+ /* ── Production section ── */
621
+
622
+ .production-section {
623
+ display: flex;
624
+ flex-direction: column;
625
+ gap: var(--space-4);
626
+ padding-top: var(--space-8);
627
+ border-top: 1px solid var(--ui-border-subtle);
628
+ }
629
+
630
+ .production-header {
631
+ display: flex;
632
+ flex-direction: column;
633
+ gap: var(--space-2);
634
+ padding: 0 var(--space-4);
635
+ }
636
+
637
+ .production-label {
638
+ font-size: var(--font-xs);
639
+ color: var(--ui-text-muted);
640
+ text-transform: uppercase;
641
+ letter-spacing: 0.05em;
642
+ }
643
+
644
+ .production-name {
645
+ font-size: var(--font-md);
646
+ font-weight: var(--font-weight-semibold);
647
+ color: var(--ui-text-primary);
648
+ }
649
+
650
+ .production-update-btn {
651
+ background: var(--ui-surface-high);
652
+ border-color: var(--ui-border-medium);
653
+ color: var(--ui-text-primary);
654
+ }
655
+
656
+ .production-update-btn:hover:not(:disabled) {
657
+ background: var(--ui-surface-higher);
658
+ border-color: var(--ui-border-strong);
659
+ }
660
+
661
+ .production-update-btn.updating i { animation: spin 1s linear infinite; }
662
+ .production-update-btn.done { color: var(--ui-text-success); }
663
+ .production-update-btn.error { color: var(--ui-text-muted); }
664
+
665
+ .production-match {
666
+ font-size: var(--font-xs);
667
+ color: var(--ui-text-muted);
668
+ padding: 0 var(--space-4);
669
+ letter-spacing: 0.02em;
670
+ }
671
+
672
+ .production-diff {
673
+ font-size: var(--font-xs);
674
+ color: var(--ui-text-warning, #e6a030);
675
+ padding: 0 var(--space-4);
676
+ letter-spacing: 0.02em;
677
+ }
678
+
679
+ @keyframes spin {
680
+ from { transform: rotate(0deg); }
681
+ to { transform: rotate(360deg); }
682
+ }
683
+ </style>