@seedvault/server 0.2.0 → 0.2.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/dist/index.html CHANGED
@@ -6,34 +6,83 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  <title>Seedvault</title>
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400&display=swap"
11
12
  rel="stylesheet" />
12
13
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css" />
13
14
  <style>
14
- * {
15
- box-sizing: border-box;
16
- margin: 0;
17
- }
18
-
19
- html {
20
- color-scheme: light dark;
21
- font-family: ui-monospace, monospace;
22
- font-size: 12px;
15
+ /* ── Variables ── */
16
+
17
+ :root {
18
+ --bg: #F5F3EE;
19
+ --surface: #FFFFFF;
20
+ --surface-hover: #F0EDE6;
21
+ --surface-active: rgba(62, 124, 83, 0.07);
22
+ --border: #E2DED6;
23
+ --border-subtle: #ECEAE4;
24
+ --text: #1D1D1B;
25
+ --text-secondary: #7D7870;
26
+ --text-tertiary: #B0A99E;
27
+ --accent: #3E7C53;
28
+ --accent-hover: #346A47;
29
+ --accent-text: #FFFFFF;
30
+ --highlight: #C4903D;
31
+ --highlight-dim: rgba(196, 144, 61, 0.10);
32
+ --diff-add: #2E7D42;
33
+ --diff-del: #C62828;
34
+ --diff-hunk: #6A42C1;
35
+ --code-bg: rgba(0, 0, 0, 0.035);
36
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
37
+ --shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
38
+ --radius: 3px;
39
+ --radius-sm: 2px;
40
+ --font-body: system-ui, -apple-system, sans-serif;
41
+ --font-mono: 'IBM Plex Mono', ui-monospace, monospace;
42
+ --transition: 150ms ease;
23
43
  }
24
44
 
25
45
  @media (prefers-color-scheme: dark) {
26
- html {
27
- background: rgb(18, 18, 18);
46
+ :root {
47
+ --bg: #0A0A09;
48
+ --surface: #111110;
49
+ --surface-hover: #1A1A18;
50
+ --surface-active: rgba(107, 175, 126, 0.08);
51
+ --border: #222220;
52
+ --border-subtle: #1A1A18;
53
+ --text: #D4D0C8;
54
+ --text-secondary: #8E887E;
55
+ --text-tertiary: #5A554D;
56
+ --accent: #6BAF7E;
57
+ --accent-hover: #82C494;
58
+ --accent-text: #0A0A09;
59
+ --highlight: #D4A853;
60
+ --highlight-dim: rgba(212, 168, 83, 0.12);
61
+ --diff-add: #81C784;
62
+ --diff-del: #EF9A9A;
63
+ --diff-hunk: #B39DDB;
64
+ --code-bg: rgba(255, 255, 255, 0.04);
65
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
66
+ --shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
28
67
  }
68
+ }
29
69
 
30
- #nav {
31
- color: rgb(204, 204, 204);
32
- }
70
+ /* ── Reset & Base ── */
33
71
 
34
- #content {
35
- color: rgb(230, 230, 230);
36
- }
72
+ *,
73
+ *::before,
74
+ *::after {
75
+ box-sizing: border-box;
76
+ margin: 0;
77
+ }
78
+
79
+ html {
80
+ font-family: var(--font-body);
81
+ font-size: 13px;
82
+ background: var(--bg);
83
+ color: var(--text);
84
+ -webkit-font-smoothing: antialiased;
85
+ -moz-osx-font-smoothing: grayscale;
37
86
  }
38
87
 
39
88
  html,
@@ -42,133 +91,194 @@
42
91
  }
43
92
 
44
93
  body {
45
- padding: 12px;
46
94
  display: flex;
47
95
  flex-direction: column;
96
+ overflow: hidden;
48
97
  }
49
98
 
50
- .top {
99
+ /* ── Top Bar ── */
100
+
101
+ .top-bar {
51
102
  display: flex;
52
- gap: 8px;
53
- margin-bottom: 12px;
54
- flex-wrap: wrap;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ gap: 12px;
106
+ padding: 0 16px;
107
+ height: 52px;
55
108
  flex-shrink: 0;
109
+ border-bottom: 1px solid var(--border);
110
+ background: var(--surface);
111
+ }
112
+
113
+ .top-bar-left {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 12px;
117
+ }
118
+
119
+ #contributor-select {
120
+ font-family: var(--font-body);
121
+ font-size: 13px;
122
+ font-weight: 500;
123
+ padding: 6px 28px 6px 10px;
124
+ min-width: 120px;
125
+ cursor: pointer;
126
+ appearance: none;
127
+ -webkit-appearance: none;
128
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%237D7870'/%3E%3C/svg%3E");
129
+ background-repeat: no-repeat;
130
+ background-position: right 10px center;
131
+ }
132
+
133
+ #contributor-select:focus {
134
+ border-color: var(--accent);
135
+ }
136
+
137
+ #contributor-select:disabled {
138
+ opacity: 0.5;
139
+ cursor: default;
140
+ }
141
+
142
+ @media (prefers-color-scheme: dark) {
143
+ #contributor-select {
144
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%238E887E'/%3E%3C/svg%3E");
145
+ }
146
+ }
147
+
148
+ .top-bar-right {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 8px;
56
152
  }
57
153
 
58
154
  input,
59
- select,
60
- button {
61
- font: inherit;
62
- padding: 4px 8px;
63
- background: transparent;
64
- color: inherit;
65
- border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
155
+ select {
156
+ font-family: var(--font-mono);
157
+ font-size: 12px;
158
+ padding: 6px 10px;
159
+ background: var(--bg);
160
+ color: var(--text);
161
+ border: 1px solid var(--border);
162
+ border-radius: var(--radius-sm);
163
+ outline: none;
164
+ transition: border-color var(--transition);
165
+ }
166
+
167
+ input:focus {
168
+ border-color: var(--accent);
169
+ }
170
+
171
+ input::placeholder {
172
+ color: var(--text-tertiary);
66
173
  }
67
174
 
68
175
  #token {
69
- min-width: 260px;
70
- flex: 1;
176
+ width: 240px;
71
177
  }
72
178
 
73
179
  button {
180
+ font-family: var(--font-mono);
181
+ font-size: 11px;
182
+ font-weight: 500;
74
183
  cursor: pointer;
184
+ border: none;
185
+ background: transparent;
186
+ color: var(--text);
187
+ padding: 0;
188
+ transition: color var(--transition), background var(--transition), opacity var(--transition);
189
+ }
190
+
191
+ .btn-primary {
192
+ padding: 6px 14px;
193
+ background: var(--accent);
194
+ color: var(--accent-text);
195
+ border-radius: var(--radius-sm);
196
+ }
197
+
198
+ .btn-primary:hover {
199
+ background: var(--accent-hover);
75
200
  }
76
201
 
77
202
  #menu-toggle {
78
203
  display: none;
204
+ padding: 6px 8px;
79
205
  font-size: 18px;
80
- padding: 4px 10px;
206
+ border-radius: var(--radius-sm);
81
207
  }
82
208
 
209
+ #menu-toggle:hover {
210
+ background: var(--surface-hover);
211
+ }
212
+
213
+ /* ── Mobile Nav ── */
214
+
83
215
  #nav-close {
84
216
  display: none;
85
- float: right;
86
- border: none;
217
+ padding: 4px;
87
218
  font-size: 16px;
88
- padding: 0 4px;
89
- line-height: 1;
219
+ border-radius: var(--radius-sm);
220
+ opacity: 0.5;
221
+ }
222
+
223
+ #nav-close:hover {
224
+ opacity: 1;
225
+ background: var(--surface-hover);
90
226
  }
91
227
 
92
228
  #backdrop {
93
229
  display: none;
94
230
  position: fixed;
95
231
  inset: 0;
96
- background: rgba(0, 0, 0, 0.4);
232
+ background: rgba(0, 0, 0, 0.35);
97
233
  z-index: 90;
234
+ backdrop-filter: blur(2px);
235
+ -webkit-backdrop-filter: blur(2px);
98
236
  }
99
237
 
100
238
  #backdrop.visible {
101
239
  display: block;
102
240
  }
103
241
 
242
+ /* ── Grid Layout ── */
243
+
104
244
  .grid {
105
245
  display: flex;
106
- gap: 0;
107
246
  flex: 1;
108
247
  min-height: 0;
109
248
  }
110
249
 
111
250
  .grid>.panel:first-child {
112
251
  flex-shrink: 0;
252
+ background: var(--surface);
253
+ border-right: 1px solid var(--border);
113
254
  }
114
255
 
115
256
  .grid>.panel:last-child {
116
257
  flex: 1;
117
258
  min-width: 0;
259
+ background: var(--bg);
118
260
  }
119
261
 
120
262
  .divider {
121
- width: 5px;
263
+ width: 4px;
122
264
  cursor: col-resize;
123
265
  background: transparent;
124
266
  flex-shrink: 0;
267
+ position: relative;
268
+ z-index: 2;
269
+ margin-left: -2px;
270
+ margin-right: -2px;
125
271
  }
126
272
 
127
273
  .divider:hover,
128
274
  .divider.dragging {
129
- background: color-mix(in srgb, currentColor 20%, transparent);
275
+ background: var(--accent);
276
+ opacity: 0.3;
130
277
  }
131
278
 
132
- @media (max-width: 600px) {
133
- #menu-toggle {
134
- display: block;
135
- }
136
-
137
- #nav-close {
138
- display: block;
139
- }
140
-
141
- .grid {
142
- flex-direction: column;
143
- position: relative;
144
- }
145
-
146
- #nav {
147
- position: fixed;
148
- top: 0;
149
- left: -100vw;
150
- width: 100vw !important;
151
- height: 100%;
152
- z-index: 100;
153
- background: Canvas;
154
- transition: transform 0.2s ease;
155
- }
156
-
157
- #nav.open {
158
- transform: translateX(100vw);
159
- }
160
-
161
- .grid>.panel:first-child {
162
- width: auto;
163
- }
164
-
165
- .divider {
166
- display: none;
167
- }
168
- }
279
+ /* ── Panels ── */
169
280
 
170
281
  .panel {
171
- border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
172
282
  display: flex;
173
283
  flex-direction: column;
174
284
  min-height: 0;
@@ -176,13 +286,18 @@
176
286
  }
177
287
 
178
288
  .panel-head {
179
- padding: 4px 8px;
180
- border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
181
- font-weight: bold;
289
+ padding: 10px 14px;
290
+ border-bottom: 1px solid var(--border-subtle);
291
+ font-family: var(--font-mono);
182
292
  font-size: 11px;
293
+ font-weight: 600;
183
294
  text-transform: uppercase;
184
- letter-spacing: 0.05em;
295
+ letter-spacing: 0.06em;
296
+ color: var(--text-secondary);
185
297
  flex-shrink: 0;
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: space-between;
186
301
  }
187
302
 
188
303
  #nav-body:focus {
@@ -193,34 +308,46 @@
193
308
  flex: 1;
194
309
  overflow: auto;
195
310
  scrollbar-width: thin;
196
- scrollbar-color: color-mix(in srgb, currentColor 20%, transparent) transparent;
311
+ scrollbar-color: var(--border) transparent;
312
+ }
313
+
314
+ .grid>.panel:last-child>.panel-body {
315
+ overflow: auto;
197
316
  }
198
317
 
318
+ /* ── Scrollbars ── */
319
+
199
320
  .panel-body::-webkit-scrollbar,
200
- #content::-webkit-scrollbar {
321
+ #content::-webkit-scrollbar,
322
+ #activity-dialog-body::-webkit-scrollbar {
201
323
  width: 6px;
202
324
  height: 6px;
203
325
  }
204
326
 
205
327
  .panel-body::-webkit-scrollbar-track,
206
- #content::-webkit-scrollbar-track {
328
+ #content::-webkit-scrollbar-track,
329
+ #activity-dialog-body::-webkit-scrollbar-track {
207
330
  background: transparent;
208
331
  }
209
332
 
210
333
  .panel-body::-webkit-scrollbar-thumb,
211
- #content::-webkit-scrollbar-thumb {
212
- background: color-mix(in srgb, currentColor 20%, transparent);
334
+ #content::-webkit-scrollbar-thumb,
335
+ #activity-dialog-body::-webkit-scrollbar-thumb {
336
+ background: var(--border);
213
337
  border-radius: 3px;
214
338
  }
215
339
 
216
340
  .panel-body::-webkit-scrollbar-thumb:hover,
217
- #content::-webkit-scrollbar-thumb:hover {
218
- background: color-mix(in srgb, currentColor 30%, transparent);
341
+ #content::-webkit-scrollbar-thumb:hover,
342
+ #activity-dialog-body::-webkit-scrollbar-thumb:hover {
343
+ background: var(--text-tertiary);
219
344
  }
220
345
 
346
+ /* ── File Tree ── */
347
+
221
348
  .tree {
222
349
  list-style: none;
223
- padding: 0;
350
+ padding: 4px 0;
224
351
  }
225
352
 
226
353
  .tree ul {
@@ -233,11 +360,15 @@
233
360
  align-items: baseline;
234
361
  width: 100%;
235
362
  text-align: left;
236
- border: none;
237
- padding: 4px 8px;
363
+ padding: 5px 12px;
238
364
  white-space: nowrap;
239
365
  overflow: hidden;
240
366
  cursor: pointer;
367
+ border-left: 2px solid transparent;
368
+ border-radius: 0;
369
+ font-family: var(--font-mono);
370
+ font-size: 11px;
371
+ transition: background var(--transition), border-color var(--transition), color var(--transition);
241
372
  }
242
373
 
243
374
  .tree-row .name {
@@ -250,172 +381,332 @@
250
381
  .tree-row .ctime {
251
382
  flex-shrink: 0;
252
383
  margin-left: 8px;
253
- opacity: 0.45;
384
+ color: var(--text-tertiary);
254
385
  font-size: 10px;
386
+ font-family: var(--font-mono);
255
387
  }
256
388
 
257
389
  .tree-row:hover {
258
- background: color-mix(in srgb, currentColor 8%, transparent);
390
+ background: var(--surface-hover);
259
391
  }
260
392
 
261
393
  .tree-row.active {
262
- background: CanvasText;
263
- color: Canvas;
394
+ background: var(--text);
395
+ border-left-color: transparent;
396
+ color: var(--bg);
397
+ font-weight: 500;
264
398
  }
265
399
 
266
400
  .tree-row .arrow {
267
401
  display: inline-block;
268
402
  width: 1em;
269
403
  text-align: center;
404
+ font-size: 9px;
405
+ color: var(--text-tertiary);
406
+ transition: color var(--transition);
407
+ }
408
+
409
+ .tree-row:hover .arrow {
410
+ color: var(--text-secondary);
270
411
  }
271
412
 
272
413
  .tree-row .ph {
273
414
  display: inline-block;
274
415
  font-size: 14px;
275
- margin-left: 12px;
276
- margin-right: 2px;
277
- vertical-align: -0.15em;
416
+ margin-left: 6px;
417
+ margin-right: 4px;
418
+ vertical-align: -0.12em;
419
+ color: var(--text-tertiary);
420
+ }
421
+
422
+ .tree-row.active .ph {
423
+ color: var(--bg);
424
+ }
425
+
426
+ .tree-row.active .ctime {
427
+ color: var(--bg);
428
+ opacity: 0.6;
429
+ }
430
+
431
+ .tree-row.active .arrow {
432
+ color: var(--bg);
278
433
  }
279
434
 
280
435
  .tree ul.collapsed {
281
436
  display: none;
282
437
  }
283
438
 
439
+ /* ── Item Header ── */
440
+
441
+ .item-header {
442
+ padding: 24px 0 20px;
443
+ margin-bottom: 20px;
444
+ border-bottom: 1px solid var(--border);
445
+ }
446
+
447
+ .item-header-title {
448
+ font-family: var(--font-body);
449
+ font-size: 5rem;
450
+ font-weight: 500;
451
+ line-height: 1.05;
452
+ letter-spacing: -0.01em;
453
+ color: var(--text);
454
+ overflow-wrap: break-word;
455
+ word-break: break-word;
456
+ }
457
+
458
+ .item-header-path {
459
+ font-size: 11px;
460
+ font-family: var(--font-mono);
461
+ color: var(--text-tertiary);
462
+ margin-bottom: 6px;
463
+ }
464
+
465
+ .item-header-meta {
466
+ display: flex;
467
+ flex-direction: column;
468
+ gap: 6px;
469
+ margin-top: 14px;
470
+ font-size: 12px;
471
+ }
472
+
473
+ .item-header-meta .meta-field {
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 6px;
477
+ }
478
+
479
+ .item-header-meta .meta-label {
480
+ font-weight: 600;
481
+ text-transform: uppercase;
482
+ font-size: 10px;
483
+ letter-spacing: 0.04em;
484
+ color: var(--text-tertiary);
485
+ }
486
+
487
+ .item-header-meta .meta-value {
488
+ font-family: var(--font-mono);
489
+ font-size: 11px;
490
+ color: var(--text-secondary);
491
+ }
492
+
493
+ .item-header-meta .meta-tag {
494
+ display: inline-block;
495
+ padding: 2px 8px;
496
+ border-radius: 99px;
497
+ background: var(--surface-hover);
498
+ border: 1px solid var(--border-subtle);
499
+ font-size: 11px;
500
+ color: var(--text-secondary);
501
+ }
502
+
503
+ /* ── Empty State ── */
504
+
505
+ .empty-state {
506
+ display: flex;
507
+ flex-direction: column;
508
+ align-items: center;
509
+ justify-content: center;
510
+ height: 100%;
511
+ color: var(--text-tertiary);
512
+ gap: 12px;
513
+ user-select: none;
514
+ }
515
+
516
+ .empty-state .ph {
517
+ font-size: 48px;
518
+ opacity: 0.4;
519
+ }
520
+
521
+ .empty-state p {
522
+ font-size: 14px;
523
+ font-weight: 500;
524
+ }
525
+
526
+ /* ── Content / Markdown ── */
527
+
284
528
  #content {
285
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
529
+ font-family: var(--font-body);
286
530
  font-size: 16px;
287
- padding: 24px 32px;
288
- overflow: auto;
289
- line-height: 1.6;
531
+ padding: 28px 32px;
532
+ line-height: 1.5;
533
+ color: var(--text);
534
+ max-width: 544px;
535
+ margin: 0 auto;
290
536
  }
291
537
 
292
538
  #content h1 {
293
- font-size: 1.75em;
294
- font-weight: 700;
295
- margin: 0 0 0.5em;
296
- line-height: 1.3;
539
+ font-size: 2rem;
540
+ font-weight: 600;
541
+ margin: 1rem 0 0.5rem;
542
+ line-height: 2.5rem;
543
+ letter-spacing: 0.02rem;
297
544
  }
298
545
 
299
546
  #content h2 {
300
- font-size: 1.4em;
547
+ font-size: 1.6rem;
301
548
  font-weight: 600;
302
- margin: 1em 0 0.5em;
303
- line-height: 1.3;
549
+ margin: 1rem 0 0.3rem;
550
+ line-height: 2.5rem;
551
+ letter-spacing: 0.02rem;
304
552
  }
305
553
 
306
554
  #content h3 {
307
- font-size: 1.2em;
555
+ font-size: 1.2rem;
308
556
  font-weight: 600;
309
- margin: 1em 0 0.5em;
310
- line-height: 1.3;
557
+ margin: 0.2rem 0 0;
558
+ line-height: 2rem;
559
+ letter-spacing: 0.02rem;
311
560
  }
312
561
 
313
562
  #content h4,
314
563
  #content h5,
315
564
  #content h6 {
316
- font-size: 1.05em;
565
+ font-size: 1em;
317
566
  font-weight: 600;
318
- margin: 0.75em 0 0.5em;
567
+ margin: 0.8em 0 0.4em;
319
568
  }
320
569
 
321
570
  #content p {
322
- margin: 0 0 0.75em;
571
+ margin: 0 0 0.8em;
323
572
  }
324
573
 
325
574
  #content img {
326
575
  max-width: 100%;
327
576
  height: auto;
328
577
  display: block;
578
+ border-radius: var(--radius-sm);
329
579
  }
330
580
 
331
581
  #content ul,
332
582
  #content ol {
333
- margin: 0 0 0.75em;
334
- padding-left: 1.5em;
583
+ margin: 0 0 0.8em;
584
+ padding-left: 1.4em;
585
+ }
586
+
587
+ #content li {
588
+ margin-bottom: 0.2em;
335
589
  }
336
590
 
337
591
  #content pre,
338
592
  #content code {
339
- font-family: ui-monospace, monospace;
340
- font-size: 0.9em;
593
+ font-family: var(--font-mono);
594
+ font-size: 0.88em;
341
595
  }
342
596
 
343
597
  #content pre {
344
- background: color-mix(in srgb, currentColor 8%, transparent);
345
- padding: 10px;
346
- border-radius: 4px;
598
+ background: var(--code-bg);
599
+ padding: 14px 16px;
600
+ border-radius: var(--radius);
347
601
  overflow-x: auto;
348
- margin: 0.5em 0;
602
+ margin: 0.6em 0 1em;
603
+ border: 1px solid var(--border-subtle);
604
+ line-height: 1.5;
349
605
  }
350
606
 
351
607
  #content code {
352
- padding: 0.15em 0.3em;
353
- border-radius: 3px;
354
- background: color-mix(in srgb, currentColor 8%, transparent);
608
+ padding: 0.15em 0.35em;
609
+ border-radius: var(--radius-sm);
610
+ background: var(--code-bg);
355
611
  }
356
612
 
357
613
  #content pre code {
358
614
  padding: 0;
359
615
  background: none;
616
+ border: none;
360
617
  }
361
618
 
362
619
  #content blockquote {
363
- border-left: 4px solid color-mix(in srgb, currentColor 30%, transparent);
364
- margin: 0.5em 0;
365
- padding-left: 1em;
366
- color: color-mix(in srgb, currentColor 80%, transparent);
620
+ border-left: 3px solid var(--accent);
621
+ margin: 0.6em 0 1em;
622
+ padding: 0.3em 0 0.3em 1em;
623
+ color: var(--text-secondary);
624
+ }
625
+
626
+ #content blockquote p:last-child {
627
+ margin-bottom: 0;
367
628
  }
368
629
 
369
630
  #content a {
370
- color: inherit;
631
+ color: var(--accent);
371
632
  text-decoration: underline;
633
+ text-decoration-color: rgba(62, 124, 83, 0.3);
634
+ text-underline-offset: 2px;
635
+ transition: text-decoration-color var(--transition);
636
+ }
637
+
638
+ #content a:hover {
639
+ text-decoration-color: var(--accent);
640
+ }
641
+
642
+ @media (prefers-color-scheme: dark) {
643
+ #content a {
644
+ text-decoration-color: rgba(107, 175, 126, 0.3);
645
+ }
372
646
  }
373
647
 
374
648
  #content hr {
375
649
  border: none;
376
- border-top: 1px solid color-mix(in srgb, currentColor 25%, transparent);
377
- margin: 1.5em 0;
650
+ border-top: 1px solid var(--border);
651
+ margin: 2em 0;
378
652
  }
379
653
 
380
654
  #content table {
381
655
  border-collapse: collapse;
382
- margin: 0.5em 0;
656
+ margin: 0.6em 0 1em;
657
+ font-size: 0.92em;
383
658
  }
384
659
 
385
660
  #content th,
386
661
  #content td {
387
- border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
388
- padding: 6px 10px;
662
+ border: 1px solid var(--border);
663
+ padding: 8px 12px;
389
664
  text-align: left;
390
665
  }
391
666
 
667
+ #content th {
668
+ background: var(--code-bg);
669
+ font-weight: 600;
670
+ }
671
+
672
+ /* ── Status Bar ── */
673
+
392
674
  #status {
393
- margin-top: 8px;
675
+ padding: 0 16px;
676
+ height: 32px;
394
677
  font-size: 11px;
395
- opacity: 0.6;
678
+ color: var(--text-tertiary);
396
679
  flex-shrink: 0;
397
680
  display: flex;
398
681
  align-items: center;
682
+ border-top: 1px solid var(--border-subtle);
683
+ background: var(--surface);
399
684
  }
400
685
 
401
686
  #status-text {
402
687
  flex: 1;
688
+ font-family: var(--font-mono);
689
+ font-size: 11px;
403
690
  }
404
691
 
405
692
  #activity-btn {
406
- border: none;
407
693
  font-size: 11px;
408
- padding: 0 4px;
409
- opacity: 0.6;
694
+ font-weight: 500;
695
+ padding: 4px 8px;
696
+ border-radius: var(--radius-sm);
697
+ color: var(--text-secondary);
410
698
  display: flex;
411
699
  align-items: center;
412
- gap: 3px;
700
+ gap: 4px;
413
701
  }
414
702
 
415
703
  #activity-btn:hover {
416
- opacity: 1;
704
+ color: var(--text);
705
+ background: var(--surface-hover);
417
706
  }
418
707
 
708
+ /* ── Activity Panel ── */
709
+
419
710
  #activity-modal {
420
711
  display: none;
421
712
  position: fixed;
@@ -430,49 +721,79 @@
430
721
  #activity-overlay {
431
722
  position: absolute;
432
723
  inset: 0;
433
- background: rgba(0, 0, 0, 0.4);
724
+ background: rgba(0, 0, 0, 0.3);
725
+ backdrop-filter: blur(2px);
726
+ -webkit-backdrop-filter: blur(2px);
727
+ animation: fadeIn 0.2s ease;
434
728
  }
435
729
 
436
730
  #activity-dialog {
437
731
  position: absolute;
438
732
  inset: 0;
439
- background: Canvas;
440
- color: CanvasText;
733
+ background: var(--surface);
734
+ color: var(--text);
441
735
  display: flex;
442
736
  flex-direction: column;
443
737
  overflow: hidden;
444
738
  z-index: 1;
739
+ animation: fadeIn 0.2s ease;
740
+ }
741
+
742
+ @keyframes fadeIn {
743
+ from {
744
+ opacity: 0;
745
+ }
746
+
747
+ to {
748
+ opacity: 1;
749
+ }
445
750
  }
446
751
 
752
+
447
753
  #activity-dialog-head {
448
- padding: 8px 12px;
449
- border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
450
- font-weight: bold;
451
- font-size: 11px;
452
- text-transform: uppercase;
453
- letter-spacing: 0.05em;
754
+ padding: 10px 16px;
755
+ border-bottom: 1px solid var(--border);
756
+ font-size: 12px;
757
+ font-weight: 600;
454
758
  display: flex;
455
759
  align-items: center;
456
760
  flex-shrink: 0;
761
+ color: var(--text-secondary);
762
+ }
763
+
764
+ #activity-dialog-head span {
765
+ display: flex;
766
+ align-items: center;
767
+ gap: 6px;
768
+ }
769
+
770
+ #activity-dialog-head .ph {
771
+ font-size: 16px;
772
+ color: var(--accent);
457
773
  }
458
774
 
459
775
  #activity-dialog-head button {
460
776
  margin-left: auto;
461
- border: none;
777
+ padding: 4px 6px;
462
778
  font-size: 16px;
463
- padding: 0 4px;
464
- line-height: 1;
779
+ border-radius: var(--radius-sm);
780
+ color: var(--text-tertiary);
781
+ }
782
+
783
+ #activity-dialog-head button:hover {
784
+ color: var(--text);
785
+ background: var(--surface-hover);
465
786
  }
466
787
 
467
788
  #activity-dialog-body {
468
789
  flex: 1;
469
790
  overflow-y: auto;
470
791
  scrollbar-width: thin;
471
- scrollbar-color: color-mix(in srgb, currentColor 20%, transparent) transparent;
792
+ scrollbar-color: var(--border) transparent;
472
793
  }
473
794
 
474
795
  .activity-list {
475
- padding: 16px 0;
796
+ padding: 12px 0;
476
797
  margin: 0;
477
798
  list-style: none;
478
799
  }
@@ -491,13 +812,14 @@
491
812
 
492
813
  .activity-time {
493
814
  font-size: 11px;
494
- opacity: 0.7;
815
+ font-family: var(--font-mono);
816
+ color: var(--text-secondary);
495
817
  white-space: nowrap;
496
818
  }
497
819
 
498
820
  .activity-date {
499
821
  font-size: 10px;
500
- opacity: 0.35;
822
+ color: var(--text-tertiary);
501
823
  }
502
824
 
503
825
  .activity-dot-col {
@@ -515,7 +837,7 @@
515
837
  bottom: 0;
516
838
  width: 1px;
517
839
  transform: translateX(-50%);
518
- background: color-mix(in srgb, currentColor 15%, transparent);
840
+ background: var(--border);
519
841
  }
520
842
 
521
843
  .activity-item:first-child .activity-dot-col::after {
@@ -527,13 +849,13 @@
527
849
  }
528
850
 
529
851
  .activity-dot {
530
- width: 5px;
531
- height: 5px;
852
+ width: 6px;
853
+ height: 6px;
532
854
  border-radius: 50%;
533
- background: CanvasText;
534
- opacity: 0.35;
855
+ background: var(--accent);
856
+ opacity: 0.5;
535
857
  flex-shrink: 0;
536
- margin-top: 11px;
858
+ margin-top: 10px;
537
859
  position: relative;
538
860
  z-index: 1;
539
861
  }
@@ -543,156 +865,244 @@
543
865
  }
544
866
 
545
867
  .activity-content {
546
- padding: 6px 0 0;
868
+ padding: 6px 16px 8px 0;
547
869
  min-width: 0;
548
870
  }
549
871
 
550
872
  .activity-action {
551
- font-family: ui-monospace, monospace;
873
+ font-family: var(--font-mono);
552
874
  font-size: 11px;
875
+ font-weight: 500;
553
876
  }
554
877
 
555
878
  .activity-by {
556
879
  font-size: 11px;
557
- opacity: 0.45;
880
+ color: var(--text-tertiary);
558
881
  }
559
882
 
560
883
  .activity-contributor {
561
884
  font-weight: 600;
562
- opacity: 1;
885
+ color: var(--text-secondary);
563
886
  }
564
887
 
565
888
  .activity-detail {
566
- margin-top: 2px;
889
+ margin-top: 3px;
567
890
  font-size: 11px;
568
- opacity: 0.45;
569
- font-family: ui-monospace, monospace;
891
+ color: var(--text-tertiary);
892
+ font-family: var(--font-mono);
570
893
  word-break: break-all;
571
894
  }
572
895
 
573
896
  .activity-diff {
574
- margin-top: 4px;
575
- font-family: ui-monospace, monospace;
897
+ margin-top: 6px;
898
+ font-family: var(--font-mono);
576
899
  font-size: 11px;
577
- line-height: 1.4;
900
+ line-height: 1.45;
578
901
  white-space: pre-wrap;
579
902
  word-break: break-all;
580
- padding: 4px 6px;
581
- background: color-mix(in srgb, currentColor 4%, transparent);
582
- border-radius: 3px;
903
+ padding: 8px 10px;
904
+ background: var(--code-bg);
905
+ border-radius: var(--radius-sm);
906
+ border: 1px solid var(--border-subtle);
583
907
  overflow-x: auto;
584
908
  }
585
909
 
586
910
  .activity-diff .diff-add {
587
- color: #22863a;
911
+ color: var(--diff-add);
588
912
  }
589
913
 
590
914
  .activity-diff .diff-del {
591
- color: #cb2431;
915
+ color: var(--diff-del);
592
916
  }
593
917
 
594
918
  .activity-diff .diff-hunk {
595
- color: #6f42c1;
919
+ color: var(--diff-hunk);
596
920
  opacity: 0.7;
597
921
  }
598
922
 
599
- @media (prefers-color-scheme: dark) {
600
- .activity-diff .diff-add {
601
- color: #85e89d;
602
- }
603
-
604
- .activity-diff .diff-del {
605
- color: #f97583;
606
- }
607
-
608
- .activity-diff .diff-hunk {
609
- color: #b392f0;
610
- }
611
- }
612
-
613
923
  .activity-diff-truncated {
614
924
  font-size: 10px;
615
- opacity: 0.5;
925
+ color: var(--text-tertiary);
616
926
  font-style: italic;
927
+ margin-top: 2px;
617
928
  }
618
929
 
619
930
  #activity-load-more {
620
931
  width: 100%;
621
- padding: 8px;
622
- border: none;
623
- border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent);
932
+ padding: 10px;
933
+ border-top: 1px solid var(--border-subtle);
624
934
  font-size: 11px;
935
+ font-weight: 500;
625
936
  text-transform: uppercase;
626
- letter-spacing: 0.05em;
627
- opacity: 0.6;
937
+ letter-spacing: 0.06em;
938
+ color: var(--text-tertiary);
628
939
  }
629
940
 
630
941
  #activity-load-more:hover {
631
- opacity: 1;
632
- background: color-mix(in srgb, currentColor 5%, transparent);
942
+ color: var(--text);
943
+ background: var(--surface-hover);
944
+ }
945
+
946
+ /* ── Responsive ── */
947
+
948
+ @media (max-width: 600px) {
949
+ .top-bar {
950
+ padding: 0 12px;
951
+ height: 48px;
952
+ gap: 8px;
953
+ }
954
+
955
+ .top-bar-right {
956
+ flex: 1;
957
+ min-width: 0;
958
+ }
959
+
960
+ #contributor-select {
961
+ min-width: 100px;
962
+ flex-shrink: 1;
963
+ }
964
+
965
+ #token {
966
+ width: auto;
967
+ flex: 1;
968
+ min-width: 80px;
969
+ }
970
+
971
+ #menu-toggle {
972
+ display: flex;
973
+ align-items: center;
974
+ }
975
+
976
+ #nav-close {
977
+ display: flex;
978
+ align-items: center;
979
+ }
980
+
981
+ .grid {
982
+ flex-direction: column;
983
+ position: relative;
984
+ }
985
+
986
+ #nav {
987
+ position: fixed;
988
+ top: 0;
989
+ left: -100vw;
990
+ width: 100vw !important;
991
+ height: 100%;
992
+ z-index: 100;
993
+ background: var(--surface);
994
+ transition: transform 0.2s ease;
995
+ border-right: none !important;
996
+ }
997
+
998
+ #nav.open {
999
+ transform: translateX(100vw);
1000
+ }
1001
+
1002
+ .grid>.panel:first-child {
1003
+ width: auto;
1004
+ }
1005
+
1006
+ .divider {
1007
+ display: none;
1008
+ }
1009
+
1010
+ #content {
1011
+ padding: 20px 16px;
1012
+ }
1013
+
1014
+ .item-header-title {
1015
+ font-size: 3rem;
1016
+ }
633
1017
  }
634
1018
  </style>
635
1019
  </head>
636
1020
 
637
1021
  <body>
638
- <div class="top">
639
- <button id="menu-toggle" aria-label="Toggle navigation">&#9776;</button>
640
- <input id="token" type="password" placeholder="API key (&quot;sv_...&quot;)" />
641
- <button id="connect">Load</button>
642
- </div>
1022
+ <header class="top-bar">
1023
+ <div class="top-bar-left">
1024
+ <button id="menu-toggle" aria-label="Toggle navigation"><i class="ph ph-list"></i></button>
1025
+ <select id="contributor-select" disabled>
1026
+ <option value="">Contributors</option>
1027
+ </select>
1028
+ </div>
1029
+ <div class="top-bar-right">
1030
+ <input id="token" type="password" placeholder="API key" />
1031
+ <button id="connect" class="btn-primary">Connect</button>
1032
+ </div>
1033
+ </header>
643
1034
  <div id="backdrop"></div>
644
1035
  <div class="grid">
645
- <section id="nav" class="panel" style="width:220px">
646
- <div class="panel-head">Files<button id="nav-close" aria-label="Close navigation">&times;</button></div>
1036
+ <section id="nav" class="panel" style="width:260px">
1037
+ <div class="panel-head"><span>Files</span><button id="nav-close" aria-label="Close navigation"><i
1038
+ class="ph ph-x"></i></button></div>
647
1039
  <div class="panel-body" id="nav-body" tabindex="0">
648
1040
  <ul id="files" class="tree"></ul>
649
1041
  </div>
650
1042
  </section>
651
1043
  <div id="divider" class="divider"></div>
652
1044
  <section class="panel">
653
- <div class="panel-head">Content</div>
654
1045
  <div class="panel-body">
655
- <div id="content"></div>
1046
+ <div id="content">
1047
+ <div id="item-header" class="item-header" hidden>
1048
+ <div class="item-header-path" id="item-path"></div>
1049
+ <div class="item-header-title" id="item-title"></div>
1050
+ <div class="item-header-meta" id="item-meta"></div>
1051
+ </div>
1052
+ <div id="content-body">
1053
+ <div class="empty-state">
1054
+ <i class="ph ph-plant"></i>
1055
+ <p>Select a file to view</p>
1056
+ </div>
1057
+ </div>
1058
+ </div>
656
1059
  </div>
657
1060
  </section>
658
1061
  </div>
659
1062
  <div id="activity-modal">
660
1063
  <div id="activity-overlay"></div>
661
1064
  <div id="activity-dialog">
662
- <div id="activity-dialog-head">Activity Log<button id="activity-close" aria-label="Close">&times;</button></div>
1065
+ <div id="activity-dialog-head"><span><i class="ph ph-pulse"></i> Activity</span><button id="activity-close"
1066
+ aria-label="Close"><i class="ph ph-x"></i></button></div>
663
1067
  <div id="activity-dialog-body"></div>
664
1068
  </div>
665
1069
  </div>
666
- <div id="status">
1070
+ <footer id="status">
667
1071
  <span id="status-text"></span>
668
- <button id="activity-btn" aria-label="Activity log"><i class="ph ph-pulse"></i> Activity Log</button>
669
- </div>
1072
+ <button id="activity-btn" aria-label="Activity log"><i class="ph ph-clock-counter-clockwise"></i>
1073
+ Activity</button>
1074
+ </footer>
670
1075
  <script type="module">
671
1076
  const { marked } = await import("https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js");
672
1077
  const matter = (await import("https://cdn.jsdelivr.net/npm/gray-matter@4.0.3/+esm")).default;
673
1078
  const DOMPurify = (await import("https://cdn.jsdelivr.net/npm/dompurify@3.0.9/+esm")).default;
674
1079
  const morphdom = (await import("https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm")).default;
675
1080
 
1081
+ const emptyStateHTML = '<div class="empty-state"><i class="ph ph-plant"></i><p>Select a file to view</p></div>';
1082
+
676
1083
  function renderMarkdown(raw) {
677
- if (typeof raw !== "string") return "";
1084
+ if (typeof raw !== "string") return { html: "", meta: {} };
678
1085
  try {
679
- const { content } = matter(raw);
1086
+ const { content, data } = matter(raw);
680
1087
  const html = marked.parse(content);
681
- return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
1088
+ return {
1089
+ html: DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }),
1090
+ meta: data || {},
1091
+ };
682
1092
  } catch {
683
1093
  const escaped = raw
684
1094
  .replace(/&/g, "&amp;")
685
1095
  .replace(/</g, "&lt;")
686
1096
  .replace(/>/g, "&gt;")
687
1097
  .replace(/"/g, "&quot;");
688
- return "<pre>" + escaped + "</pre>";
1098
+ return { html: "<pre>" + escaped + "</pre>", meta: {} };
689
1099
  }
690
1100
  }
691
1101
 
692
1102
  const $ = (id) => document.getElementById(id);
693
1103
  const tokenEl = $("token");
694
1104
  const filesEl = $("files");
695
- const contentEl = $("content");
1105
+ const contentEl = $("content-body");
696
1106
  const statusTextEl = $("status-text");
697
1107
  const backdropEl = $("backdrop");
698
1108
  const menuToggleEl = $("menu-toggle");
@@ -731,8 +1141,7 @@
731
1141
  return res;
732
1142
  }
733
1143
 
734
- const savedContributor = localStorage.getItem("sv-contributor") || "";
735
- const savedFile = localStorage.getItem("sv-file") || "";
1144
+ const selectEl = $("contributor-select");
736
1145
 
737
1146
  function getExpandedKeys() {
738
1147
  try {
@@ -805,7 +1214,7 @@
805
1214
  li.dataset.key = nodePath;
806
1215
  const row = document.createElement("div");
807
1216
  row.className = "tree-row";
808
- row.style.paddingLeft = (depth * 12 + 8) + "px";
1217
+ row.style.paddingLeft = (depth * 14 + 12) + "px";
809
1218
 
810
1219
  if (isFile) {
811
1220
  const ctime = child.__file.createdAt || "";
@@ -819,7 +1228,7 @@
819
1228
  const expanded = getExpandedKeys().has(username + ":" + nodePath);
820
1229
  const arrow = expanded ? "&#9660;" : "&#9654;";
821
1230
  if (!expanded) sub.classList.add("collapsed");
822
- row.innerHTML = '<span class="name"><span class="arrow">' + arrow + '</span><i class="ph ph-folder"></i> ' + key + ' <span style="opacity:0.5">(' + fileCount + ')</span></span>';
1231
+ row.innerHTML = '<span class="name"><span class="arrow">' + arrow + '</span><i class="ph ph-folder"></i> ' + key + ' <span style="opacity:0.45;font-size:11px">' + fileCount + '</span></span>';
823
1232
  row.dataset.action = "toggle";
824
1233
  row.dataset.toggleKey = username + ":" + nodePath;
825
1234
  renderTree(child, sub, username, depth + 1, nodePath);
@@ -877,12 +1286,6 @@
877
1286
  rows[idx].click();
878
1287
  }
879
1288
 
880
- function getLoadedContributorList(username) {
881
- return filesEl.querySelector(
882
- 'ul[data-contributor="' + CSS.escape(username) + '"][data-loaded="true"]'
883
- );
884
- }
885
-
886
1289
  filesEl.addEventListener("click", (e) => {
887
1290
  const row = e.target.closest(".tree-row");
888
1291
  if (!row) return;
@@ -902,26 +1305,13 @@
902
1305
  else keys.add(toggleKey);
903
1306
  saveExpandedKeys(keys);
904
1307
  }
905
- } else if (action === "contributor") {
906
- const sub = row.nextElementSibling;
907
- if (!sub) return;
908
- const collapsed = sub.classList.contains("collapsed");
909
- const keys = getExpandedKeys();
910
- const toggleKey = "contributor:" + row.dataset.contributor;
911
- if (collapsed) {
912
- sub.classList.remove("collapsed");
913
- row.querySelector(".arrow").innerHTML = "&#9660;";
914
- keys.add(toggleKey);
915
- } else {
916
- sub.classList.add("collapsed");
917
- row.querySelector(".arrow").innerHTML = "&#9654;";
918
- keys.delete(toggleKey);
919
- }
920
- saveExpandedKeys(keys);
921
1308
  }
922
1309
  });
923
1310
 
924
- async function loadContributorFiles(username, sub, opts = {}) {
1311
+ async function loadSelectedContributor(opts = {}) {
1312
+ const username = selectEl.value;
1313
+ if (!username) return;
1314
+ localStorage.setItem("sv-contributor", username);
925
1315
  const silent = !!opts.silent;
926
1316
  if (!silent) status("Loading files...");
927
1317
  const { files } = await (await api("/v1/files?prefix=" + encodeURIComponent(username + "/"))).json();
@@ -931,63 +1321,79 @@
931
1321
  path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path,
932
1322
  })));
933
1323
  const tmp = document.createElement("ul");
934
- renderTree(tree, tmp, username, 1);
935
- if (sub.hasChildNodes()) {
936
- morphdom(sub, tmp, {
1324
+ renderTree(tree, tmp, username, 0);
1325
+ if (filesEl.hasChildNodes()) {
1326
+ morphdom(filesEl, tmp, {
937
1327
  childrenOnly: true,
938
1328
  getNodeKey(node) {
939
1329
  return node.dataset?.key || "";
940
1330
  },
941
1331
  });
942
1332
  } else {
943
- sub.append(...tmp.childNodes);
1333
+ filesEl.append(...tmp.childNodes);
944
1334
  }
945
1335
  markActiveRow();
946
- if (!silent) status(files.length + " file(s)");
947
- const row = sub.parentElement && sub.parentElement.querySelector(".tree-row");
948
- if (row) {
949
- const arrow = row.querySelector(".arrow").outerHTML;
950
- row.innerHTML = '<span class="name">' + arrow + '<i class="ph ph-user"></i> ' + username + ' <span style="opacity:0.5">(' + files.length + ')</span></span>';
951
- }
1336
+ status(files.length + " file(s)");
952
1337
  }
953
1338
 
954
1339
  async function loadContributors() {
955
1340
  filesEl.innerHTML = "";
956
- contentEl.innerHTML = "";
1341
+ contentEl.innerHTML = emptyStateHTML;
1342
+ $("item-header").hidden = true;
957
1343
  status("Loading contributors...");
958
1344
  const { contributors } = await (await api("/v1/contributors")).json();
959
- const expandedKeys = getExpandedKeys();
960
1345
 
1346
+ selectEl.innerHTML = "";
961
1347
  for (const b of contributors) {
962
- const li = document.createElement("li");
963
- const row = document.createElement("div");
964
- row.className = "tree-row";
965
- row.style.paddingLeft = "8px";
966
- const sub = document.createElement("ul");
967
- sub.dataset.contributor = b.username;
968
- sub.dataset.loaded = "true";
969
- const expanded = expandedKeys.has("contributor:" + b.username);
970
- if (!expanded) sub.classList.add("collapsed");
971
- const arrow = expanded ? "&#9660;" : "&#9654;";
972
- row.innerHTML = '<span class="name"><span class="arrow">' + arrow + '</span><i class="ph ph-user"></i> ' + b.username + '</span>';
973
- row.dataset.action = "contributor";
974
- row.dataset.contributor = b.username;
975
- li.appendChild(row);
976
- li.appendChild(sub);
977
- filesEl.appendChild(li);
1348
+ const opt = document.createElement("option");
1349
+ opt.value = b.username;
1350
+ opt.textContent = b.username;
1351
+ selectEl.appendChild(opt);
978
1352
  }
1353
+ selectEl.disabled = contributors.length === 0;
979
1354
 
980
- await Promise.all(contributors.map((b) => {
981
- const sub = getLoadedContributorList(b.username);
982
- return sub ? loadContributorFiles(b.username, sub, { silent: true }) : null;
983
- }));
1355
+ const saved = localStorage.getItem("sv-contributor");
1356
+ if (saved && contributors.some((c) => c.username === saved)) {
1357
+ selectEl.value = saved;
1358
+ } else if (contributors.length > 0) {
1359
+ selectEl.value = contributors[0].username;
1360
+ }
984
1361
 
985
- if (savedFile && savedContributor) {
1362
+ await loadSelectedContributor();
1363
+
1364
+ const savedFile = localStorage.getItem("sv-file");
1365
+ if (savedFile) {
986
1366
  const match = filesEl.querySelector('[data-path="' + CSS.escape(savedFile) + '"]');
987
- if (match) await loadContent(savedContributor, savedFile, match);
1367
+ if (match) await loadContent(selectEl.value, savedFile, match);
988
1368
  }
1369
+ }
989
1370
 
990
- status(contributors.length + " contributor(s)");
1371
+ selectEl.addEventListener("change", () => {
1372
+ contentEl.innerHTML = emptyStateHTML;
1373
+ $("item-header").hidden = true;
1374
+ localStorage.removeItem("sv-file");
1375
+ loadSelectedContributor().catch((e) => status(e.message));
1376
+ });
1377
+
1378
+ function escapeHtml(str) {
1379
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1380
+ }
1381
+
1382
+ function renderMeta(meta) {
1383
+ const skip = new Set(["title"]);
1384
+ const parts = [];
1385
+ for (const [key, val] of Object.entries(meta)) {
1386
+ if (skip.has(key) || val == null) continue;
1387
+ const label = '<span class="meta-label">' + escapeHtml(key) + '</span>';
1388
+ let value;
1389
+ if (Array.isArray(val)) {
1390
+ value = val.map((v) => '<span class="meta-tag">' + escapeHtml(v) + "</span>").join(" ");
1391
+ } else {
1392
+ value = '<span class="meta-value">' + escapeHtml(val) + "</span>";
1393
+ }
1394
+ parts.push('<span class="meta-field">' + label + " " + value + "</span>");
1395
+ }
1396
+ return parts.join("");
991
1397
  }
992
1398
 
993
1399
  async function loadContent(username, path, row) {
@@ -1001,8 +1407,18 @@
1001
1407
  const encodedPath = path.split("/").map(encodeURIComponent).join("/");
1002
1408
  const res = await api("/v1/files/" + encodeURIComponent(username) + "/" + encodedPath);
1003
1409
  const text = await res.text();
1004
- contentEl.innerHTML = renderMarkdown(text);
1005
- contentEl.parentElement.scrollTop = 0;
1410
+ const { html, meta } = renderMarkdown(text);
1411
+ const headerEl = $("item-header");
1412
+ const titleEl = $("item-title");
1413
+ const pathEl = $("item-path");
1414
+ const metaEl = $("item-meta");
1415
+ const fileName = path.split("/").pop().replace(/\.md$/, "");
1416
+ titleEl.textContent = meta.title || fileName;
1417
+ pathEl.textContent = username + ":" + path;
1418
+ metaEl.innerHTML = renderMeta(meta);
1419
+ headerEl.hidden = false;
1420
+ contentEl.innerHTML = html;
1421
+ $("content").scrollTop = 0;
1006
1422
  status(path);
1007
1423
  }
1008
1424
 
@@ -1017,16 +1433,15 @@
1017
1433
  const contributorReloadTimers = new Map();
1018
1434
 
1019
1435
  function scheduleContributorReload(username) {
1020
- if (!getLoadedContributorList(username)) return;
1436
+ if (username !== selectEl.value) return;
1021
1437
 
1022
1438
  const existing = contributorReloadTimers.get(username);
1023
1439
  if (existing) clearTimeout(existing);
1024
1440
 
1025
1441
  const timer = setTimeout(() => {
1026
1442
  contributorReloadTimers.delete(username);
1027
- const targetUl = getLoadedContributorList(username);
1028
- if (!targetUl) return;
1029
- loadContributorFiles(username, targetUl, { silent: true }).catch((e) => status(e.message));
1443
+ if (username !== selectEl.value) return;
1444
+ loadSelectedContributor({ silent: true }).catch((e) => status(e.message));
1030
1445
  }, 100);
1031
1446
 
1032
1447
  contributorReloadTimers.set(username, timer);
@@ -1041,21 +1456,28 @@
1041
1456
  evtSource.addEventListener("file_updated", (e) => {
1042
1457
  const { contributor, path } = JSON.parse(e.data);
1043
1458
  scheduleContributorReload(contributor);
1044
- // If this file is currently open, reload its content
1045
1459
  if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
1046
1460
  const encodedPath = path.split("/").map(encodeURIComponent).join("/");
1047
1461
  api("/v1/files/" + encodeURIComponent(contributor) + "/" + encodedPath)
1048
1462
  .then((res) => res.text())
1049
- .then((text) => { contentEl.innerHTML = renderMarkdown(text); });
1463
+ .then((text) => {
1464
+ const { html, meta } = renderMarkdown(text);
1465
+ const fileName = path.split("/").pop().replace(/\.md$/, "");
1466
+ $("item-title").textContent = meta.title || fileName;
1467
+ $("item-path").textContent = contributor + ":" + path;
1468
+ $("item-meta").innerHTML = renderMeta(meta);
1469
+ $("item-header").hidden = false;
1470
+ contentEl.innerHTML = html;
1471
+ });
1050
1472
  }
1051
1473
  });
1052
1474
 
1053
1475
  evtSource.addEventListener("file_deleted", (e) => {
1054
1476
  const { contributor, path } = JSON.parse(e.data);
1055
1477
  scheduleContributorReload(contributor);
1056
- // If this file was being viewed, clear the content
1057
1478
  if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
1058
- contentEl.innerHTML = "";
1479
+ $("item-header").hidden = true;
1480
+ contentEl.innerHTML = emptyStateHTML;
1059
1481
  status("File deleted: " + path);
1060
1482
  }
1061
1483
  });
@@ -1227,12 +1649,12 @@
1227
1649
 
1228
1650
  return '<li class="activity-item">' +
1229
1651
  '<div class="activity-time-col">' +
1230
- '<div class="activity-time">' + time + '</div>' +
1231
- '<div class="activity-date">' + date + '</div>' +
1652
+ '<div class="activity-time">' + time + '</div>' +
1653
+ '<div class="activity-date">' + date + '</div>' +
1232
1654
  '</div>' +
1233
1655
  '<div class="activity-dot-col">' +
1234
- '<div class="activity-dot"></div>' +
1235
- '<div class="activity-line"></div>' +
1656
+ '<div class="activity-dot"></div>' +
1657
+ '<div class="activity-line"></div>' +
1236
1658
  '</div>' +
1237
1659
  '<div class="activity-content">' + content + '</div>' +
1238
1660
  '</li>';
@@ -1261,7 +1683,7 @@
1261
1683
  }
1262
1684
 
1263
1685
  if (activitySeenIds.size === 0) {
1264
- activityBody.innerHTML = '<p style="padding:12px;opacity:0.5;font-size:12px">No activity.</p>';
1686
+ activityBody.innerHTML = '<p style="padding:16px;color:var(--text-tertiary);font-size:13px">No activity yet.</p>';
1265
1687
  return;
1266
1688
  }
1267
1689
 
@@ -1278,4 +1700,4 @@
1278
1700
  </script>
1279
1701
  </body>
1280
1702
 
1281
- </html>
1703
+ </html>