@seedvault/server 0.1.5 → 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 +1073 -231
- package/dist/server.js +241 -7
- package/package.json +1 -1
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
color: rgb(204, 204, 204);
|
|
32
|
-
}
|
|
70
|
+
/* ── Reset & Base ── */
|
|
33
71
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
99
|
+
/* ── Top Bar ── */
|
|
100
|
+
|
|
101
|
+
.top-bar {
|
|
51
102
|
display: flex;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
font:
|
|
62
|
-
padding:
|
|
63
|
-
background:
|
|
64
|
-
color:
|
|
65
|
-
border: 1px solid
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
border: none;
|
|
217
|
+
padding: 4px;
|
|
87
218
|
font-size: 16px;
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
275
|
+
background: var(--accent);
|
|
276
|
+
opacity: 0.3;
|
|
130
277
|
}
|
|
131
278
|
|
|
132
|
-
|
|
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:
|
|
180
|
-
border-bottom: 1px solid
|
|
181
|
-
font-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,204 +381,729 @@
|
|
|
250
381
|
.tree-row .ctime {
|
|
251
382
|
flex-shrink: 0;
|
|
252
383
|
margin-left: 8px;
|
|
253
|
-
|
|
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:
|
|
390
|
+
background: var(--surface-hover);
|
|
259
391
|
}
|
|
260
392
|
|
|
261
393
|
.tree-row.active {
|
|
262
|
-
background:
|
|
263
|
-
color:
|
|
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:
|
|
276
|
-
margin-right:
|
|
277
|
-
vertical-align: -0.
|
|
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:
|
|
529
|
+
font-family: var(--font-body);
|
|
286
530
|
font-size: 16px;
|
|
287
|
-
padding:
|
|
288
|
-
|
|
289
|
-
|
|
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:
|
|
294
|
-
font-weight:
|
|
295
|
-
margin:
|
|
296
|
-
line-height:
|
|
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.
|
|
547
|
+
font-size: 1.6rem;
|
|
301
548
|
font-weight: 600;
|
|
302
|
-
margin:
|
|
303
|
-
line-height:
|
|
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.
|
|
555
|
+
font-size: 1.2rem;
|
|
308
556
|
font-weight: 600;
|
|
309
|
-
margin:
|
|
310
|
-
line-height:
|
|
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:
|
|
565
|
+
font-size: 1em;
|
|
317
566
|
font-weight: 600;
|
|
318
|
-
margin: 0.
|
|
567
|
+
margin: 0.8em 0 0.4em;
|
|
319
568
|
}
|
|
320
569
|
|
|
321
570
|
#content p {
|
|
322
|
-
margin: 0 0 0.
|
|
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.
|
|
334
|
-
padding-left: 1.
|
|
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:
|
|
340
|
-
font-size: 0.
|
|
593
|
+
font-family: var(--font-mono);
|
|
594
|
+
font-size: 0.88em;
|
|
341
595
|
}
|
|
342
596
|
|
|
343
597
|
#content pre {
|
|
344
|
-
background:
|
|
345
|
-
padding:
|
|
346
|
-
border-radius:
|
|
598
|
+
background: var(--code-bg);
|
|
599
|
+
padding: 14px 16px;
|
|
600
|
+
border-radius: var(--radius);
|
|
347
601
|
overflow-x: auto;
|
|
348
|
-
margin: 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.
|
|
353
|
-
border-radius:
|
|
354
|
-
background:
|
|
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:
|
|
364
|
-
margin: 0.
|
|
365
|
-
padding
|
|
366
|
-
color:
|
|
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:
|
|
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
|
|
377
|
-
margin:
|
|
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.
|
|
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
|
|
388
|
-
padding:
|
|
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
|
-
|
|
675
|
+
padding: 0 16px;
|
|
676
|
+
height: 32px;
|
|
394
677
|
font-size: 11px;
|
|
395
|
-
|
|
678
|
+
color: var(--text-tertiary);
|
|
679
|
+
flex-shrink: 0;
|
|
680
|
+
display: flex;
|
|
681
|
+
align-items: center;
|
|
682
|
+
border-top: 1px solid var(--border-subtle);
|
|
683
|
+
background: var(--surface);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
#status-text {
|
|
687
|
+
flex: 1;
|
|
688
|
+
font-family: var(--font-mono);
|
|
689
|
+
font-size: 11px;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#activity-btn {
|
|
693
|
+
font-size: 11px;
|
|
694
|
+
font-weight: 500;
|
|
695
|
+
padding: 4px 8px;
|
|
696
|
+
border-radius: var(--radius-sm);
|
|
697
|
+
color: var(--text-secondary);
|
|
698
|
+
display: flex;
|
|
699
|
+
align-items: center;
|
|
700
|
+
gap: 4px;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#activity-btn:hover {
|
|
704
|
+
color: var(--text);
|
|
705
|
+
background: var(--surface-hover);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* ── Activity Panel ── */
|
|
709
|
+
|
|
710
|
+
#activity-modal {
|
|
711
|
+
display: none;
|
|
712
|
+
position: fixed;
|
|
713
|
+
inset: 0;
|
|
714
|
+
z-index: 300;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
#activity-modal.open {
|
|
718
|
+
display: flex;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#activity-overlay {
|
|
722
|
+
position: absolute;
|
|
723
|
+
inset: 0;
|
|
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;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#activity-dialog {
|
|
731
|
+
position: absolute;
|
|
732
|
+
inset: 0;
|
|
733
|
+
background: var(--surface);
|
|
734
|
+
color: var(--text);
|
|
735
|
+
display: flex;
|
|
736
|
+
flex-direction: column;
|
|
737
|
+
overflow: hidden;
|
|
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
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
#activity-dialog-head {
|
|
754
|
+
padding: 10px 16px;
|
|
755
|
+
border-bottom: 1px solid var(--border);
|
|
756
|
+
font-size: 12px;
|
|
757
|
+
font-weight: 600;
|
|
758
|
+
display: flex;
|
|
759
|
+
align-items: center;
|
|
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);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
#activity-dialog-head button {
|
|
776
|
+
margin-left: auto;
|
|
777
|
+
padding: 4px 6px;
|
|
778
|
+
font-size: 16px;
|
|
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);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#activity-dialog-body {
|
|
789
|
+
flex: 1;
|
|
790
|
+
overflow-y: auto;
|
|
791
|
+
scrollbar-width: thin;
|
|
792
|
+
scrollbar-color: var(--border) transparent;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.activity-list {
|
|
796
|
+
padding: 12px 0;
|
|
797
|
+
margin: 0;
|
|
798
|
+
list-style: none;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.activity-item {
|
|
802
|
+
display: grid;
|
|
803
|
+
grid-template-columns: 90px 20px 1fr;
|
|
804
|
+
gap: 0 8px;
|
|
805
|
+
min-height: 40px;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.activity-time-col {
|
|
809
|
+
text-align: right;
|
|
810
|
+
padding-top: 6px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.activity-time {
|
|
814
|
+
font-size: 11px;
|
|
815
|
+
font-family: var(--font-mono);
|
|
816
|
+
color: var(--text-secondary);
|
|
817
|
+
white-space: nowrap;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.activity-date {
|
|
821
|
+
font-size: 10px;
|
|
822
|
+
color: var(--text-tertiary);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.activity-dot-col {
|
|
826
|
+
position: relative;
|
|
827
|
+
display: flex;
|
|
828
|
+
flex-direction: column;
|
|
829
|
+
align-items: center;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.activity-dot-col::after {
|
|
833
|
+
content: '';
|
|
834
|
+
position: absolute;
|
|
835
|
+
left: 50%;
|
|
836
|
+
top: 0;
|
|
837
|
+
bottom: 0;
|
|
838
|
+
width: 1px;
|
|
839
|
+
transform: translateX(-50%);
|
|
840
|
+
background: var(--border);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.activity-item:first-child .activity-dot-col::after {
|
|
844
|
+
top: 13px;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.activity-item:last-child .activity-dot-col::after {
|
|
848
|
+
bottom: calc(100% - 14px);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.activity-dot {
|
|
852
|
+
width: 6px;
|
|
853
|
+
height: 6px;
|
|
854
|
+
border-radius: 50%;
|
|
855
|
+
background: var(--accent);
|
|
856
|
+
opacity: 0.5;
|
|
396
857
|
flex-shrink: 0;
|
|
858
|
+
margin-top: 10px;
|
|
859
|
+
position: relative;
|
|
860
|
+
z-index: 1;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.activity-line {
|
|
864
|
+
display: none;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.activity-content {
|
|
868
|
+
padding: 6px 16px 8px 0;
|
|
869
|
+
min-width: 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.activity-action {
|
|
873
|
+
font-family: var(--font-mono);
|
|
874
|
+
font-size: 11px;
|
|
875
|
+
font-weight: 500;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.activity-by {
|
|
879
|
+
font-size: 11px;
|
|
880
|
+
color: var(--text-tertiary);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.activity-contributor {
|
|
884
|
+
font-weight: 600;
|
|
885
|
+
color: var(--text-secondary);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.activity-detail {
|
|
889
|
+
margin-top: 3px;
|
|
890
|
+
font-size: 11px;
|
|
891
|
+
color: var(--text-tertiary);
|
|
892
|
+
font-family: var(--font-mono);
|
|
893
|
+
word-break: break-all;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.activity-diff {
|
|
897
|
+
margin-top: 6px;
|
|
898
|
+
font-family: var(--font-mono);
|
|
899
|
+
font-size: 11px;
|
|
900
|
+
line-height: 1.45;
|
|
901
|
+
white-space: pre-wrap;
|
|
902
|
+
word-break: break-all;
|
|
903
|
+
padding: 8px 10px;
|
|
904
|
+
background: var(--code-bg);
|
|
905
|
+
border-radius: var(--radius-sm);
|
|
906
|
+
border: 1px solid var(--border-subtle);
|
|
907
|
+
overflow-x: auto;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.activity-diff .diff-add {
|
|
911
|
+
color: var(--diff-add);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.activity-diff .diff-del {
|
|
915
|
+
color: var(--diff-del);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.activity-diff .diff-hunk {
|
|
919
|
+
color: var(--diff-hunk);
|
|
920
|
+
opacity: 0.7;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.activity-diff-truncated {
|
|
924
|
+
font-size: 10px;
|
|
925
|
+
color: var(--text-tertiary);
|
|
926
|
+
font-style: italic;
|
|
927
|
+
margin-top: 2px;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
#activity-load-more {
|
|
931
|
+
width: 100%;
|
|
932
|
+
padding: 10px;
|
|
933
|
+
border-top: 1px solid var(--border-subtle);
|
|
934
|
+
font-size: 11px;
|
|
935
|
+
font-weight: 500;
|
|
936
|
+
text-transform: uppercase;
|
|
937
|
+
letter-spacing: 0.06em;
|
|
938
|
+
color: var(--text-tertiary);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
#activity-load-more:hover {
|
|
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
|
+
}
|
|
397
1017
|
}
|
|
398
1018
|
</style>
|
|
399
1019
|
</head>
|
|
400
1020
|
|
|
401
1021
|
<body>
|
|
402
|
-
<
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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>
|
|
407
1034
|
<div id="backdrop"></div>
|
|
408
1035
|
<div class="grid">
|
|
409
|
-
<section id="nav" class="panel" style="width:
|
|
410
|
-
<div class="panel-head">Files
|
|
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>
|
|
411
1039
|
<div class="panel-body" id="nav-body" tabindex="0">
|
|
412
1040
|
<ul id="files" class="tree"></ul>
|
|
413
1041
|
</div>
|
|
414
1042
|
</section>
|
|
415
1043
|
<div id="divider" class="divider"></div>
|
|
416
1044
|
<section class="panel">
|
|
417
|
-
<div class="panel-head">Content</div>
|
|
418
1045
|
<div class="panel-body">
|
|
419
|
-
<div id="content"
|
|
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>
|
|
420
1059
|
</div>
|
|
421
1060
|
</section>
|
|
422
1061
|
</div>
|
|
423
|
-
<div id="
|
|
1062
|
+
<div id="activity-modal">
|
|
1063
|
+
<div id="activity-overlay"></div>
|
|
1064
|
+
<div id="activity-dialog">
|
|
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>
|
|
1067
|
+
<div id="activity-dialog-body"></div>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
<footer id="status">
|
|
1071
|
+
<span id="status-text"></span>
|
|
1072
|
+
<button id="activity-btn" aria-label="Activity log"><i class="ph ph-clock-counter-clockwise"></i>
|
|
1073
|
+
Activity</button>
|
|
1074
|
+
</footer>
|
|
424
1075
|
<script type="module">
|
|
425
1076
|
const { marked } = await import("https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js");
|
|
426
1077
|
const matter = (await import("https://cdn.jsdelivr.net/npm/gray-matter@4.0.3/+esm")).default;
|
|
427
1078
|
const DOMPurify = (await import("https://cdn.jsdelivr.net/npm/dompurify@3.0.9/+esm")).default;
|
|
428
1079
|
const morphdom = (await import("https://cdn.jsdelivr.net/npm/morphdom@2.7.4/+esm")).default;
|
|
429
1080
|
|
|
1081
|
+
const emptyStateHTML = '<div class="empty-state"><i class="ph ph-plant"></i><p>Select a file to view</p></div>';
|
|
1082
|
+
|
|
430
1083
|
function renderMarkdown(raw) {
|
|
431
|
-
if (typeof raw !== "string") return "";
|
|
1084
|
+
if (typeof raw !== "string") return { html: "", meta: {} };
|
|
432
1085
|
try {
|
|
433
|
-
const { content } = matter(raw);
|
|
1086
|
+
const { content, data } = matter(raw);
|
|
434
1087
|
const html = marked.parse(content);
|
|
435
|
-
return
|
|
1088
|
+
return {
|
|
1089
|
+
html: DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }),
|
|
1090
|
+
meta: data || {},
|
|
1091
|
+
};
|
|
436
1092
|
} catch {
|
|
437
1093
|
const escaped = raw
|
|
438
1094
|
.replace(/&/g, "&")
|
|
439
1095
|
.replace(/</g, "<")
|
|
440
1096
|
.replace(/>/g, ">")
|
|
441
1097
|
.replace(/"/g, """);
|
|
442
|
-
return "<pre>" + escaped + "</pre>";
|
|
1098
|
+
return { html: "<pre>" + escaped + "</pre>", meta: {} };
|
|
443
1099
|
}
|
|
444
1100
|
}
|
|
445
1101
|
|
|
446
1102
|
const $ = (id) => document.getElementById(id);
|
|
447
1103
|
const tokenEl = $("token");
|
|
448
1104
|
const filesEl = $("files");
|
|
449
|
-
const contentEl = $("content");
|
|
450
|
-
const
|
|
1105
|
+
const contentEl = $("content-body");
|
|
1106
|
+
const statusTextEl = $("status-text");
|
|
451
1107
|
const backdropEl = $("backdrop");
|
|
452
1108
|
const menuToggleEl = $("menu-toggle");
|
|
453
1109
|
|
|
@@ -468,7 +1124,7 @@
|
|
|
468
1124
|
let token = localStorage.getItem("sv-token") || "";
|
|
469
1125
|
tokenEl.value = token;
|
|
470
1126
|
|
|
471
|
-
function status(msg) {
|
|
1127
|
+
function status(msg) { statusTextEl.textContent = msg; }
|
|
472
1128
|
|
|
473
1129
|
async function api(url, opts = {}) {
|
|
474
1130
|
const res = await fetch(url, {
|
|
@@ -485,8 +1141,7 @@
|
|
|
485
1141
|
return res;
|
|
486
1142
|
}
|
|
487
1143
|
|
|
488
|
-
const
|
|
489
|
-
const savedFile = localStorage.getItem("sv-file") || "";
|
|
1144
|
+
const selectEl = $("contributor-select");
|
|
490
1145
|
|
|
491
1146
|
function getExpandedKeys() {
|
|
492
1147
|
try {
|
|
@@ -559,7 +1214,7 @@
|
|
|
559
1214
|
li.dataset.key = nodePath;
|
|
560
1215
|
const row = document.createElement("div");
|
|
561
1216
|
row.className = "tree-row";
|
|
562
|
-
row.style.paddingLeft = (depth *
|
|
1217
|
+
row.style.paddingLeft = (depth * 14 + 12) + "px";
|
|
563
1218
|
|
|
564
1219
|
if (isFile) {
|
|
565
1220
|
const ctime = child.__file.createdAt || "";
|
|
@@ -573,7 +1228,7 @@
|
|
|
573
1228
|
const expanded = getExpandedKeys().has(username + ":" + nodePath);
|
|
574
1229
|
const arrow = expanded ? "▼" : "▶";
|
|
575
1230
|
if (!expanded) sub.classList.add("collapsed");
|
|
576
|
-
row.innerHTML = '<span class="name"><span class="arrow">' + arrow + '</span><i class="ph ph-folder"></i> ' + key + ' <span style="opacity:0.
|
|
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>';
|
|
577
1232
|
row.dataset.action = "toggle";
|
|
578
1233
|
row.dataset.toggleKey = username + ":" + nodePath;
|
|
579
1234
|
renderTree(child, sub, username, depth + 1, nodePath);
|
|
@@ -631,12 +1286,6 @@
|
|
|
631
1286
|
rows[idx].click();
|
|
632
1287
|
}
|
|
633
1288
|
|
|
634
|
-
function getLoadedContributorList(username) {
|
|
635
|
-
return filesEl.querySelector(
|
|
636
|
-
'ul[data-contributor="' + CSS.escape(username) + '"][data-loaded="true"]'
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
1289
|
filesEl.addEventListener("click", (e) => {
|
|
641
1290
|
const row = e.target.closest(".tree-row");
|
|
642
1291
|
if (!row) return;
|
|
@@ -656,26 +1305,13 @@
|
|
|
656
1305
|
else keys.add(toggleKey);
|
|
657
1306
|
saveExpandedKeys(keys);
|
|
658
1307
|
}
|
|
659
|
-
} else if (action === "contributor") {
|
|
660
|
-
const sub = row.nextElementSibling;
|
|
661
|
-
if (!sub) return;
|
|
662
|
-
const collapsed = sub.classList.contains("collapsed");
|
|
663
|
-
const keys = getExpandedKeys();
|
|
664
|
-
const toggleKey = "contributor:" + row.dataset.contributor;
|
|
665
|
-
if (collapsed) {
|
|
666
|
-
sub.classList.remove("collapsed");
|
|
667
|
-
row.querySelector(".arrow").innerHTML = "▼";
|
|
668
|
-
keys.add(toggleKey);
|
|
669
|
-
} else {
|
|
670
|
-
sub.classList.add("collapsed");
|
|
671
|
-
row.querySelector(".arrow").innerHTML = "▶";
|
|
672
|
-
keys.delete(toggleKey);
|
|
673
|
-
}
|
|
674
|
-
saveExpandedKeys(keys);
|
|
675
1308
|
}
|
|
676
1309
|
});
|
|
677
1310
|
|
|
678
|
-
async function
|
|
1311
|
+
async function loadSelectedContributor(opts = {}) {
|
|
1312
|
+
const username = selectEl.value;
|
|
1313
|
+
if (!username) return;
|
|
1314
|
+
localStorage.setItem("sv-contributor", username);
|
|
679
1315
|
const silent = !!opts.silent;
|
|
680
1316
|
if (!silent) status("Loading files...");
|
|
681
1317
|
const { files } = await (await api("/v1/files?prefix=" + encodeURIComponent(username + "/"))).json();
|
|
@@ -685,63 +1321,79 @@
|
|
|
685
1321
|
path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path,
|
|
686
1322
|
})));
|
|
687
1323
|
const tmp = document.createElement("ul");
|
|
688
|
-
renderTree(tree, tmp, username,
|
|
689
|
-
if (
|
|
690
|
-
morphdom(
|
|
1324
|
+
renderTree(tree, tmp, username, 0);
|
|
1325
|
+
if (filesEl.hasChildNodes()) {
|
|
1326
|
+
morphdom(filesEl, tmp, {
|
|
691
1327
|
childrenOnly: true,
|
|
692
1328
|
getNodeKey(node) {
|
|
693
1329
|
return node.dataset?.key || "";
|
|
694
1330
|
},
|
|
695
1331
|
});
|
|
696
1332
|
} else {
|
|
697
|
-
|
|
1333
|
+
filesEl.append(...tmp.childNodes);
|
|
698
1334
|
}
|
|
699
1335
|
markActiveRow();
|
|
700
|
-
|
|
701
|
-
const row = sub.parentElement && sub.parentElement.querySelector(".tree-row");
|
|
702
|
-
if (row) {
|
|
703
|
-
const arrow = row.querySelector(".arrow").outerHTML;
|
|
704
|
-
row.innerHTML = '<span class="name">' + arrow + '<i class="ph ph-user"></i> ' + username + ' <span style="opacity:0.5">(' + files.length + ')</span></span>';
|
|
705
|
-
}
|
|
1336
|
+
status(files.length + " file(s)");
|
|
706
1337
|
}
|
|
707
1338
|
|
|
708
1339
|
async function loadContributors() {
|
|
709
1340
|
filesEl.innerHTML = "";
|
|
710
|
-
contentEl.innerHTML =
|
|
1341
|
+
contentEl.innerHTML = emptyStateHTML;
|
|
1342
|
+
$("item-header").hidden = true;
|
|
711
1343
|
status("Loading contributors...");
|
|
712
1344
|
const { contributors } = await (await api("/v1/contributors")).json();
|
|
713
|
-
const expandedKeys = getExpandedKeys();
|
|
714
1345
|
|
|
1346
|
+
selectEl.innerHTML = "";
|
|
715
1347
|
for (const b of contributors) {
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
row.dataset.contributor = b.username;
|
|
729
|
-
li.appendChild(row);
|
|
730
|
-
li.appendChild(sub);
|
|
731
|
-
filesEl.appendChild(li);
|
|
1348
|
+
const opt = document.createElement("option");
|
|
1349
|
+
opt.value = b.username;
|
|
1350
|
+
opt.textContent = b.username;
|
|
1351
|
+
selectEl.appendChild(opt);
|
|
1352
|
+
}
|
|
1353
|
+
selectEl.disabled = contributors.length === 0;
|
|
1354
|
+
|
|
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;
|
|
732
1360
|
}
|
|
733
1361
|
|
|
734
|
-
await
|
|
735
|
-
const sub = getLoadedContributorList(b.username);
|
|
736
|
-
return sub ? loadContributorFiles(b.username, sub, { silent: true }) : null;
|
|
737
|
-
}));
|
|
1362
|
+
await loadSelectedContributor();
|
|
738
1363
|
|
|
739
|
-
|
|
1364
|
+
const savedFile = localStorage.getItem("sv-file");
|
|
1365
|
+
if (savedFile) {
|
|
740
1366
|
const match = filesEl.querySelector('[data-path="' + CSS.escape(savedFile) + '"]');
|
|
741
|
-
if (match) await loadContent(
|
|
1367
|
+
if (match) await loadContent(selectEl.value, savedFile, match);
|
|
742
1368
|
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
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
|
+
});
|
|
743
1377
|
|
|
744
|
-
|
|
1378
|
+
function escapeHtml(str) {
|
|
1379
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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("");
|
|
745
1397
|
}
|
|
746
1398
|
|
|
747
1399
|
async function loadContent(username, path, row) {
|
|
@@ -755,8 +1407,18 @@
|
|
|
755
1407
|
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
756
1408
|
const res = await api("/v1/files/" + encodeURIComponent(username) + "/" + encodedPath);
|
|
757
1409
|
const text = await res.text();
|
|
758
|
-
|
|
759
|
-
|
|
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;
|
|
760
1422
|
status(path);
|
|
761
1423
|
}
|
|
762
1424
|
|
|
@@ -771,16 +1433,15 @@
|
|
|
771
1433
|
const contributorReloadTimers = new Map();
|
|
772
1434
|
|
|
773
1435
|
function scheduleContributorReload(username) {
|
|
774
|
-
if (
|
|
1436
|
+
if (username !== selectEl.value) return;
|
|
775
1437
|
|
|
776
1438
|
const existing = contributorReloadTimers.get(username);
|
|
777
1439
|
if (existing) clearTimeout(existing);
|
|
778
1440
|
|
|
779
1441
|
const timer = setTimeout(() => {
|
|
780
1442
|
contributorReloadTimers.delete(username);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
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));
|
|
784
1445
|
}, 100);
|
|
785
1446
|
|
|
786
1447
|
contributorReloadTimers.set(username, timer);
|
|
@@ -795,25 +1456,36 @@
|
|
|
795
1456
|
evtSource.addEventListener("file_updated", (e) => {
|
|
796
1457
|
const { contributor, path } = JSON.parse(e.data);
|
|
797
1458
|
scheduleContributorReload(contributor);
|
|
798
|
-
// If this file is currently open, reload its content
|
|
799
1459
|
if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
|
|
800
1460
|
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
801
1461
|
api("/v1/files/" + encodeURIComponent(contributor) + "/" + encodedPath)
|
|
802
1462
|
.then((res) => res.text())
|
|
803
|
-
.then((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
|
+
});
|
|
804
1472
|
}
|
|
805
1473
|
});
|
|
806
1474
|
|
|
807
1475
|
evtSource.addEventListener("file_deleted", (e) => {
|
|
808
1476
|
const { contributor, path } = JSON.parse(e.data);
|
|
809
1477
|
scheduleContributorReload(contributor);
|
|
810
|
-
// If this file was being viewed, clear the content
|
|
811
1478
|
if (localStorage.getItem("sv-file") === path && localStorage.getItem("sv-contributor") === contributor) {
|
|
812
|
-
|
|
1479
|
+
$("item-header").hidden = true;
|
|
1480
|
+
contentEl.innerHTML = emptyStateHTML;
|
|
813
1481
|
status("File deleted: " + path);
|
|
814
1482
|
}
|
|
815
1483
|
});
|
|
816
1484
|
|
|
1485
|
+
evtSource.addEventListener("activity", (e) => {
|
|
1486
|
+
handleActivitySSE(e);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
817
1489
|
evtSource.onerror = () => {
|
|
818
1490
|
// EventSource auto-reconnects
|
|
819
1491
|
};
|
|
@@ -855,7 +1527,177 @@
|
|
|
855
1527
|
document.addEventListener("mousemove", onMove);
|
|
856
1528
|
document.addEventListener("mouseup", onUp);
|
|
857
1529
|
});
|
|
1530
|
+
|
|
1531
|
+
// --- Activity modal ---
|
|
1532
|
+
|
|
1533
|
+
const activityModal = $("activity-modal");
|
|
1534
|
+
const activityBody = $("activity-dialog-body");
|
|
1535
|
+
const ACTIVITY_PAGE_SIZE = 1000;
|
|
1536
|
+
let activitySeenIds = new Set();
|
|
1537
|
+
let activityOffset = 0;
|
|
1538
|
+
let activityHasMore = false;
|
|
1539
|
+
|
|
1540
|
+
function openActivityModal() {
|
|
1541
|
+
activitySeenIds = new Set();
|
|
1542
|
+
activityOffset = 0;
|
|
1543
|
+
activityBody.innerHTML = "";
|
|
1544
|
+
activityModal.classList.add("open");
|
|
1545
|
+
loadActivityPage();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function closeActivityModal() {
|
|
1549
|
+
activityModal.classList.remove("open");
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function handleActivitySSE(ev) {
|
|
1553
|
+
if (!activityModal.classList.contains("open")) return;
|
|
1554
|
+
const event = JSON.parse(ev.detail || ev.data);
|
|
1555
|
+
if (activitySeenIds.has(event.id)) return;
|
|
1556
|
+
activitySeenIds.add(event.id);
|
|
1557
|
+
activityOffset++;
|
|
1558
|
+
let list = activityBody.querySelector(".activity-list");
|
|
1559
|
+
if (!list) {
|
|
1560
|
+
const empty = activityBody.querySelector("p");
|
|
1561
|
+
if (empty) empty.remove();
|
|
1562
|
+
list = document.createElement("ul");
|
|
1563
|
+
list.className = "activity-list";
|
|
1564
|
+
activityBody.prepend(list);
|
|
1565
|
+
}
|
|
1566
|
+
list.insertAdjacentHTML("afterbegin", renderActivityItems([event]));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
$("activity-btn").addEventListener("click", openActivityModal);
|
|
1570
|
+
$("activity-close").addEventListener("click", closeActivityModal);
|
|
1571
|
+
$("activity-overlay").addEventListener("click", closeActivityModal);
|
|
1572
|
+
|
|
1573
|
+
document.addEventListener("keydown", (e) => {
|
|
1574
|
+
if (e.key === "Escape" && activityModal.classList.contains("open")) {
|
|
1575
|
+
closeActivityModal();
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
function formatActivityTime(iso) {
|
|
1580
|
+
if (!iso) return { time: "", date: "" };
|
|
1581
|
+
const d = new Date(iso);
|
|
1582
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
1583
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
1584
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
1585
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
1586
|
+
const mon = d.toLocaleString(undefined, { month: "short" });
|
|
1587
|
+
const day = d.getDate();
|
|
1588
|
+
const yr = d.getFullYear();
|
|
1589
|
+
const now = new Date();
|
|
1590
|
+
const dateStr = yr === now.getFullYear()
|
|
1591
|
+
? mon + " " + day
|
|
1592
|
+
: mon + " " + day + ", " + yr;
|
|
1593
|
+
return {
|
|
1594
|
+
time: hh + ":" + mm + ":" + ss + "." + ms,
|
|
1595
|
+
date: dateStr,
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function parseDetail(raw) {
|
|
1600
|
+
if (!raw) return null;
|
|
1601
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function esc(str) {
|
|
1605
|
+
return String(str)
|
|
1606
|
+
.replace(/&/g, "&")
|
|
1607
|
+
.replace(/</g, "<")
|
|
1608
|
+
.replace(/>/g, ">")
|
|
1609
|
+
.replace(/"/g, """);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function formatDetail(detail) {
|
|
1613
|
+
if (!detail || typeof detail !== "object") return "";
|
|
1614
|
+
return Object.entries(detail)
|
|
1615
|
+
.filter(([k]) => k !== "diff" && k !== "diff_truncated")
|
|
1616
|
+
.map(([k, v]) => k + "=" + v)
|
|
1617
|
+
.join(" ");
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function renderDiff(raw) {
|
|
1621
|
+
if (!raw) return "";
|
|
1622
|
+
const lines = raw.split("\n");
|
|
1623
|
+
const htmlLines = lines.map((line) => {
|
|
1624
|
+
const escaped = esc(line);
|
|
1625
|
+
if (line.startsWith("@@")) return '<span class="diff-hunk">' + escaped + '</span>';
|
|
1626
|
+
if (line.startsWith("+")) return '<span class="diff-add">' + escaped + '</span>';
|
|
1627
|
+
if (line.startsWith("-")) return '<span class="diff-del">' + escaped + '</span>';
|
|
1628
|
+
return escaped;
|
|
1629
|
+
});
|
|
1630
|
+
return htmlLines.join("\n");
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function renderActivityItems(events) {
|
|
1634
|
+
return events.map((ev) => {
|
|
1635
|
+
const { time, date } = formatActivityTime(ev.created_at);
|
|
1636
|
+
const parsed = parseDetail(ev.detail);
|
|
1637
|
+
const detail = formatDetail(parsed);
|
|
1638
|
+
const diff = parsed?.diff || null;
|
|
1639
|
+
const truncated = parsed?.diff_truncated || false;
|
|
1640
|
+
|
|
1641
|
+
let content = '<span class="activity-action">' + esc(ev.action) + '</span>' +
|
|
1642
|
+
' <span class="activity-by">by</span>' +
|
|
1643
|
+
' <span class="activity-contributor">' + esc(ev.contributor) + '</span>';
|
|
1644
|
+
if (detail) content += '<div class="activity-detail">' + esc(detail) + '</div>';
|
|
1645
|
+
if (diff) {
|
|
1646
|
+
content += '<div class="activity-diff">' + renderDiff(diff) + '</div>';
|
|
1647
|
+
if (truncated) content += '<span class="activity-diff-truncated">diff truncated</span>';
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
return '<li class="activity-item">' +
|
|
1651
|
+
'<div class="activity-time-col">' +
|
|
1652
|
+
'<div class="activity-time">' + time + '</div>' +
|
|
1653
|
+
'<div class="activity-date">' + date + '</div>' +
|
|
1654
|
+
'</div>' +
|
|
1655
|
+
'<div class="activity-dot-col">' +
|
|
1656
|
+
'<div class="activity-dot"></div>' +
|
|
1657
|
+
'<div class="activity-line"></div>' +
|
|
1658
|
+
'</div>' +
|
|
1659
|
+
'<div class="activity-content">' + content + '</div>' +
|
|
1660
|
+
'</li>';
|
|
1661
|
+
}).join("");
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async function loadActivityPage() {
|
|
1665
|
+
const existingBtn = activityBody.querySelector("#activity-load-more");
|
|
1666
|
+
if (existingBtn) existingBtn.remove();
|
|
1667
|
+
|
|
1668
|
+
const limit = ACTIVITY_PAGE_SIZE;
|
|
1669
|
+
const offset = activityOffset;
|
|
1670
|
+
const url = "/v1/activity?limit=" + (limit + 1) + "&offset=" + offset;
|
|
1671
|
+
const { events } = await (await api(url)).json();
|
|
1672
|
+
|
|
1673
|
+
activityHasMore = events.length > limit;
|
|
1674
|
+
const page = activityHasMore ? events.slice(0, limit) : events;
|
|
1675
|
+
for (const ev of page) activitySeenIds.add(ev.id);
|
|
1676
|
+
activityOffset += page.length;
|
|
1677
|
+
|
|
1678
|
+
let list = activityBody.querySelector(".activity-list");
|
|
1679
|
+
if (!list) {
|
|
1680
|
+
list = document.createElement("ul");
|
|
1681
|
+
list.className = "activity-list";
|
|
1682
|
+
activityBody.appendChild(list);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (activitySeenIds.size === 0) {
|
|
1686
|
+
activityBody.innerHTML = '<p style="padding:16px;color:var(--text-tertiary);font-size:13px">No activity yet.</p>';
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
list.insertAdjacentHTML("beforeend", renderActivityItems(page));
|
|
1691
|
+
|
|
1692
|
+
if (activityHasMore) {
|
|
1693
|
+
const btn = document.createElement("button");
|
|
1694
|
+
btn.id = "activity-load-more";
|
|
1695
|
+
btn.textContent = "Load more";
|
|
1696
|
+
btn.addEventListener("click", () => loadActivityPage());
|
|
1697
|
+
activityBody.appendChild(btn);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
858
1700
|
</script>
|
|
859
1701
|
</body>
|
|
860
1702
|
|
|
861
|
-
</html>
|
|
1703
|
+
</html>
|