skimmd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/bin/skimmd.js +379 -0
- package/package.json +44 -0
- package/public/index.html +1107 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>skimmd</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url("https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&family=Source+Serif+4:opsz,wght@8..60,400;600&family=JetBrains+Mono:wght@400;600&display=swap");
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
color-scheme: light;
|
|
12
|
+
--bg: #f7f6f3;
|
|
13
|
+
--bg-strong: #f1f1ee;
|
|
14
|
+
--panel: #ffffff;
|
|
15
|
+
--panel-strong: #ffffff;
|
|
16
|
+
--ink: #2f3437;
|
|
17
|
+
--muted: #787774;
|
|
18
|
+
--accent: #2f6fed;
|
|
19
|
+
--accent-strong: #1d4ed8;
|
|
20
|
+
--accent-soft: #e9f0ff;
|
|
21
|
+
--border: #e3e3e0;
|
|
22
|
+
--border-strong: #d7d7d2;
|
|
23
|
+
--code-bg: #f7f6f3;
|
|
24
|
+
--code-ink: #2f3437;
|
|
25
|
+
--shadow: 0 10px 24px rgba(15, 15, 14, 0.08);
|
|
26
|
+
--shadow-soft: 0 6px 14px rgba(15, 15, 14, 0.06);
|
|
27
|
+
--radius: 12px;
|
|
28
|
+
--radius-sm: 8px;
|
|
29
|
+
--font-ui: "Source Sans 3", "Trebuchet MS", sans-serif;
|
|
30
|
+
--font-body: "Source Sans 3", "Trebuchet MS", sans-serif;
|
|
31
|
+
--font-mono: "JetBrains Mono", "SFMono-Regular", monospace;
|
|
32
|
+
--hover-bg: #efefec;
|
|
33
|
+
--toc-active: #e8e7e4;
|
|
34
|
+
--folder-bg: #fbfbfa;
|
|
35
|
+
--pill-bg: #ededeb;
|
|
36
|
+
--inline-code-bg: #f1f1ef;
|
|
37
|
+
--inline-code-ink: #2f3437;
|
|
38
|
+
--blockquote-bg: #fbfbfa;
|
|
39
|
+
--editing-bg: #fbfbfa;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[data-theme="dark"] {
|
|
43
|
+
color-scheme: dark;
|
|
44
|
+
--bg: #1a1a1a;
|
|
45
|
+
--bg-strong: #222222;
|
|
46
|
+
--panel: #252525;
|
|
47
|
+
--panel-strong: #2a2a2a;
|
|
48
|
+
--ink: #e4e4e4;
|
|
49
|
+
--muted: #888888;
|
|
50
|
+
--accent: #5b9aff;
|
|
51
|
+
--accent-strong: #7db0ff;
|
|
52
|
+
--accent-soft: #1e3a5f;
|
|
53
|
+
--border: #3a3a3a;
|
|
54
|
+
--border-strong: #4a4a4a;
|
|
55
|
+
--code-bg: #2a2a2a;
|
|
56
|
+
--code-ink: #e4e4e4;
|
|
57
|
+
--shadow: 0 10px 24px rgba(0, 0, 0, 0.3);
|
|
58
|
+
--shadow-soft: 0 6px 14px rgba(0, 0, 0, 0.2);
|
|
59
|
+
--hover-bg: #333333;
|
|
60
|
+
--toc-active: #3a3a3a;
|
|
61
|
+
--folder-bg: #2a2a2a;
|
|
62
|
+
--pill-bg: #333333;
|
|
63
|
+
--inline-code-bg: #333333;
|
|
64
|
+
--inline-code-ink: #e4e4e4;
|
|
65
|
+
--blockquote-bg: #2a2a2a;
|
|
66
|
+
--editing-bg: #2a2a2a;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (prefers-color-scheme: dark) {
|
|
70
|
+
[data-theme="system"] {
|
|
71
|
+
color-scheme: dark;
|
|
72
|
+
--bg: #1a1a1a;
|
|
73
|
+
--bg-strong: #222222;
|
|
74
|
+
--panel: #252525;
|
|
75
|
+
--panel-strong: #2a2a2a;
|
|
76
|
+
--ink: #e4e4e4;
|
|
77
|
+
--muted: #888888;
|
|
78
|
+
--accent: #5b9aff;
|
|
79
|
+
--accent-strong: #7db0ff;
|
|
80
|
+
--accent-soft: #1e3a5f;
|
|
81
|
+
--border: #3a3a3a;
|
|
82
|
+
--border-strong: #4a4a4a;
|
|
83
|
+
--code-bg: #2a2a2a;
|
|
84
|
+
--code-ink: #e4e4e4;
|
|
85
|
+
--shadow: 0 10px 24px rgba(0, 0, 0, 0.3);
|
|
86
|
+
--shadow-soft: 0 6px 14px rgba(0, 0, 0, 0.2);
|
|
87
|
+
--hover-bg: #333333;
|
|
88
|
+
--toc-active: #3a3a3a;
|
|
89
|
+
--folder-bg: #2a2a2a;
|
|
90
|
+
--pill-bg: #333333;
|
|
91
|
+
--inline-code-bg: #333333;
|
|
92
|
+
--inline-code-ink: #e4e4e4;
|
|
93
|
+
--blockquote-bg: #2a2a2a;
|
|
94
|
+
--editing-bg: #2a2a2a;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
* {
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
body {
|
|
103
|
+
margin: 0;
|
|
104
|
+
font-family: var(--font-ui);
|
|
105
|
+
background: var(--bg);
|
|
106
|
+
color: var(--ink);
|
|
107
|
+
min-height: 100vh;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.app {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: minmax(240px, 320px) 1fr;
|
|
113
|
+
min-height: 100vh;
|
|
114
|
+
gap: 16px;
|
|
115
|
+
padding: 20px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.sidebar {
|
|
119
|
+
background: var(--bg);
|
|
120
|
+
border-radius: var(--radius);
|
|
121
|
+
padding: 18px 16px 14px;
|
|
122
|
+
box-shadow: var(--shadow-soft);
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: 14px;
|
|
126
|
+
min-height: 0;
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.brand {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: space-between;
|
|
134
|
+
gap: 12px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.logo {
|
|
138
|
+
font-size: 18px;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
letter-spacing: 0.01em;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.pill {
|
|
144
|
+
font-size: 11px;
|
|
145
|
+
text-transform: uppercase;
|
|
146
|
+
letter-spacing: 0.08em;
|
|
147
|
+
padding: 5px 9px;
|
|
148
|
+
border-radius: 999px;
|
|
149
|
+
background: var(--pill-bg);
|
|
150
|
+
color: var(--muted);
|
|
151
|
+
border: 1px solid var(--border);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.folder {
|
|
155
|
+
font-size: 12px;
|
|
156
|
+
color: var(--muted);
|
|
157
|
+
background: var(--folder-bg);
|
|
158
|
+
padding: 8px 10px;
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
border: 1px solid var(--border);
|
|
161
|
+
word-break: break-all;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.search {
|
|
165
|
+
position: relative;
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 10px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.filter {
|
|
172
|
+
flex: 1;
|
|
173
|
+
border: 1px solid var(--border);
|
|
174
|
+
border-radius: 8px;
|
|
175
|
+
padding: 8px 10px;
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
background: var(--panel-strong);
|
|
178
|
+
color: var(--ink);
|
|
179
|
+
font-family: var(--font-ui);
|
|
180
|
+
outline: none;
|
|
181
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.filter::placeholder {
|
|
185
|
+
color: var(--muted);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.filter:focus {
|
|
189
|
+
border-color: var(--accent);
|
|
190
|
+
box-shadow: 0 0 0 2px rgba(47, 111, 237, 0.15);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.search-hint {
|
|
194
|
+
font-size: 11px;
|
|
195
|
+
color: var(--muted);
|
|
196
|
+
background: var(--pill-bg);
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
padding: 4px 6px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.file-list {
|
|
202
|
+
list-style: none;
|
|
203
|
+
padding: 0;
|
|
204
|
+
margin: 0;
|
|
205
|
+
overflow-y: auto;
|
|
206
|
+
flex: 1;
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
gap: 2px;
|
|
210
|
+
min-height: 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.file-item {
|
|
214
|
+
padding: 6px 10px;
|
|
215
|
+
border-radius: 6px;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
transition: background 0.15s ease;
|
|
218
|
+
background: transparent;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
color: var(--ink);
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
text-overflow: ellipsis;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.file-item:hover {
|
|
227
|
+
background: var(--hover-bg);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.file-item.active {
|
|
231
|
+
background: var(--accent);
|
|
232
|
+
color: #fff;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.sidebar-footer {
|
|
236
|
+
font-size: 11px;
|
|
237
|
+
color: var(--muted);
|
|
238
|
+
display: flex;
|
|
239
|
+
justify-content: space-between;
|
|
240
|
+
border-top: 1px solid var(--border);
|
|
241
|
+
padding-top: 10px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.viewer {
|
|
245
|
+
background: var(--panel);
|
|
246
|
+
border-radius: var(--radius);
|
|
247
|
+
padding: 20px;
|
|
248
|
+
box-shadow: var(--shadow);
|
|
249
|
+
overflow: hidden;
|
|
250
|
+
display: grid;
|
|
251
|
+
grid-template-rows: auto 1fr;
|
|
252
|
+
gap: 16px;
|
|
253
|
+
min-height: 0;
|
|
254
|
+
max-height: 100vh;
|
|
255
|
+
border: 1px solid var(--border);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.viewer-header {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: space-between;
|
|
262
|
+
gap: 16px;
|
|
263
|
+
flex-wrap: wrap;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.meta-wrap {
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-direction: column;
|
|
269
|
+
gap: 6px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.meta-label {
|
|
273
|
+
font-size: 11px;
|
|
274
|
+
text-transform: uppercase;
|
|
275
|
+
letter-spacing: 0.08em;
|
|
276
|
+
color: var(--muted);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.meta {
|
|
280
|
+
font-size: 13px;
|
|
281
|
+
color: var(--muted);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.toolbar {
|
|
285
|
+
display: flex;
|
|
286
|
+
gap: 10px;
|
|
287
|
+
flex-wrap: wrap;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.button {
|
|
291
|
+
border: 1px solid var(--border);
|
|
292
|
+
background: var(--panel);
|
|
293
|
+
padding: 7px 14px;
|
|
294
|
+
border-radius: 8px;
|
|
295
|
+
font-size: 13px;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
298
|
+
font-family: var(--font-ui);
|
|
299
|
+
color: var(--ink);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.button:hover {
|
|
303
|
+
border-color: var(--border-strong);
|
|
304
|
+
background: var(--hover-bg);
|
|
305
|
+
color: var(--ink);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.button.primary {
|
|
309
|
+
background: var(--accent);
|
|
310
|
+
border-color: var(--accent);
|
|
311
|
+
color: #fff;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.button.primary:hover {
|
|
315
|
+
color: #fff;
|
|
316
|
+
background: var(--accent-strong);
|
|
317
|
+
border-color: var(--accent-strong);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.button.secondary {
|
|
321
|
+
background: var(--bg-strong);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.button[disabled] {
|
|
325
|
+
opacity: 0.5;
|
|
326
|
+
cursor: not-allowed;
|
|
327
|
+
box-shadow: none;
|
|
328
|
+
transform: none;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.viewer-body {
|
|
332
|
+
display: grid;
|
|
333
|
+
grid-template-columns: minmax(0, 1fr) 260px;
|
|
334
|
+
gap: 16px;
|
|
335
|
+
min-height: 0;
|
|
336
|
+
overflow-y: auto;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.document {
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-direction: column;
|
|
342
|
+
gap: 16px;
|
|
343
|
+
min-height: 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.doc-header {
|
|
347
|
+
display: flex;
|
|
348
|
+
flex-direction: column;
|
|
349
|
+
gap: 4px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.doc-title {
|
|
353
|
+
font-size: 22px;
|
|
354
|
+
font-weight: 700;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.doc-path {
|
|
358
|
+
font-size: 12px;
|
|
359
|
+
color: var(--muted);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.content {
|
|
363
|
+
flex: 1;
|
|
364
|
+
background: var(--panel-strong);
|
|
365
|
+
border-radius: var(--radius-sm);
|
|
366
|
+
padding: 20px 22px 28px;
|
|
367
|
+
border: 1px solid var(--border);
|
|
368
|
+
font-family: var(--font-body);
|
|
369
|
+
font-size: 16px;
|
|
370
|
+
line-height: 1.7;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.content.editing {
|
|
374
|
+
border: 1px solid var(--accent);
|
|
375
|
+
box-shadow: inset 0 0 0 1px rgba(47, 111, 237, 0.2);
|
|
376
|
+
background: var(--editing-bg);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.content.editing:focus {
|
|
380
|
+
outline: none;
|
|
381
|
+
box-shadow: inset 0 0 0 2px rgba(47, 111, 237, 0.25);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.content h1,
|
|
385
|
+
.content h2,
|
|
386
|
+
.content h3 {
|
|
387
|
+
font-family: var(--font-ui);
|
|
388
|
+
margin-top: 1.8em;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.content h1 {
|
|
392
|
+
font-size: 28px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.content h2 {
|
|
396
|
+
font-size: 22px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.content h3 {
|
|
400
|
+
font-size: 18px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.content a {
|
|
404
|
+
color: var(--accent);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.content pre {
|
|
408
|
+
background: var(--code-bg);
|
|
409
|
+
color: var(--code-ink);
|
|
410
|
+
padding: 14px 16px;
|
|
411
|
+
border-radius: 10px;
|
|
412
|
+
overflow-x: auto;
|
|
413
|
+
border: 1px solid var(--border);
|
|
414
|
+
box-shadow: none;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.content code {
|
|
418
|
+
font-family: var(--font-mono);
|
|
419
|
+
font-size: 0.95em;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.content :not(pre) > code {
|
|
423
|
+
background: var(--inline-code-bg);
|
|
424
|
+
color: var(--inline-code-ink);
|
|
425
|
+
border-radius: 6px;
|
|
426
|
+
padding: 2px 6px;
|
|
427
|
+
border: 1px solid var(--border);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.content blockquote {
|
|
431
|
+
margin: 1.5em 0;
|
|
432
|
+
padding: 10px 16px;
|
|
433
|
+
border-left: 3px solid var(--border-strong);
|
|
434
|
+
background: var(--blockquote-bg);
|
|
435
|
+
border-radius: 8px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.content table {
|
|
439
|
+
border-collapse: collapse;
|
|
440
|
+
width: 100%;
|
|
441
|
+
margin: 1.5em 0;
|
|
442
|
+
font-size: 15px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.content th,
|
|
446
|
+
.content td {
|
|
447
|
+
border: 1px solid var(--border);
|
|
448
|
+
padding: 8px 10px;
|
|
449
|
+
text-align: left;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.content img {
|
|
453
|
+
max-width: 100%;
|
|
454
|
+
border-radius: 10px;
|
|
455
|
+
box-shadow: none;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.empty {
|
|
459
|
+
color: var(--muted);
|
|
460
|
+
display: flex;
|
|
461
|
+
align-items: center;
|
|
462
|
+
justify-content: center;
|
|
463
|
+
font-size: 16px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.side-panel {
|
|
467
|
+
display: flex;
|
|
468
|
+
flex-direction: column;
|
|
469
|
+
gap: 16px;
|
|
470
|
+
min-height: 0;
|
|
471
|
+
position: sticky;
|
|
472
|
+
top: 20px;
|
|
473
|
+
align-self: start;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.toc,
|
|
477
|
+
.inspector {
|
|
478
|
+
border: 1px solid var(--border);
|
|
479
|
+
border-radius: var(--radius-sm);
|
|
480
|
+
padding: 12px 14px;
|
|
481
|
+
display: flex;
|
|
482
|
+
flex-direction: column;
|
|
483
|
+
gap: 10px;
|
|
484
|
+
background: var(--folder-bg);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.toc-title,
|
|
488
|
+
.inspector-title {
|
|
489
|
+
font-size: 12px;
|
|
490
|
+
font-weight: 700;
|
|
491
|
+
text-transform: uppercase;
|
|
492
|
+
letter-spacing: 0.1em;
|
|
493
|
+
color: var(--muted);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.toc-list {
|
|
497
|
+
list-style: none;
|
|
498
|
+
padding: 0;
|
|
499
|
+
margin: 0;
|
|
500
|
+
overflow-y: auto;
|
|
501
|
+
display: grid;
|
|
502
|
+
gap: 6px;
|
|
503
|
+
max-height: 300px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.toc-item {
|
|
507
|
+
font-size: 13px;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
color: var(--ink);
|
|
510
|
+
padding: 6px 8px;
|
|
511
|
+
border-radius: 8px;
|
|
512
|
+
transition: background 0.2s ease;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.toc-item:hover {
|
|
516
|
+
background: var(--hover-bg);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.toc-item.active {
|
|
520
|
+
background: var(--toc-active);
|
|
521
|
+
color: var(--ink);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.toc-item.level-2 {
|
|
525
|
+
padding-left: 18px;
|
|
526
|
+
color: var(--muted);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.toc-item.level-3 {
|
|
530
|
+
padding-left: 28px;
|
|
531
|
+
color: var(--muted);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.inspector-text {
|
|
535
|
+
font-size: 13px;
|
|
536
|
+
color: var(--muted);
|
|
537
|
+
line-height: 1.5;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.badge {
|
|
541
|
+
align-self: flex-start;
|
|
542
|
+
font-size: 12px;
|
|
543
|
+
font-weight: 600;
|
|
544
|
+
padding: 5px 9px;
|
|
545
|
+
border-radius: 999px;
|
|
546
|
+
background: var(--pill-bg);
|
|
547
|
+
color: var(--muted);
|
|
548
|
+
border: 1px solid var(--border);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
body.editing .badge {
|
|
552
|
+
background: var(--accent-soft);
|
|
553
|
+
color: var(--accent-strong);
|
|
554
|
+
border-color: var(--accent-soft);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.hidden {
|
|
558
|
+
display: none;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.theme-toggle {
|
|
562
|
+
display: flex;
|
|
563
|
+
gap: 4px;
|
|
564
|
+
padding: 3px;
|
|
565
|
+
background: var(--pill-bg);
|
|
566
|
+
border-radius: 8px;
|
|
567
|
+
border: 1px solid var(--border);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.theme-btn {
|
|
571
|
+
padding: 4px 8px;
|
|
572
|
+
border: none;
|
|
573
|
+
background: transparent;
|
|
574
|
+
border-radius: 5px;
|
|
575
|
+
cursor: pointer;
|
|
576
|
+
font-size: 12px;
|
|
577
|
+
color: var(--muted);
|
|
578
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
579
|
+
font-family: var(--font-ui);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.theme-btn:hover {
|
|
583
|
+
color: var(--ink);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.theme-btn.active {
|
|
587
|
+
background: var(--panel);
|
|
588
|
+
color: var(--ink);
|
|
589
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
@media (max-width: 1100px) {
|
|
593
|
+
.viewer-body {
|
|
594
|
+
grid-template-columns: 1fr;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.side-panel {
|
|
598
|
+
flex-direction: row;
|
|
599
|
+
flex-wrap: wrap;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.toc,
|
|
603
|
+
.inspector {
|
|
604
|
+
flex: 1 1 260px;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
@media (max-width: 900px) {
|
|
609
|
+
.app {
|
|
610
|
+
grid-template-columns: 1fr;
|
|
611
|
+
padding: 20px;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.viewer {
|
|
615
|
+
padding: 20px;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
@media (max-width: 600px) {
|
|
620
|
+
.content {
|
|
621
|
+
padding: 18px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.viewer-header {
|
|
625
|
+
flex-direction: column;
|
|
626
|
+
align-items: flex-start;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
</style>
|
|
630
|
+
</head>
|
|
631
|
+
<body>
|
|
632
|
+
<div class="app">
|
|
633
|
+
<aside class="sidebar">
|
|
634
|
+
<div class="brand">
|
|
635
|
+
<div class="logo">skimmd</div>
|
|
636
|
+
<div class="pill" id="file-count">0 files</div>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="folder" id="folder"></div>
|
|
639
|
+
<div class="search">
|
|
640
|
+
<input class="filter" id="filter" placeholder="Filter files..." />
|
|
641
|
+
<div class="search-hint">Cmd+K</div>
|
|
642
|
+
</div>
|
|
643
|
+
<ul class="file-list" id="file-list"></ul>
|
|
644
|
+
<div class="sidebar-footer">
|
|
645
|
+
<div class="theme-toggle">
|
|
646
|
+
<button class="theme-btn" data-theme="light">Light</button>
|
|
647
|
+
<button class="theme-btn" data-theme="dark">Dark</button>
|
|
648
|
+
<button class="theme-btn" data-theme="system">Auto</button>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</aside>
|
|
652
|
+
<main class="viewer">
|
|
653
|
+
<div class="viewer-header">
|
|
654
|
+
<div class="meta-wrap">
|
|
655
|
+
<div class="meta-label">Current file</div>
|
|
656
|
+
<div class="meta" id="meta">Select a file to start.</div>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="toolbar">
|
|
659
|
+
<button class="button" id="edit-toggle" disabled>Edit</button>
|
|
660
|
+
<button class="button primary hidden" id="save">Save</button>
|
|
661
|
+
<button class="button secondary hidden" id="cancel">Cancel</button>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
<div class="viewer-body">
|
|
665
|
+
<section class="document">
|
|
666
|
+
<div class="doc-header">
|
|
667
|
+
<div class="doc-title" id="doc-title">No file selected</div>
|
|
668
|
+
<div class="doc-path" id="doc-path"></div>
|
|
669
|
+
</div>
|
|
670
|
+
<div id="content" class="content empty">Select a file to preview.</div>
|
|
671
|
+
</section>
|
|
672
|
+
<aside class="side-panel">
|
|
673
|
+
<section class="toc">
|
|
674
|
+
<div class="toc-title">Contents</div>
|
|
675
|
+
<ul class="toc-list" id="toc-list"></ul>
|
|
676
|
+
</section>
|
|
677
|
+
<section class="inspector">
|
|
678
|
+
<div class="inspector-title">Editing</div>
|
|
679
|
+
<div class="inspector-text">
|
|
680
|
+
Edit directly in the reader. The table of contents updates as you type.
|
|
681
|
+
</div>
|
|
682
|
+
<div class="badge" id="edit-badge">Read only</div>
|
|
683
|
+
</section>
|
|
684
|
+
</aside>
|
|
685
|
+
</div>
|
|
686
|
+
</main>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<script>
|
|
690
|
+
// Theme handling
|
|
691
|
+
function getStoredTheme() {
|
|
692
|
+
return localStorage.getItem('skimmd-theme') || 'system';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function setTheme(theme) {
|
|
696
|
+
localStorage.setItem('skimmd-theme', theme);
|
|
697
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
698
|
+
updateThemeButtons(theme);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function updateThemeButtons(theme) {
|
|
702
|
+
document.querySelectorAll('.theme-btn').forEach(btn => {
|
|
703
|
+
btn.classList.toggle('active', btn.dataset.theme === theme);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Apply theme immediately to prevent flash
|
|
708
|
+
document.documentElement.setAttribute('data-theme', getStoredTheme());
|
|
709
|
+
|
|
710
|
+
const state = {
|
|
711
|
+
files: [],
|
|
712
|
+
selectedId: null,
|
|
713
|
+
filter: "",
|
|
714
|
+
isEditing: false,
|
|
715
|
+
currentContent: "",
|
|
716
|
+
currentHtml: "",
|
|
717
|
+
tocHeadings: [],
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const fileListEl = document.getElementById("file-list");
|
|
721
|
+
const contentEl = document.getElementById("content");
|
|
722
|
+
const folderEl = document.getElementById("folder");
|
|
723
|
+
const metaEl = document.getElementById("meta");
|
|
724
|
+
const filterEl = document.getElementById("filter");
|
|
725
|
+
const editToggleEl = document.getElementById("edit-toggle");
|
|
726
|
+
const saveEl = document.getElementById("save");
|
|
727
|
+
const cancelEl = document.getElementById("cancel");
|
|
728
|
+
const tocListEl = document.getElementById("toc-list");
|
|
729
|
+
const fileCountEl = document.getElementById("file-count");
|
|
730
|
+
const docTitleEl = document.getElementById("doc-title");
|
|
731
|
+
const docPathEl = document.getElementById("doc-path");
|
|
732
|
+
const editBadgeEl = document.getElementById("edit-badge");
|
|
733
|
+
|
|
734
|
+
let tocTimer = null;
|
|
735
|
+
let scrollRaf = null;
|
|
736
|
+
let activeTocId = null;
|
|
737
|
+
|
|
738
|
+
async function fetchConfig() {
|
|
739
|
+
try {
|
|
740
|
+
const res = await fetch("/api/config");
|
|
741
|
+
if (!res.ok) return;
|
|
742
|
+
const data = await res.json();
|
|
743
|
+
if (data.folderPath) {
|
|
744
|
+
folderEl.textContent = data.folderPath;
|
|
745
|
+
}
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error("Failed to load config", err);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function fetchFiles() {
|
|
752
|
+
const res = await fetch("/api/files");
|
|
753
|
+
if (!res.ok) {
|
|
754
|
+
contentEl.textContent = "Failed to load files.";
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
state.files = await res.json();
|
|
758
|
+
renderList();
|
|
759
|
+
selectInitialFile();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function updateFileCount(filteredCount) {
|
|
763
|
+
const total = state.files.length;
|
|
764
|
+
const label = filteredCount === total ? `${total}` : `${filteredCount} of ${total}`;
|
|
765
|
+
const suffix = total === 1 ? "file" : "files";
|
|
766
|
+
fileCountEl.textContent = `${label} ${suffix}`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function renderList() {
|
|
770
|
+
fileListEl.innerHTML = "";
|
|
771
|
+
const filter = state.filter.trim().toLowerCase();
|
|
772
|
+
const filtered = state.files.filter((file) => {
|
|
773
|
+
if (!filter) return true;
|
|
774
|
+
return (
|
|
775
|
+
file.name.toLowerCase().includes(filter) ||
|
|
776
|
+
file.relativePath.toLowerCase().includes(filter)
|
|
777
|
+
);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
updateFileCount(filtered.length);
|
|
781
|
+
|
|
782
|
+
if (filtered.length === 0) {
|
|
783
|
+
const empty = document.createElement("li");
|
|
784
|
+
empty.className = "file-item";
|
|
785
|
+
empty.textContent = "No matching files";
|
|
786
|
+
fileListEl.appendChild(empty);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
filtered.forEach((file) => {
|
|
791
|
+
const item = document.createElement("li");
|
|
792
|
+
item.className = "file-item" + (file.id === state.selectedId ? " active" : "");
|
|
793
|
+
item.dataset.fileId = file.id;
|
|
794
|
+
item.textContent = file.name;
|
|
795
|
+
item.title = file.relativePath;
|
|
796
|
+
|
|
797
|
+
item.addEventListener("click", () => {
|
|
798
|
+
openFile(file.id);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
fileListEl.appendChild(item);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function updateDocHeader(file) {
|
|
806
|
+
if (!file) {
|
|
807
|
+
docTitleEl.textContent = "No file selected";
|
|
808
|
+
docPathEl.textContent = "";
|
|
809
|
+
document.title = "skimmd";
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const displayName = file.name.replace(/\.md$/i, "");
|
|
813
|
+
docTitleEl.textContent = displayName || "Untitled";
|
|
814
|
+
docPathEl.textContent = file.relativePath || "";
|
|
815
|
+
document.title = displayName ? `${displayName} - skimmd` : "mdr";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async function openFile(fileId) {
|
|
819
|
+
state.selectedId = fileId;
|
|
820
|
+
renderList();
|
|
821
|
+
setEditMode(false);
|
|
822
|
+
metaEl.textContent = "";
|
|
823
|
+
contentEl.textContent = "Loading...";
|
|
824
|
+
activeTocId = null;
|
|
825
|
+
|
|
826
|
+
const file = state.files.find((item) => item.id === fileId);
|
|
827
|
+
updateDocHeader(file);
|
|
828
|
+
|
|
829
|
+
const res = await fetch(`/api/file?path=${encodeURIComponent(fileId)}`);
|
|
830
|
+
if (!res.ok) {
|
|
831
|
+
contentEl.textContent = "Failed to load file.";
|
|
832
|
+
editToggleEl.disabled = true;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const data = await res.json();
|
|
836
|
+
state.currentContent = data.content || "";
|
|
837
|
+
state.currentHtml = data.html || "";
|
|
838
|
+
contentEl.classList.remove("empty");
|
|
839
|
+
contentEl.innerHTML = state.currentHtml;
|
|
840
|
+
editToggleEl.disabled = false;
|
|
841
|
+
if (data.metadata) {
|
|
842
|
+
const updated = new Date(data.metadata.modifiedAt).toLocaleString();
|
|
843
|
+
metaEl.textContent = `Last updated ${updated}`;
|
|
844
|
+
}
|
|
845
|
+
buildToc(contentEl);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function selectInitialFile() {
|
|
849
|
+
if (state.selectedId || state.files.length === 0) return;
|
|
850
|
+
const params = new URLSearchParams(window.location.search);
|
|
851
|
+
const target = params.get("file");
|
|
852
|
+
if (target) {
|
|
853
|
+
const match = state.files.find((file) => {
|
|
854
|
+
return (
|
|
855
|
+
file.id === target ||
|
|
856
|
+
file.relativePath === target ||
|
|
857
|
+
file.name === target ||
|
|
858
|
+
file.relativePath.endsWith("/" + target)
|
|
859
|
+
);
|
|
860
|
+
});
|
|
861
|
+
if (match) {
|
|
862
|
+
openFile(match.id);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
openFile(state.files[0].id);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function setEditMode(enabled) {
|
|
870
|
+
state.isEditing = enabled;
|
|
871
|
+
document.body.classList.toggle("editing", enabled);
|
|
872
|
+
editBadgeEl.textContent = enabled ? "Editing" : "Read only";
|
|
873
|
+
if (enabled) {
|
|
874
|
+
contentEl.contentEditable = "true";
|
|
875
|
+
contentEl.classList.add("editing");
|
|
876
|
+
editToggleEl.classList.add("hidden");
|
|
877
|
+
saveEl.classList.remove("hidden");
|
|
878
|
+
cancelEl.classList.remove("hidden");
|
|
879
|
+
} else {
|
|
880
|
+
contentEl.contentEditable = "false";
|
|
881
|
+
contentEl.classList.remove("editing");
|
|
882
|
+
editToggleEl.classList.remove("hidden");
|
|
883
|
+
saveEl.classList.add("hidden");
|
|
884
|
+
cancelEl.classList.add("hidden");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function saveFile() {
|
|
889
|
+
if (!state.selectedId) return;
|
|
890
|
+
const html = contentEl.innerHTML;
|
|
891
|
+
saveEl.disabled = true;
|
|
892
|
+
try {
|
|
893
|
+
const res = await fetch(`/api/file?path=${encodeURIComponent(state.selectedId)}`, {
|
|
894
|
+
method: "PUT",
|
|
895
|
+
headers: { "Content-Type": "application/json" },
|
|
896
|
+
body: JSON.stringify({ html }),
|
|
897
|
+
});
|
|
898
|
+
if (!res.ok) {
|
|
899
|
+
alert("Failed to save file");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const data = await res.json();
|
|
903
|
+
state.currentContent = data.content || "";
|
|
904
|
+
state.currentHtml = data.html || html;
|
|
905
|
+
contentEl.innerHTML = state.currentHtml;
|
|
906
|
+
if (data.metadata) {
|
|
907
|
+
const updated = new Date(data.metadata.modifiedAt).toLocaleString();
|
|
908
|
+
metaEl.textContent = `Last updated ${updated}`;
|
|
909
|
+
}
|
|
910
|
+
buildToc(contentEl);
|
|
911
|
+
setEditMode(false);
|
|
912
|
+
} finally {
|
|
913
|
+
saveEl.disabled = false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function scheduleTocRefresh() {
|
|
918
|
+
clearTimeout(tocTimer);
|
|
919
|
+
tocTimer = setTimeout(() => buildToc(contentEl), 250);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function slugify(text) {
|
|
923
|
+
const base = text
|
|
924
|
+
.toLowerCase()
|
|
925
|
+
.trim()
|
|
926
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
927
|
+
.replace(/^-+|-+$/g, "");
|
|
928
|
+
return base || "section";
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function setActiveToc(id) {
|
|
932
|
+
if (!id || id === activeTocId) return;
|
|
933
|
+
const prev = activeTocId
|
|
934
|
+
? tocListEl.querySelector(`[data-toc-id="${CSS.escape(activeTocId)}"]`)
|
|
935
|
+
: null;
|
|
936
|
+
if (prev) {
|
|
937
|
+
prev.classList.remove("active");
|
|
938
|
+
}
|
|
939
|
+
const next = tocListEl.querySelector(`[data-toc-id="${CSS.escape(id)}"]`);
|
|
940
|
+
if (next) {
|
|
941
|
+
next.classList.add("active");
|
|
942
|
+
const listRect = tocListEl.getBoundingClientRect();
|
|
943
|
+
const itemRect = next.getBoundingClientRect();
|
|
944
|
+
if (itemRect.top < listRect.top || itemRect.bottom > listRect.bottom) {
|
|
945
|
+
next.scrollIntoView({ block: "nearest" });
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
activeTocId = id;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function updateActiveFromScroll() {
|
|
952
|
+
if (!state.tocHeadings || state.tocHeadings.length === 0) return;
|
|
953
|
+
const scrollTop = contentEl.scrollTop;
|
|
954
|
+
let current = state.tocHeadings[0];
|
|
955
|
+
for (const heading of state.tocHeadings) {
|
|
956
|
+
if (heading.offsetTop - 24 <= scrollTop) {
|
|
957
|
+
current = heading;
|
|
958
|
+
} else {
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (current && current.id) {
|
|
963
|
+
setActiveToc(current.id);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function handleContentScroll() {
|
|
968
|
+
if (scrollRaf) return;
|
|
969
|
+
scrollRaf = requestAnimationFrame(() => {
|
|
970
|
+
scrollRaf = null;
|
|
971
|
+
updateActiveFromScroll();
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function buildToc(container) {
|
|
976
|
+
tocListEl.innerHTML = "";
|
|
977
|
+
if (!container) return;
|
|
978
|
+
const headings = Array.from(container.querySelectorAll("h1, h2, h3"));
|
|
979
|
+
state.tocHeadings = headings;
|
|
980
|
+
if (headings.length === 0) {
|
|
981
|
+
const empty = document.createElement("li");
|
|
982
|
+
empty.className = "toc-item";
|
|
983
|
+
empty.textContent = "No headings";
|
|
984
|
+
tocListEl.appendChild(empty);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const used = new Map();
|
|
989
|
+
headings.forEach((heading) => {
|
|
990
|
+
const level = Number(heading.tagName.replace("H", ""));
|
|
991
|
+
let base = heading.id || slugify(heading.textContent || "section");
|
|
992
|
+
const count = used.get(base) || 0;
|
|
993
|
+
used.set(base, count + 1);
|
|
994
|
+
if (count > 0) {
|
|
995
|
+
base = `${base}-${count}`;
|
|
996
|
+
}
|
|
997
|
+
heading.id = base;
|
|
998
|
+
|
|
999
|
+
const item = document.createElement("li");
|
|
1000
|
+
item.className = `toc-item level-${level}`;
|
|
1001
|
+
item.dataset.tocId = base;
|
|
1002
|
+
item.textContent = heading.textContent || "Untitled";
|
|
1003
|
+
item.addEventListener("click", () => {
|
|
1004
|
+
const target = container.querySelector(`#${CSS.escape(base)}`);
|
|
1005
|
+
if (target) {
|
|
1006
|
+
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
tocListEl.appendChild(item);
|
|
1010
|
+
});
|
|
1011
|
+
updateActiveFromScroll();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
filterEl.addEventListener("input", (event) => {
|
|
1015
|
+
state.filter = event.target.value || "";
|
|
1016
|
+
renderList();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
editToggleEl.addEventListener("click", () => {
|
|
1020
|
+
if (!state.selectedId) return;
|
|
1021
|
+
setEditMode(true);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
cancelEl.addEventListener("click", () => {
|
|
1025
|
+
setEditMode(false);
|
|
1026
|
+
contentEl.innerHTML = state.currentHtml;
|
|
1027
|
+
buildToc(contentEl);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
saveEl.addEventListener("click", () => {
|
|
1031
|
+
saveFile();
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
contentEl.addEventListener("input", () => {
|
|
1035
|
+
if (state.isEditing) {
|
|
1036
|
+
scheduleTocRefresh();
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
contentEl.addEventListener("scroll", handleContentScroll);
|
|
1041
|
+
|
|
1042
|
+
window.addEventListener("resize", () => {
|
|
1043
|
+
updateActiveFromScroll();
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
window.addEventListener("keydown", (event) => {
|
|
1047
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
|
|
1048
|
+
if (state.isEditing) {
|
|
1049
|
+
event.preventDefault();
|
|
1050
|
+
saveFile();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// Theme toggle listeners
|
|
1056
|
+
document.querySelectorAll('.theme-btn').forEach(btn => {
|
|
1057
|
+
btn.addEventListener('click', () => {
|
|
1058
|
+
setTheme(btn.dataset.theme);
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Initialize theme buttons
|
|
1063
|
+
updateThemeButtons(getStoredTheme());
|
|
1064
|
+
|
|
1065
|
+
// Live reload via Server-Sent Events
|
|
1066
|
+
function setupLiveReload() {
|
|
1067
|
+
const eventSource = new EventSource('/api/events');
|
|
1068
|
+
|
|
1069
|
+
eventSource.onmessage = (event) => {
|
|
1070
|
+
try {
|
|
1071
|
+
const data = JSON.parse(event.data);
|
|
1072
|
+
if (data.type === 'reload') {
|
|
1073
|
+
console.log('File changed:', data.file);
|
|
1074
|
+
// If we're not editing, reload the current file
|
|
1075
|
+
if (!state.isEditing && state.selectedId) {
|
|
1076
|
+
// Check if the changed file is the current file
|
|
1077
|
+
const changedFile = data.file.replace(/\\/g, '/');
|
|
1078
|
+
if (state.selectedId === changedFile || changedFile.endsWith(state.selectedId)) {
|
|
1079
|
+
openFile(state.selectedId);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
// Always refresh the file list in case files were added/removed
|
|
1083
|
+
fetchFiles().then(() => {
|
|
1084
|
+
// Re-select current file to update the list highlighting
|
|
1085
|
+
if (state.selectedId) {
|
|
1086
|
+
renderList();
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
console.error('Live reload error:', err);
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
eventSource.onerror = () => {
|
|
1096
|
+
// Reconnect after a delay if connection is lost
|
|
1097
|
+
eventSource.close();
|
|
1098
|
+
setTimeout(setupLiveReload, 2000);
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
setupLiveReload();
|
|
1103
|
+
fetchConfig();
|
|
1104
|
+
fetchFiles();
|
|
1105
|
+
</script>
|
|
1106
|
+
</body>
|
|
1107
|
+
</html>
|