@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,617 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import Dialog from '../components/Dialog.svelte';
4
+ import {
5
+ listBackups,
6
+ getBackupContent,
7
+ getCurrentCss,
8
+ loadTokenFile,
9
+ restoreBackup,
10
+ type BackupEntry,
11
+ } from '../lib/tokenService';
12
+ import { activeFileName } from '../lib/editorConfigStore';
13
+
14
+ export let open = false;
15
+
16
+ const dispatch = createEventDispatcher<{ close: void; restored: { type: string } }>();
17
+
18
+ let backups: BackupEntry[] = [];
19
+ let selected: BackupEntry | null = null;
20
+ let backupContent = '';
21
+ let currentContent = '';
22
+ let diffLines: DiffLine[] = [];
23
+ let loading = false;
24
+ let restoring = false;
25
+ let restoreConfirm = false;
26
+ let filterType: 'all' | 'css' | 'tokens' = 'all';
27
+
28
+ interface DiffLine {
29
+ type: 'same' | 'add' | 'remove' | 'context';
30
+ lineOld?: number;
31
+ lineNew?: number;
32
+ text: string;
33
+ }
34
+
35
+ $: filteredBackups = filterType === 'all'
36
+ ? backups
37
+ : backups.filter(b => b.type === filterType);
38
+
39
+ $: if (open) loadBackups();
40
+
41
+ async function loadBackups() {
42
+ loading = true;
43
+ try {
44
+ backups = await listBackups();
45
+ } catch {
46
+ backups = [];
47
+ }
48
+ loading = false;
49
+ }
50
+
51
+ async function selectBackup(backup: BackupEntry) {
52
+ selected = backup;
53
+ restoreConfirm = false;
54
+ loading = true;
55
+ try {
56
+ backupContent = await getBackupContent(backup.type, backup.file);
57
+ if (backup.type === 'css') {
58
+ currentContent = await getCurrentCss();
59
+ } else {
60
+ const tokenData = await loadTokenFile(backup.name);
61
+ currentContent = JSON.stringify(tokenData, null, 2);
62
+ try {
63
+ backupContent = JSON.stringify(JSON.parse(backupContent), null, 2);
64
+ } catch { /* keep as-is */ }
65
+ }
66
+ diffLines = computeDiff(backupContent, currentContent);
67
+ } catch {
68
+ diffLines = [];
69
+ }
70
+ loading = false;
71
+ }
72
+
73
+ async function handleRestore() {
74
+ if (!selected) return;
75
+ restoring = true;
76
+ try {
77
+ await restoreBackup(selected.type, selected.file);
78
+ restoreConfirm = false;
79
+ dispatch('restored', { type: selected.type });
80
+ await loadBackups();
81
+ await selectBackup(selected);
82
+ } catch {
83
+ // silent
84
+ }
85
+ restoring = false;
86
+ }
87
+
88
+ function close() {
89
+ open = false;
90
+ selected = null;
91
+ diffLines = [];
92
+ restoreConfirm = false;
93
+ dispatch('close');
94
+ }
95
+
96
+ function formatTimestamp(ts: string): string {
97
+ try {
98
+ const d = new Date(ts);
99
+ return d.toLocaleString(undefined, {
100
+ month: 'short', day: 'numeric',
101
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
102
+ });
103
+ } catch {
104
+ return ts;
105
+ }
106
+ }
107
+
108
+ function formatSize(bytes: number): string {
109
+ if (bytes < 1024) return `${bytes}B`;
110
+ return `${(bytes / 1024).toFixed(1)}KB`;
111
+ }
112
+
113
+ // ── LCS-based line diff ──────────────────────────────────
114
+
115
+ function computeDiff(oldText: string, newText: string): DiffLine[] {
116
+ const oldLines = oldText.split('\n');
117
+ const newLines = newText.split('\n');
118
+ const ops = myersDiff(oldLines, newLines);
119
+ const result: DiffLine[] = [];
120
+ let oldIdx = 0;
121
+ let newIdx = 0;
122
+
123
+ for (const op of ops) {
124
+ if (op === 'equal') {
125
+ result.push({ type: 'same', lineOld: oldIdx + 1, lineNew: newIdx + 1, text: oldLines[oldIdx] });
126
+ oldIdx++;
127
+ newIdx++;
128
+ } else if (op === 'delete') {
129
+ result.push({ type: 'remove', lineOld: oldIdx + 1, text: oldLines[oldIdx] });
130
+ oldIdx++;
131
+ } else if (op === 'insert') {
132
+ result.push({ type: 'add', lineNew: newIdx + 1, text: newLines[newIdx] });
133
+ newIdx++;
134
+ }
135
+ }
136
+
137
+ return collapseUnchanged(result);
138
+ }
139
+
140
+ function myersDiff(a: string[], b: string[]): ('equal' | 'delete' | 'insert')[] {
141
+ const n = a.length;
142
+ const m = b.length;
143
+ const max = n + m;
144
+ const v: Record<number, number> = { 1: 0 };
145
+ const trace: Record<number, number>[] = [];
146
+
147
+ for (let d = 0; d <= max; d++) {
148
+ const vSnap: Record<number, number> = {};
149
+ for (const k in v) vSnap[Number(k)] = v[Number(k)];
150
+ trace.push(vSnap);
151
+
152
+ for (let k = -d; k <= d; k += 2) {
153
+ let x: number;
154
+ if (k === -d || (k !== d && (v[k - 1] ?? -1) < (v[k + 1] ?? -1))) {
155
+ x = v[k + 1] ?? 0;
156
+ } else {
157
+ x = (v[k - 1] ?? 0) + 1;
158
+ }
159
+ let y = x - k;
160
+ while (x < n && y < m && a[x] === b[y]) {
161
+ x++;
162
+ y++;
163
+ }
164
+ v[k] = x;
165
+ if (x >= n && y >= m) {
166
+ return backtrack(trace, a, b);
167
+ }
168
+ }
169
+ }
170
+ return [];
171
+ }
172
+
173
+ function backtrack(trace: Record<number, number>[], a: string[], b: string[]): ('equal' | 'delete' | 'insert')[] {
174
+ let x = a.length;
175
+ let y = b.length;
176
+ const ops: ('equal' | 'delete' | 'insert')[] = [];
177
+
178
+ for (let d = trace.length - 1; d > 0; d--) {
179
+ const v = trace[d - 1];
180
+ const k = x - y;
181
+ let prevK: number;
182
+ if (k === -d || (k !== d && (v[k - 1] ?? -1) < (v[k + 1] ?? -1))) {
183
+ prevK = k + 1;
184
+ } else {
185
+ prevK = k - 1;
186
+ }
187
+ const prevX = v[prevK] ?? 0;
188
+ const prevY = prevX - prevK;
189
+
190
+ while (x > prevX && y > prevY) {
191
+ ops.push('equal');
192
+ x--;
193
+ y--;
194
+ }
195
+ if (x > prevX) {
196
+ ops.push('delete');
197
+ x--;
198
+ } else if (y > prevY) {
199
+ ops.push('insert');
200
+ y--;
201
+ }
202
+ }
203
+ while (x > 0 && y > 0) {
204
+ ops.push('equal');
205
+ x--;
206
+ y--;
207
+ }
208
+ return ops.reverse();
209
+ }
210
+
211
+ function collapseUnchanged(lines: DiffLine[], contextSize = 3): DiffLine[] {
212
+ const keep = new Array(lines.length).fill(false);
213
+ for (let i = 0; i < lines.length; i++) {
214
+ if (lines[i].type !== 'same') {
215
+ for (let j = Math.max(0, i - contextSize); j <= Math.min(lines.length - 1, i + contextSize); j++) {
216
+ keep[j] = true;
217
+ }
218
+ }
219
+ }
220
+
221
+ const result: DiffLine[] = [];
222
+ let skipping = false;
223
+ for (let i = 0; i < lines.length; i++) {
224
+ if (keep[i]) {
225
+ skipping = false;
226
+ result.push(lines[i]);
227
+ } else if (!skipping) {
228
+ skipping = true;
229
+ result.push({ type: 'context', text: '...' });
230
+ }
231
+ }
232
+ return result;
233
+ }
234
+ </script>
235
+
236
+ <Dialog
237
+ bind:show={open}
238
+ title="Backup History"
239
+ showConfirm={false}
240
+ showCancel={false}
241
+ width="90vw"
242
+ onCancel={close}
243
+ >
244
+ <div class="browser-header">
245
+ <div class="filter-tabs">
246
+ <button class:active={filterType === 'all'} on:click={() => filterType = 'all'}>All</button>
247
+ <button class:active={filterType === 'css'} on:click={() => filterType = 'css'}>CSS</button>
248
+ <button class:active={filterType === 'tokens'} on:click={() => filterType = 'tokens'}>Tokens</button>
249
+ </div>
250
+ {#if selected}
251
+ <div class="diff-actions">
252
+ {#if restoreConfirm}
253
+ <span class="restore-warn">Restore this backup?</span>
254
+ <button class="action-btn confirm" on:click={handleRestore} disabled={restoring}>
255
+ {restoring ? 'Restoring...' : 'Confirm'}
256
+ </button>
257
+ <button class="action-btn cancel" on:click={() => restoreConfirm = false}>Cancel</button>
258
+ {:else}
259
+ <button class="action-btn restore" on:click={() => restoreConfirm = true}>
260
+ <i class="fas fa-undo"></i> Restore
261
+ </button>
262
+ {/if}
263
+ </div>
264
+ {/if}
265
+ </div>
266
+
267
+ <div class="browser-body">
268
+ <div class="backup-list">
269
+ {#if loading && !selected}
270
+ <div class="empty-state">Loading...</div>
271
+ {:else if filteredBackups.length === 0}
272
+ <div class="empty-state">No backups yet</div>
273
+ {:else}
274
+ {#each filteredBackups as backup}
275
+ <button
276
+ class="backup-item"
277
+ class:selected={selected?.file === backup.file}
278
+ on:click={() => selectBackup(backup)}
279
+ >
280
+ <span class="backup-badge" class:css={backup.type === 'css'} class:token={backup.type === 'tokens'}>
281
+ {backup.type === 'css' ? 'CSS' : 'TOK'}
282
+ </span>
283
+ <div class="backup-info">
284
+ <span class="backup-name">{backup.name}</span>
285
+ <span class="backup-meta">{formatTimestamp(backup.timestamp)} &middot; {formatSize(backup.size)}</span>
286
+ </div>
287
+ </button>
288
+ {/each}
289
+ {/if}
290
+ </div>
291
+
292
+ <div class="diff-panel">
293
+ {#if !selected}
294
+ <div class="empty-state">Select a backup to view changes</div>
295
+ {:else if loading}
296
+ <div class="empty-state">Loading diff...</div>
297
+ {:else}
298
+ <div class="diff-meta">
299
+ <span class="backup-badge" class:css={selected.type === 'css'} class:token={selected.type === 'tokens'}>
300
+ {selected.type === 'css' ? 'CSS' : 'TOK'}
301
+ </span>
302
+ <strong>{selected.name}</strong>
303
+ <span class="diff-timestamp">{formatTimestamp(selected.timestamp)}</span>
304
+ </div>
305
+ <div class="diff-legend">
306
+ <span class="legend-item legend-remove">
307
+ <span class="legend-swatch"></span> Backup (restore to this)
308
+ </span>
309
+ <span class="legend-item legend-add">
310
+ <span class="legend-swatch"></span> Current file
311
+ </span>
312
+ </div>
313
+ <div class="diff-content">
314
+ {#each diffLines as line}
315
+ {#if line.type === 'context'}
316
+ <div class="diff-line context">
317
+ <span class="line-num"></span>
318
+ <span class="line-num"></span>
319
+ <span class="line-text">{line.text}</span>
320
+ </div>
321
+ {:else if line.type === 'remove'}
322
+ <div class="diff-line remove">
323
+ <span class="line-num">{line.lineOld ?? ''}</span>
324
+ <span class="line-num"></span>
325
+ <span class="line-text">- {line.text}</span>
326
+ </div>
327
+ {:else if line.type === 'add'}
328
+ <div class="diff-line add">
329
+ <span class="line-num"></span>
330
+ <span class="line-num">{line.lineNew ?? ''}</span>
331
+ <span class="line-text">+ {line.text}</span>
332
+ </div>
333
+ {:else}
334
+ <div class="diff-line same">
335
+ <span class="line-num">{line.lineOld ?? ''}</span>
336
+ <span class="line-num">{line.lineNew ?? ''}</span>
337
+ <span class="line-text"> {line.text}</span>
338
+ </div>
339
+ {/if}
340
+ {/each}
341
+ {#if diffLines.length === 0}
342
+ <div class="empty-state">Files are identical</div>
343
+ {/if}
344
+ </div>
345
+ {/if}
346
+ </div>
347
+ </div>
348
+ </Dialog>
349
+
350
+ <style>
351
+ .browser-header {
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: space-between;
355
+ padding-bottom: var(--space-12);
356
+ border-bottom: 1px solid var(--border-neutral-subtle);
357
+ margin-bottom: var(--space-12);
358
+ }
359
+
360
+ .filter-tabs {
361
+ display: flex;
362
+ gap: 2px;
363
+ background: var(--surface-neutral-low, #1a1a1a);
364
+ border-radius: var(--radius-md);
365
+ padding: 2px;
366
+ }
367
+
368
+ .filter-tabs button {
369
+ padding: var(--space-4) var(--space-12);
370
+ font-size: var(--font-sm);
371
+ background: none;
372
+ border: none;
373
+ color: var(--text-tertiary);
374
+ border-radius: var(--radius-sm);
375
+ cursor: pointer;
376
+ transition: all var(--transition-fast);
377
+ }
378
+
379
+ .filter-tabs button:hover { color: var(--text-secondary); }
380
+ .filter-tabs button.active { background: var(--surface-neutral-medium, #333); color: var(--text-primary); }
381
+
382
+ .diff-actions {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: var(--space-8);
386
+ }
387
+
388
+ .restore-warn {
389
+ font-size: var(--font-sm);
390
+ color: var(--text-warning, #e6a030);
391
+ }
392
+
393
+ .action-btn {
394
+ padding: var(--space-4) var(--space-12);
395
+ font-size: var(--font-sm);
396
+ font-weight: var(--font-weight-medium);
397
+ border: 1px solid var(--border-neutral-subtle);
398
+ border-radius: var(--radius-md);
399
+ cursor: pointer;
400
+ transition: all var(--transition-fast);
401
+ }
402
+
403
+ .action-btn.restore {
404
+ background: var(--surface-success-low, #1a2a1a);
405
+ color: var(--text-success, #8ecf8e);
406
+ border-color: var(--border-success-subtle, #2a4a2a);
407
+ }
408
+ .action-btn.restore:hover { background: var(--surface-success-medium, #2a3a2a); }
409
+
410
+ .action-btn.confirm {
411
+ background: var(--surface-success-medium, #3a5a3a);
412
+ color: var(--text-success-strong, #c0f0c0);
413
+ border-color: var(--border-success-medium, #4a7a4a);
414
+ }
415
+ .action-btn.confirm:hover { background: var(--surface-success-high, #4a6a4a); }
416
+ .action-btn.confirm:disabled { opacity: var(--opacity-disabled, 0.5); cursor: not-allowed; }
417
+
418
+ .action-btn.cancel {
419
+ background: var(--surface-neutral-low, #1a1a1a);
420
+ color: var(--text-secondary);
421
+ }
422
+ .action-btn.cancel:hover { background: var(--surface-neutral-medium, #2a2a2a); }
423
+
424
+ /* ── Layout ── */
425
+ .browser-body {
426
+ display: flex;
427
+ height: 60vh;
428
+ min-width: 0;
429
+ border: 1px solid var(--border-neutral-subtle);
430
+ border-radius: var(--radius-md);
431
+ overflow: hidden;
432
+ }
433
+
434
+ /* ── Backup list ── */
435
+ .backup-list {
436
+ width: 240px;
437
+ flex-shrink: 0;
438
+ border-right: 1px solid var(--border-neutral-subtle);
439
+ overflow-y: auto;
440
+ background: var(--surface-neutral-lowest, #0d0d0d);
441
+ }
442
+
443
+ @media (max-width: 1280px) {
444
+ .backup-list {
445
+ width: 180px;
446
+ }
447
+ }
448
+
449
+ @media (max-width: 1024px) {
450
+ .backup-list {
451
+ width: 140px;
452
+ }
453
+ .line-num {
454
+ width: 32px;
455
+ }
456
+ }
457
+
458
+ .backup-item {
459
+ display: flex;
460
+ align-items: center;
461
+ gap: var(--space-8);
462
+ width: 100%;
463
+ padding: var(--space-8) var(--space-12);
464
+ background: none;
465
+ border: none;
466
+ border-bottom: 1px solid var(--border-neutral-faint, #1a1a1a);
467
+ color: var(--text-tertiary);
468
+ cursor: pointer;
469
+ text-align: left;
470
+ transition: background var(--transition-fast);
471
+ }
472
+ .backup-item:hover { background: var(--hover-low, rgba(255,255,255,0.04)); }
473
+ .backup-item.selected { background: var(--surface-neutral-low, #1f1f1f); color: var(--text-primary); }
474
+
475
+ .backup-badge {
476
+ font-size: 10px;
477
+ font-weight: 700;
478
+ letter-spacing: 0.05em;
479
+ padding: 2px 6px;
480
+ border-radius: var(--radius-sm);
481
+ flex-shrink: 0;
482
+ }
483
+ .backup-badge.css { background: var(--surface-success-low, #1a3a2a); color: var(--text-success, #6dcf97); }
484
+ .backup-badge.token { background: var(--surface-warning-low, #2a2a1a); color: var(--text-warning, #cfb86d); }
485
+
486
+ .backup-info {
487
+ display: flex;
488
+ flex-direction: column;
489
+ gap: 2px;
490
+ min-width: 0;
491
+ }
492
+
493
+ .backup-name {
494
+ font-size: var(--font-sm);
495
+ font-weight: var(--font-weight-medium);
496
+ white-space: nowrap;
497
+ overflow: hidden;
498
+ text-overflow: ellipsis;
499
+ }
500
+
501
+ .backup-meta {
502
+ font-size: var(--font-xs);
503
+ color: var(--text-muted);
504
+ }
505
+
506
+ /* ── Diff panel ── */
507
+ .diff-panel {
508
+ flex: 1;
509
+ display: flex;
510
+ flex-direction: column;
511
+ min-width: 0;
512
+ background: var(--surface-neutral-lowest, #0a0a0a);
513
+ }
514
+
515
+ .diff-meta {
516
+ display: flex;
517
+ align-items: center;
518
+ gap: var(--space-8);
519
+ padding: var(--space-8) var(--space-16);
520
+ border-bottom: 1px solid var(--border-neutral-faint);
521
+ font-size: var(--font-sm);
522
+ color: var(--text-secondary);
523
+ flex-shrink: 0;
524
+ }
525
+
526
+ .diff-timestamp {
527
+ color: var(--text-muted);
528
+ font-weight: 400;
529
+ }
530
+
531
+ .diff-legend {
532
+ display: flex;
533
+ gap: var(--space-16);
534
+ padding: var(--space-4) var(--space-16);
535
+ border-bottom: 1px solid var(--border-neutral-faint);
536
+ flex-shrink: 0;
537
+ }
538
+
539
+ .legend-item {
540
+ display: flex;
541
+ align-items: center;
542
+ gap: var(--space-4);
543
+ font-size: var(--font-xs);
544
+ color: var(--text-muted);
545
+ }
546
+
547
+ .legend-swatch {
548
+ width: 12px;
549
+ height: 12px;
550
+ border-radius: 2px;
551
+ }
552
+
553
+ .legend-remove .legend-swatch { background: rgba(220, 80, 80, 0.25); border: 1px solid rgba(220, 80, 80, 0.4); }
554
+ .legend-add .legend-swatch { background: rgba(80, 180, 80, 0.25); border: 1px solid rgba(80, 180, 80, 0.4); }
555
+
556
+ .diff-content {
557
+ flex: 1;
558
+ overflow: auto;
559
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
560
+ font-size: 12px;
561
+ line-height: 1.5;
562
+ }
563
+
564
+ .diff-line {
565
+ display: flex;
566
+ white-space: pre;
567
+ min-width: fit-content;
568
+ }
569
+
570
+ .line-num {
571
+ width: 44px;
572
+ flex-shrink: 0;
573
+ text-align: right;
574
+ padding-right: 8px;
575
+ color: var(--text-muted, #444);
576
+ user-select: none;
577
+ border-right: 1px solid var(--border-neutral-faint, #1a1a1a);
578
+ }
579
+
580
+ .line-text {
581
+ padding: 0 12px;
582
+ flex: 1;
583
+ }
584
+
585
+ .diff-line.same { color: var(--text-muted, #777); }
586
+ .diff-line.same:hover { background: rgba(255, 255, 255, 0.02); }
587
+
588
+ .diff-line.remove {
589
+ background: rgba(220, 80, 80, 0.12);
590
+ color: #e09090;
591
+ }
592
+ .diff-line.remove .line-num { color: #a06060; }
593
+
594
+ .diff-line.add {
595
+ background: rgba(80, 180, 80, 0.12);
596
+ color: #90c890;
597
+ }
598
+ .diff-line.add .line-num { color: #60a060; }
599
+
600
+ .diff-line.context {
601
+ color: var(--text-muted, #444);
602
+ padding: var(--space-4) 0;
603
+ justify-content: center;
604
+ font-style: italic;
605
+ }
606
+ .diff-line.context .line-num { border: none; }
607
+
608
+ .empty-state {
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: center;
612
+ height: 100%;
613
+ min-height: 80px;
614
+ color: var(--text-muted);
615
+ font-size: var(--font-sm);
616
+ }
617
+ </style>