obsidigen 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.
Files changed (112) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +521 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +115 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cloudflare/access.d.ts +31 -0
  8. package/dist/cloudflare/access.d.ts.map +1 -0
  9. package/dist/cloudflare/access.js +194 -0
  10. package/dist/cloudflare/access.js.map +1 -0
  11. package/dist/cloudflare/tunnel.d.ts +49 -0
  12. package/dist/cloudflare/tunnel.d.ts.map +1 -0
  13. package/dist/cloudflare/tunnel.js +333 -0
  14. package/dist/cloudflare/tunnel.js.map +1 -0
  15. package/dist/commands/access.d.ts +2 -0
  16. package/dist/commands/access.d.ts.map +1 -0
  17. package/dist/commands/access.js +162 -0
  18. package/dist/commands/access.js.map +1 -0
  19. package/dist/commands/config.d.ts +8 -0
  20. package/dist/commands/config.d.ts.map +1 -0
  21. package/dist/commands/config.js +94 -0
  22. package/dist/commands/config.js.map +1 -0
  23. package/dist/commands/daemon.d.ts +5 -0
  24. package/dist/commands/daemon.d.ts.map +1 -0
  25. package/dist/commands/daemon.js +135 -0
  26. package/dist/commands/daemon.js.map +1 -0
  27. package/dist/commands/init.d.ts +7 -0
  28. package/dist/commands/init.d.ts.map +1 -0
  29. package/dist/commands/init.js +74 -0
  30. package/dist/commands/init.js.map +1 -0
  31. package/dist/commands/service.d.ts +2 -0
  32. package/dist/commands/service.d.ts.map +1 -0
  33. package/dist/commands/service.js +334 -0
  34. package/dist/commands/service.js.map +1 -0
  35. package/dist/commands/start.d.ts +9 -0
  36. package/dist/commands/start.d.ts.map +1 -0
  37. package/dist/commands/start.js +185 -0
  38. package/dist/commands/start.js.map +1 -0
  39. package/dist/commands/tunnel.d.ts +7 -0
  40. package/dist/commands/tunnel.d.ts.map +1 -0
  41. package/dist/commands/tunnel.js +174 -0
  42. package/dist/commands/tunnel.js.map +1 -0
  43. package/dist/config/global.d.ts +16 -0
  44. package/dist/config/global.d.ts.map +1 -0
  45. package/dist/config/global.js +102 -0
  46. package/dist/config/global.js.map +1 -0
  47. package/dist/config/types.d.ts +45 -0
  48. package/dist/config/types.d.ts.map +1 -0
  49. package/dist/config/types.js +3 -0
  50. package/dist/config/types.js.map +1 -0
  51. package/dist/config/vault.d.ts +12 -0
  52. package/dist/config/vault.d.ts.map +1 -0
  53. package/dist/config/vault.js +87 -0
  54. package/dist/config/vault.js.map +1 -0
  55. package/dist/server/index.d.ts +8 -0
  56. package/dist/server/index.d.ts.map +1 -0
  57. package/dist/server/index.js +84 -0
  58. package/dist/server/index.js.map +1 -0
  59. package/dist/server/routes/graph.d.ts +11 -0
  60. package/dist/server/routes/graph.d.ts.map +1 -0
  61. package/dist/server/routes/graph.js +97 -0
  62. package/dist/server/routes/graph.js.map +1 -0
  63. package/dist/server/routes/preview.d.ts +11 -0
  64. package/dist/server/routes/preview.d.ts.map +1 -0
  65. package/dist/server/routes/preview.js +41 -0
  66. package/dist/server/routes/preview.js.map +1 -0
  67. package/dist/server/routes/search.d.ts +11 -0
  68. package/dist/server/routes/search.d.ts.map +1 -0
  69. package/dist/server/routes/search.js +38 -0
  70. package/dist/server/routes/search.js.map +1 -0
  71. package/dist/server/routes/static.d.ts +9 -0
  72. package/dist/server/routes/static.d.ts.map +1 -0
  73. package/dist/server/routes/static.js +45 -0
  74. package/dist/server/routes/static.js.map +1 -0
  75. package/dist/server/routes/tree.d.ts +18 -0
  76. package/dist/server/routes/tree.d.ts.map +1 -0
  77. package/dist/server/routes/tree.js +61 -0
  78. package/dist/server/routes/tree.js.map +1 -0
  79. package/dist/server/routes/wiki.d.ts +11 -0
  80. package/dist/server/routes/wiki.d.ts.map +1 -0
  81. package/dist/server/routes/wiki.js +102 -0
  82. package/dist/server/routes/wiki.js.map +1 -0
  83. package/dist/templates/page.d.ts +23 -0
  84. package/dist/templates/page.d.ts.map +1 -0
  85. package/dist/templates/page.js +1913 -0
  86. package/dist/templates/page.js.map +1 -0
  87. package/dist/utils/plist.d.ts +60 -0
  88. package/dist/utils/plist.d.ts.map +1 -0
  89. package/dist/utils/plist.js +185 -0
  90. package/dist/utils/plist.js.map +1 -0
  91. package/dist/utils/systemd.d.ts +75 -0
  92. package/dist/utils/systemd.d.ts.map +1 -0
  93. package/dist/utils/systemd.js +220 -0
  94. package/dist/utils/systemd.js.map +1 -0
  95. package/dist/vault/indexer.d.ts +92 -0
  96. package/dist/vault/indexer.d.ts.map +1 -0
  97. package/dist/vault/indexer.js +360 -0
  98. package/dist/vault/indexer.js.map +1 -0
  99. package/dist/vault/parser.d.ts +14 -0
  100. package/dist/vault/parser.d.ts.map +1 -0
  101. package/dist/vault/parser.js +206 -0
  102. package/dist/vault/parser.js.map +1 -0
  103. package/dist/vault/resolver.d.ts +27 -0
  104. package/dist/vault/resolver.d.ts.map +1 -0
  105. package/dist/vault/resolver.js +111 -0
  106. package/dist/vault/resolver.js.map +1 -0
  107. package/dist/vault/watcher.d.ts +28 -0
  108. package/dist/vault/watcher.d.ts.map +1 -0
  109. package/dist/vault/watcher.js +86 -0
  110. package/dist/vault/watcher.js.map +1 -0
  111. package/package.json +77 -0
  112. package/public/styles/.gitkeep +0 -0
@@ -0,0 +1,1913 @@
1
+ // HTML templates for wiki pages - Three-column layout
2
+ const styles = `
3
+ /* ============================================
4
+ CSS CUSTOM PROPERTIES - THEME SYSTEM
5
+ ============================================ */
6
+
7
+ :root {
8
+ /* Dark theme (default) */
9
+ --bg-primary: #0f0f0f;
10
+ --bg-secondary: #1a1a1a;
11
+ --bg-tertiary: #252525;
12
+ --bg-hover: #2a2a2a;
13
+ --text-primary: #e8e8e8;
14
+ --text-secondary: #a0a0a0;
15
+ --text-muted: #666;
16
+ --accent: #7c9cff;
17
+ --accent-hover: #a0b8ff;
18
+ --accent-subtle: rgba(124, 156, 255, 0.1);
19
+ --border: #333;
20
+ --border-subtle: #262626;
21
+ --link: #7c9cff;
22
+ --link-missing: #ff6b6b;
23
+ --success: #4ade80;
24
+ --warning: #fbbf24;
25
+ --shadow: rgba(0, 0, 0, 0.4);
26
+ --sidebar-width: 280px;
27
+ --backlinks-width: 260px;
28
+ --header-height: 0px;
29
+ --font-mono: 'Berkeley Mono', 'IBM Plex Mono', 'JetBrains Mono', 'Fira Code', monospace;
30
+ --font-sans: 'Söhne', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
31
+ --font-serif: 'Newsreader', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
32
+ }
33
+
34
+ [data-theme="light"] {
35
+ --bg-primary: #fafafa;
36
+ --bg-secondary: #ffffff;
37
+ --bg-tertiary: #f0f0f0;
38
+ --bg-hover: #e8e8e8;
39
+ --text-primary: #1a1a1a;
40
+ --text-secondary: #555;
41
+ --text-muted: #888;
42
+ --accent: #4a6cd4;
43
+ --accent-hover: #3654b8;
44
+ --accent-subtle: rgba(74, 108, 212, 0.08);
45
+ --border: #e0e0e0;
46
+ --border-subtle: #eee;
47
+ --link: #4a6cd4;
48
+ --link-missing: #d94444;
49
+ --success: #22c55e;
50
+ --warning: #f59e0b;
51
+ --shadow: rgba(0, 0, 0, 0.08);
52
+ }
53
+
54
+ /* ============================================
55
+ BASE STYLES
56
+ ============================================ */
57
+
58
+ * {
59
+ box-sizing: border-box;
60
+ margin: 0;
61
+ padding: 0;
62
+ }
63
+
64
+ html {
65
+ font-size: 15px;
66
+ scroll-behavior: smooth;
67
+ }
68
+
69
+ body {
70
+ font-family: var(--font-sans);
71
+ background: var(--bg-primary);
72
+ color: var(--text-primary);
73
+ line-height: 1.7;
74
+ min-height: 100vh;
75
+ overflow: hidden;
76
+ }
77
+
78
+ /* ============================================
79
+ THREE-COLUMN LAYOUT
80
+ ============================================ */
81
+
82
+ .wiki-layout {
83
+ display: grid;
84
+ grid-template-columns: var(--sidebar-width) minmax(0, 800px) var(--backlinks-width);
85
+ justify-content: center;
86
+ height: 100vh;
87
+ overflow: hidden;
88
+ background: var(--bg-primary);
89
+ }
90
+
91
+ /* ============================================
92
+ LEFT SIDEBAR
93
+ ============================================ */
94
+
95
+ .sidebar-left {
96
+ background: var(--bg-primary);
97
+ display: flex;
98
+ flex-direction: column;
99
+ height: 100vh;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .sidebar-header {
104
+ padding: 1.25rem 1rem;
105
+ flex-shrink: 0;
106
+ }
107
+
108
+ .vault-title {
109
+ font-size: 1.1rem;
110
+ font-weight: 600;
111
+ color: var(--text-primary);
112
+ margin-bottom: 1rem;
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 0.5rem;
116
+ letter-spacing: -0.01em;
117
+ }
118
+
119
+ .vault-title svg {
120
+ width: 20px;
121
+ height: 20px;
122
+ opacity: 0.8;
123
+ }
124
+
125
+ /* Search */
126
+ .search-container {
127
+ position: relative;
128
+ }
129
+
130
+ .search-input {
131
+ width: 100%;
132
+ padding: 0.6rem 0.75rem 0.6rem 2.25rem;
133
+ background: var(--bg-tertiary);
134
+ border: 1px solid var(--border-subtle);
135
+ border-radius: 6px;
136
+ color: var(--text-primary);
137
+ font-size: 0.9rem;
138
+ font-family: inherit;
139
+ transition: border-color 0.15s, box-shadow 0.15s;
140
+ }
141
+
142
+ .search-input:focus {
143
+ outline: none;
144
+ border-color: var(--accent);
145
+ box-shadow: 0 0 0 3px var(--accent-subtle);
146
+ }
147
+
148
+ .search-input::placeholder {
149
+ color: var(--text-muted);
150
+ }
151
+
152
+ .search-icon {
153
+ position: absolute;
154
+ left: 0.75rem;
155
+ top: 50%;
156
+ transform: translateY(-50%);
157
+ color: var(--text-muted);
158
+ pointer-events: none;
159
+ }
160
+
161
+ .search-results {
162
+ position: absolute;
163
+ top: calc(100% + 4px);
164
+ left: 0;
165
+ right: 0;
166
+ background: var(--bg-secondary);
167
+ border: 1px solid var(--border);
168
+ border-radius: 8px;
169
+ max-height: 320px;
170
+ overflow-y: auto;
171
+ display: none;
172
+ z-index: 1000;
173
+ box-shadow: 0 8px 24px var(--shadow);
174
+ }
175
+
176
+ .search-results.active {
177
+ display: block;
178
+ }
179
+
180
+ .search-result-item {
181
+ display: block;
182
+ padding: 0.65rem 0.85rem;
183
+ text-decoration: none;
184
+ border-bottom: 1px solid var(--border-subtle);
185
+ transition: background 0.1s;
186
+ }
187
+
188
+ .search-result-item:last-child {
189
+ border-bottom: none;
190
+ }
191
+
192
+ .search-result-item:hover {
193
+ background: var(--bg-hover);
194
+ }
195
+
196
+ .search-result-title {
197
+ color: var(--text-primary);
198
+ font-weight: 500;
199
+ font-size: 0.9rem;
200
+ }
201
+
202
+ .search-result-path {
203
+ color: var(--text-muted);
204
+ font-size: 0.75rem;
205
+ font-family: var(--font-mono);
206
+ margin-top: 2px;
207
+ }
208
+
209
+ /* Tree Navigation */
210
+ .tree-container {
211
+ flex: 1;
212
+ overflow-y: auto;
213
+ overflow-x: hidden;
214
+ padding: 0.5rem 0;
215
+ }
216
+
217
+ .tree-container::-webkit-scrollbar {
218
+ width: 6px;
219
+ }
220
+
221
+ .tree-container::-webkit-scrollbar-track {
222
+ background: transparent;
223
+ }
224
+
225
+ .tree-container::-webkit-scrollbar-thumb {
226
+ background: var(--border);
227
+ border-radius: 3px;
228
+ }
229
+
230
+ .tree-container::-webkit-scrollbar-thumb:hover {
231
+ background: var(--text-muted);
232
+ }
233
+
234
+ .tree-list {
235
+ list-style: none;
236
+ padding-left: 0;
237
+ }
238
+
239
+ .tree-list .tree-list {
240
+ padding-left: 0.875rem;
241
+ border-left: 1px solid var(--border-subtle);
242
+ margin-left: 0.5rem;
243
+ }
244
+
245
+ .tree-item {
246
+ user-select: none;
247
+ }
248
+
249
+ .tree-folder-header {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 0.25rem;
253
+ padding: 0.2rem 0.75rem;
254
+ cursor: pointer;
255
+ color: var(--text-muted);
256
+ font-size: 0.8rem;
257
+ font-weight: 500;
258
+ text-transform: uppercase;
259
+ letter-spacing: 0.03em;
260
+ transition: color 0.1s;
261
+ }
262
+
263
+ .tree-folder-header:hover {
264
+ color: var(--text-secondary);
265
+ }
266
+
267
+ .tree-page-link {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 0.25rem;
271
+ padding: 0.15rem 0.75rem;
272
+ cursor: pointer;
273
+ color: var(--text-secondary);
274
+ text-decoration: none;
275
+ font-size: 0.85rem;
276
+ transition: color 0.1s;
277
+ line-height: 1.5;
278
+ }
279
+
280
+ .tree-page-link:hover {
281
+ color: var(--accent);
282
+ }
283
+
284
+ .tree-page-link:hover .tree-name {
285
+ text-decoration: underline;
286
+ text-underline-offset: 2px;
287
+ }
288
+
289
+ .tree-page-link.active {
290
+ color: var(--accent);
291
+ }
292
+
293
+ .tree-page-link.active .tree-name {
294
+ font-weight: 500;
295
+ }
296
+
297
+ .tree-chevron {
298
+ width: 12px;
299
+ height: 12px;
300
+ flex-shrink: 0;
301
+ transition: transform 0.15s;
302
+ opacity: 0.4;
303
+ }
304
+
305
+ .tree-chevron.expanded {
306
+ transform: rotate(90deg);
307
+ }
308
+
309
+ .tree-icon {
310
+ display: none;
311
+ }
312
+
313
+ .tree-folder-header .tree-icon {
314
+ display: none;
315
+ }
316
+
317
+ .tree-name {
318
+ flex: 1;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
+ white-space: nowrap;
322
+ }
323
+
324
+ .tree-children {
325
+ display: none;
326
+ padding-top: 0.1rem;
327
+ padding-bottom: 0.25rem;
328
+ }
329
+
330
+ .tree-children.expanded {
331
+ display: block;
332
+ }
333
+
334
+ /* ============================================
335
+ MAIN CONTENT AREA
336
+ ============================================ */
337
+
338
+ .content-area {
339
+ height: 100vh;
340
+ overflow-y: auto;
341
+ background: var(--bg-secondary);
342
+ border-left: 1px solid var(--border-subtle);
343
+ border-right: 1px solid var(--border-subtle);
344
+ }
345
+
346
+ .content-area::-webkit-scrollbar {
347
+ width: 8px;
348
+ }
349
+
350
+ .content-area::-webkit-scrollbar-track {
351
+ background: transparent;
352
+ }
353
+
354
+ .content-area::-webkit-scrollbar-thumb {
355
+ background: var(--border);
356
+ border-radius: 4px;
357
+ }
358
+
359
+ .content-area::-webkit-scrollbar-thumb:hover {
360
+ background: var(--text-muted);
361
+ }
362
+
363
+ .content-wrapper {
364
+ padding: 2.5rem 3rem;
365
+ }
366
+
367
+ /* Article */
368
+ .article {
369
+ /* Clean wiki style - no card */
370
+ }
371
+
372
+ .article-header {
373
+ margin-bottom: 1.5rem;
374
+ padding-bottom: 1rem;
375
+ border-bottom: 1px solid var(--border-subtle);
376
+ }
377
+
378
+ .article-title {
379
+ font-size: 2.25rem;
380
+ font-weight: 700;
381
+ color: var(--text-primary);
382
+ font-family: var(--font-serif);
383
+ letter-spacing: -0.02em;
384
+ line-height: 1.2;
385
+ }
386
+
387
+ /* Markdown Content */
388
+ .content {
389
+ font-size: 1rem;
390
+ line-height: 1.75;
391
+ }
392
+
393
+ .content h1 {
394
+ font-size: 1.7rem;
395
+ margin: 2.5rem 0 1rem;
396
+ font-weight: 700;
397
+ color: var(--text-primary);
398
+ font-family: var(--font-serif);
399
+ letter-spacing: -0.02em;
400
+ }
401
+
402
+ .content h2 {
403
+ font-size: 1.4rem;
404
+ margin: 2rem 0 0.75rem;
405
+ font-weight: 600;
406
+ color: var(--text-primary);
407
+ font-family: var(--font-serif);
408
+ letter-spacing: -0.01em;
409
+ }
410
+
411
+ .content h3 {
412
+ font-size: 1.2rem;
413
+ margin: 1.75rem 0 0.6rem;
414
+ font-weight: 600;
415
+ color: var(--text-primary);
416
+ }
417
+
418
+ .content h4, .content h5, .content h6 {
419
+ font-size: 1.05rem;
420
+ margin: 1.5rem 0 0.5rem;
421
+ font-weight: 600;
422
+ color: var(--text-secondary);
423
+ }
424
+
425
+ .content p {
426
+ margin-bottom: 1.25rem;
427
+ }
428
+
429
+ .content a {
430
+ color: var(--link);
431
+ text-decoration: none;
432
+ border-bottom: 1px solid transparent;
433
+ transition: border-color 0.15s;
434
+ }
435
+
436
+ .content a:hover {
437
+ border-bottom-color: var(--link);
438
+ }
439
+
440
+ .content .wiki-link {
441
+ color: var(--accent);
442
+ font-weight: 500;
443
+ }
444
+
445
+ .content .wiki-link-missing {
446
+ color: var(--link-missing);
447
+ font-style: italic;
448
+ }
449
+
450
+ .content ul, .content ol {
451
+ margin: 1rem 0 1rem 1.5rem;
452
+ }
453
+
454
+ .content li {
455
+ margin-bottom: 0.35rem;
456
+ }
457
+
458
+ .content li > ul, .content li > ol {
459
+ margin: 0.35rem 0 0.35rem 1.25rem;
460
+ }
461
+
462
+ .content blockquote {
463
+ border-left: 3px solid var(--accent);
464
+ padding-left: 1.25rem;
465
+ margin: 1.5rem 0;
466
+ color: var(--text-secondary);
467
+ font-style: italic;
468
+ }
469
+
470
+ .content code {
471
+ background: var(--bg-tertiary);
472
+ padding: 0.15rem 0.4rem;
473
+ border-radius: 4px;
474
+ font-family: var(--font-mono);
475
+ font-size: 0.875em;
476
+ }
477
+
478
+ .content pre {
479
+ background: var(--bg-tertiary);
480
+ padding: 1.25rem;
481
+ border-radius: 8px;
482
+ overflow-x: auto;
483
+ margin: 1.5rem 0;
484
+ border: 1px solid var(--border-subtle);
485
+ }
486
+
487
+ .content pre code {
488
+ background: none;
489
+ padding: 0;
490
+ font-size: 0.85rem;
491
+ line-height: 1.5;
492
+ }
493
+
494
+ .content hr {
495
+ border: none;
496
+ height: 1px;
497
+ background: var(--border);
498
+ margin: 2.5rem 0;
499
+ }
500
+
501
+ .content table {
502
+ width: 100%;
503
+ border-collapse: collapse;
504
+ margin: 1.5rem 0;
505
+ font-size: 0.95rem;
506
+ }
507
+
508
+ .content th, .content td {
509
+ border: 1px solid var(--border);
510
+ padding: 0.75rem;
511
+ text-align: left;
512
+ }
513
+
514
+ .content th {
515
+ background: var(--bg-tertiary);
516
+ font-weight: 600;
517
+ }
518
+
519
+ .content img {
520
+ max-width: 100%;
521
+ height: auto;
522
+ border-radius: 8px;
523
+ margin: 1.25rem 0;
524
+ }
525
+
526
+ .content mark {
527
+ background: rgba(251, 191, 36, 0.25);
528
+ color: var(--text-primary);
529
+ padding: 0.1rem 0.3rem;
530
+ border-radius: 3px;
531
+ }
532
+
533
+ /* Callouts */
534
+ .callout {
535
+ margin: 1.5rem 0;
536
+ padding: 1rem 1.25rem;
537
+ border-radius: 8px;
538
+ border-left: 4px solid var(--accent);
539
+ background: var(--bg-tertiary);
540
+ }
541
+
542
+ .callout-title {
543
+ font-weight: 600;
544
+ margin-bottom: 0.5rem;
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 0.5rem;
548
+ }
549
+
550
+ .callout-info { border-left-color: var(--accent); }
551
+ .callout-warning { border-left-color: var(--warning); }
552
+ .callout-danger { border-left-color: var(--link-missing); }
553
+ .callout-success { border-left-color: var(--success); }
554
+ .callout-note { border-left-color: var(--text-muted); }
555
+
556
+ /* ============================================
557
+ RIGHT SIDEBAR - WIDGETS
558
+ ============================================ */
559
+
560
+ .sidebar-right {
561
+ background: var(--bg-primary);
562
+ height: 100vh;
563
+ overflow-y: auto;
564
+ padding: 1.25rem 1rem;
565
+ display: flex;
566
+ flex-direction: column;
567
+ gap: 1.25rem;
568
+ }
569
+
570
+ .sidebar-right::-webkit-scrollbar {
571
+ width: 6px;
572
+ }
573
+
574
+ .sidebar-right::-webkit-scrollbar-track {
575
+ background: transparent;
576
+ }
577
+
578
+ .sidebar-right::-webkit-scrollbar-thumb {
579
+ background: var(--border);
580
+ border-radius: 3px;
581
+ }
582
+
583
+ /* Widget Base - Clean wiki style */
584
+ .widget {
585
+ /* No background/border - clean look */
586
+ }
587
+
588
+ .widget-header {
589
+ display: flex;
590
+ align-items: center;
591
+ gap: 0.5rem;
592
+ padding-bottom: 0.4rem;
593
+ margin-bottom: 0.4rem;
594
+ border-bottom: 1px solid var(--border-subtle);
595
+ }
596
+
597
+ .widget-title {
598
+ font-size: 0.7rem;
599
+ font-weight: 600;
600
+ color: var(--text-muted);
601
+ text-transform: uppercase;
602
+ letter-spacing: 0.05em;
603
+ }
604
+
605
+ .widget-count {
606
+ font-size: 0.65rem;
607
+ color: var(--text-muted);
608
+ }
609
+
610
+ .widget-content {
611
+ max-height: 220px;
612
+ overflow-y: auto;
613
+ }
614
+
615
+ .widget-content::-webkit-scrollbar {
616
+ width: 4px;
617
+ }
618
+
619
+ .widget-content::-webkit-scrollbar-track {
620
+ background: transparent;
621
+ }
622
+
623
+ .widget-content::-webkit-scrollbar-thumb {
624
+ background: var(--border);
625
+ border-radius: 2px;
626
+ }
627
+
628
+ .widget-empty {
629
+ color: var(--text-muted);
630
+ font-size: 0.78rem;
631
+ font-style: italic;
632
+ }
633
+
634
+ /* On This Page Widget (TOC) */
635
+ .toc-list {
636
+ list-style: none;
637
+ }
638
+
639
+ .toc-item {
640
+ margin: 0;
641
+ }
642
+
643
+ .toc-link {
644
+ display: block;
645
+ padding: 0.2rem 0;
646
+ padding-left: 0.6rem;
647
+ color: var(--text-secondary);
648
+ text-decoration: none;
649
+ font-size: 0.78rem;
650
+ line-height: 1.45;
651
+ transition: color 0.1s;
652
+ border-left: 1px solid var(--border-subtle);
653
+ margin-left: 0;
654
+ }
655
+
656
+ .toc-link:hover {
657
+ color: var(--accent);
658
+ }
659
+
660
+ .toc-link.active {
661
+ color: var(--accent);
662
+ border-left-color: var(--accent);
663
+ }
664
+
665
+ .toc-link[data-level="2"] {
666
+ padding-left: 0.6rem;
667
+ }
668
+
669
+ .toc-link[data-level="3"] {
670
+ padding-left: 1.1rem;
671
+ font-size: 0.75rem;
672
+ }
673
+
674
+ .toc-link[data-level="4"],
675
+ .toc-link[data-level="5"],
676
+ .toc-link[data-level="6"] {
677
+ padding-left: 1.6rem;
678
+ font-size: 0.72rem;
679
+ color: var(--text-muted);
680
+ }
681
+
682
+ /* Properties Widget */
683
+ .properties-table {
684
+ width: 100%;
685
+ font-size: 0.78rem;
686
+ }
687
+
688
+ .properties-table tr {
689
+ border-bottom: 1px solid var(--border-subtle);
690
+ }
691
+
692
+ .properties-table tr:last-child {
693
+ border-bottom: none;
694
+ }
695
+
696
+ .properties-table td {
697
+ padding: 0.3rem 0;
698
+ vertical-align: top;
699
+ }
700
+
701
+ .properties-key {
702
+ color: var(--text-muted);
703
+ font-weight: 500;
704
+ width: 38%;
705
+ padding-right: 0.5rem;
706
+ }
707
+
708
+ .properties-value {
709
+ color: var(--text-secondary);
710
+ word-break: break-word;
711
+ }
712
+
713
+ .properties-value a {
714
+ color: var(--accent);
715
+ text-decoration: none;
716
+ }
717
+
718
+ .properties-value a:hover {
719
+ text-decoration: underline;
720
+ }
721
+
722
+ .properties-tag {
723
+ display: inline-block;
724
+ padding: 0.1rem 0.35rem;
725
+ background: var(--accent-subtle);
726
+ color: var(--accent);
727
+ border-radius: 3px;
728
+ font-size: 0.7rem;
729
+ margin: 0.1rem 0.15rem 0.1rem 0;
730
+ }
731
+
732
+ /* Backlinks Widget */
733
+ .backlinks-list {
734
+ list-style: none;
735
+ }
736
+
737
+ .backlinks-list li {
738
+ margin: 0;
739
+ }
740
+
741
+ .backlinks-list a {
742
+ display: block;
743
+ padding: 0.2rem 0;
744
+ color: var(--text-secondary);
745
+ text-decoration: none;
746
+ font-size: 0.78rem;
747
+ transition: color 0.1s;
748
+ }
749
+
750
+ .backlinks-list a:hover {
751
+ color: var(--accent);
752
+ }
753
+
754
+ /* Theme Toggle */
755
+ .theme-toggle-container {
756
+ margin-top: auto;
757
+ padding-top: 0.75rem;
758
+ border-top: 1px solid var(--border-subtle);
759
+ }
760
+
761
+ .theme-toggle {
762
+ display: flex;
763
+ align-items: center;
764
+ justify-content: center;
765
+ gap: 0.4rem;
766
+ padding: 0.4rem;
767
+ background: transparent;
768
+ border: none;
769
+ cursor: pointer;
770
+ color: var(--text-muted);
771
+ font-size: 0.72rem;
772
+ font-family: inherit;
773
+ width: 100%;
774
+ transition: color 0.1s;
775
+ }
776
+
777
+ .theme-toggle:hover {
778
+ color: var(--text-primary);
779
+ }
780
+
781
+ .theme-toggle svg {
782
+ width: 14px;
783
+ height: 14px;
784
+ }
785
+
786
+ /* ============================================
787
+ HOME PAGE
788
+ ============================================ */
789
+
790
+ .home-header {
791
+ text-align: center;
792
+ margin-bottom: 3rem;
793
+ }
794
+
795
+ .home-title {
796
+ font-size: 2.25rem;
797
+ font-weight: 700;
798
+ margin-bottom: 0.5rem;
799
+ font-family: var(--font-serif);
800
+ letter-spacing: -0.02em;
801
+ }
802
+
803
+ .home-stats {
804
+ color: var(--text-muted);
805
+ }
806
+
807
+ .page-grid {
808
+ display: grid;
809
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
810
+ gap: 0.75rem;
811
+ margin-top: 1.25rem;
812
+ }
813
+
814
+ .page-card {
815
+ background: var(--bg-tertiary);
816
+ border: 1px solid var(--border-subtle);
817
+ border-radius: 8px;
818
+ padding: 1rem 1.15rem;
819
+ text-decoration: none;
820
+ transition: all 0.15s;
821
+ }
822
+
823
+ .page-card:hover {
824
+ border-color: var(--accent);
825
+ transform: translateY(-1px);
826
+ box-shadow: 0 4px 12px var(--shadow);
827
+ }
828
+
829
+ .page-card-title {
830
+ color: var(--text-primary);
831
+ font-weight: 600;
832
+ margin-bottom: 0.25rem;
833
+ font-size: 0.95rem;
834
+ }
835
+
836
+ .page-card-path {
837
+ color: var(--text-muted);
838
+ font-size: 0.75rem;
839
+ font-family: var(--font-mono);
840
+ }
841
+
842
+ .section-title {
843
+ font-size: 1.1rem;
844
+ font-weight: 600;
845
+ margin: 2.5rem 0 1rem;
846
+ color: var(--text-secondary);
847
+ }
848
+
849
+ /* ============================================
850
+ 404 PAGE
851
+ ============================================ */
852
+
853
+ .error-page {
854
+ text-align: center;
855
+ padding: 4rem 2rem;
856
+ }
857
+
858
+ .error-code {
859
+ font-size: 5rem;
860
+ font-weight: 700;
861
+ color: var(--text-muted);
862
+ font-family: var(--font-mono);
863
+ opacity: 0.5;
864
+ }
865
+
866
+ .error-message {
867
+ font-size: 1.4rem;
868
+ margin-bottom: 1.5rem;
869
+ }
870
+
871
+ .error-link {
872
+ color: var(--accent);
873
+ text-decoration: none;
874
+ }
875
+
876
+ .error-link:hover {
877
+ text-decoration: underline;
878
+ }
879
+
880
+ /* ============================================
881
+ MOBILE RESPONSIVE
882
+ ============================================ */
883
+
884
+ .mobile-header {
885
+ display: none;
886
+ position: fixed;
887
+ top: 0;
888
+ left: 0;
889
+ right: 0;
890
+ height: 56px;
891
+ background: var(--bg-secondary);
892
+ border-bottom: 1px solid var(--border-subtle);
893
+ z-index: 1000;
894
+ padding: 0 1rem;
895
+ align-items: center;
896
+ justify-content: space-between;
897
+ }
898
+
899
+ .mobile-menu-btn,
900
+ .mobile-backlinks-btn {
901
+ background: transparent;
902
+ border: none;
903
+ color: var(--text-secondary);
904
+ padding: 0.5rem;
905
+ cursor: pointer;
906
+ border-radius: 6px;
907
+ transition: background 0.1s, color 0.1s;
908
+ }
909
+
910
+ .mobile-menu-btn:hover,
911
+ .mobile-backlinks-btn:hover {
912
+ background: var(--bg-hover);
913
+ color: var(--text-primary);
914
+ }
915
+
916
+ .mobile-menu-btn svg,
917
+ .mobile-backlinks-btn svg {
918
+ width: 24px;
919
+ height: 24px;
920
+ }
921
+
922
+ .mobile-title {
923
+ font-weight: 600;
924
+ font-size: 1rem;
925
+ color: var(--text-primary);
926
+ }
927
+
928
+ .sidebar-overlay {
929
+ display: none;
930
+ position: fixed;
931
+ inset: 0;
932
+ background: rgba(0, 0, 0, 0.5);
933
+ z-index: 999;
934
+ opacity: 0;
935
+ transition: opacity 0.2s;
936
+ }
937
+
938
+ .sidebar-overlay.active {
939
+ display: block;
940
+ opacity: 1;
941
+ }
942
+
943
+ @media (max-width: 1280px) {
944
+ .wiki-layout {
945
+ grid-template-columns: var(--sidebar-width) 1fr var(--backlinks-width);
946
+ justify-content: stretch;
947
+ }
948
+ }
949
+
950
+ @media (max-width: 1024px) {
951
+ .wiki-layout {
952
+ grid-template-columns: var(--sidebar-width) 1fr;
953
+ }
954
+
955
+ .sidebar-right {
956
+ display: none;
957
+ position: fixed;
958
+ right: 0;
959
+ top: 0;
960
+ bottom: 0;
961
+ width: var(--backlinks-width);
962
+ z-index: 1001;
963
+ transform: translateX(100%);
964
+ transition: transform 0.25s ease;
965
+ background: var(--bg-secondary);
966
+ border-left: 1px solid var(--border-subtle);
967
+ }
968
+
969
+ .sidebar-right.mobile-open {
970
+ display: flex;
971
+ flex-direction: column;
972
+ transform: translateX(0);
973
+ }
974
+ }
975
+
976
+ @media (max-width: 768px) {
977
+ .wiki-layout {
978
+ grid-template-columns: 1fr;
979
+ }
980
+
981
+ .mobile-header {
982
+ display: flex;
983
+ }
984
+
985
+ .sidebar-left {
986
+ display: none;
987
+ position: fixed;
988
+ left: 0;
989
+ top: 0;
990
+ bottom: 0;
991
+ width: var(--sidebar-width);
992
+ z-index: 1001;
993
+ transform: translateX(-100%);
994
+ transition: transform 0.25s ease;
995
+ background: var(--bg-secondary);
996
+ border-right: 1px solid var(--border-subtle);
997
+ }
998
+
999
+ .sidebar-left.mobile-open {
1000
+ display: flex;
1001
+ transform: translateX(0);
1002
+ }
1003
+
1004
+ .content-area {
1005
+ padding-top: 56px;
1006
+ border-left: none;
1007
+ border-right: none;
1008
+ }
1009
+
1010
+ .content-wrapper {
1011
+ padding: 1.5rem 1.25rem;
1012
+ }
1013
+
1014
+ .article-title {
1015
+ font-size: 1.75rem;
1016
+ }
1017
+ }
1018
+
1019
+ /* ============================================
1020
+ HOVER PREVIEW
1021
+ ============================================ */
1022
+
1023
+ .page-preview {
1024
+ position: fixed;
1025
+ max-width: 400px;
1026
+ max-height: 300px;
1027
+ background: var(--bg-secondary);
1028
+ border: 1px solid var(--border);
1029
+ border-radius: 8px;
1030
+ padding: 1rem;
1031
+ box-shadow: 0 8px 24px var(--shadow);
1032
+ z-index: 10000;
1033
+ pointer-events: none;
1034
+ opacity: 0;
1035
+ transition: opacity 0.15s ease;
1036
+ overflow: hidden;
1037
+ cursor: pointer;
1038
+ }
1039
+
1040
+ .page-preview.active {
1041
+ opacity: 1;
1042
+ pointer-events: auto;
1043
+ }
1044
+
1045
+ .page-preview:hover {
1046
+ border-color: var(--accent);
1047
+ box-shadow: 0 8px 32px var(--shadow);
1048
+ }
1049
+
1050
+ .page-preview-title {
1051
+ font-size: 1rem;
1052
+ font-weight: 600;
1053
+ color: var(--text-primary);
1054
+ margin-bottom: 0.5rem;
1055
+ padding-bottom: 0.5rem;
1056
+ border-bottom: 1px solid var(--border-subtle);
1057
+ }
1058
+
1059
+ .page-preview-content {
1060
+ font-size: 0.85rem;
1061
+ line-height: 1.6;
1062
+ color: var(--text-secondary);
1063
+ max-height: 200px;
1064
+ overflow-y: auto;
1065
+ overflow-x: hidden;
1066
+ }
1067
+
1068
+ .page-preview-content::-webkit-scrollbar {
1069
+ width: 4px;
1070
+ }
1071
+
1072
+ .page-preview-content::-webkit-scrollbar-track {
1073
+ background: transparent;
1074
+ }
1075
+
1076
+ .page-preview-content::-webkit-scrollbar-thumb {
1077
+ background: var(--border);
1078
+ border-radius: 2px;
1079
+ }
1080
+
1081
+ /* Scale down content in preview */
1082
+ .page-preview-content h1,
1083
+ .page-preview-content h2,
1084
+ .page-preview-content h3 {
1085
+ font-size: 0.9rem;
1086
+ margin: 0.5rem 0 0.25rem;
1087
+ }
1088
+
1089
+ .page-preview-content p {
1090
+ margin-bottom: 0.5rem;
1091
+ }
1092
+
1093
+ .page-preview-content ul,
1094
+ .page-preview-content ol {
1095
+ margin: 0.5rem 0;
1096
+ padding-left: 1.5rem;
1097
+ }
1098
+
1099
+ .page-preview-content li {
1100
+ margin-bottom: 0.25rem;
1101
+ }
1102
+
1103
+ .page-preview-content code {
1104
+ font-size: 0.8em;
1105
+ }
1106
+
1107
+ .page-preview-content pre {
1108
+ font-size: 0.75rem;
1109
+ padding: 0.5rem;
1110
+ margin: 0.5rem 0;
1111
+ }
1112
+
1113
+ .page-preview-content a {
1114
+ color: var(--text-secondary);
1115
+ text-decoration: none;
1116
+ border-bottom: none;
1117
+ cursor: default;
1118
+ }
1119
+
1120
+ .page-preview-loading {
1121
+ color: var(--text-muted);
1122
+ font-style: italic;
1123
+ text-align: center;
1124
+ padding: 1rem;
1125
+ }
1126
+
1127
+ /* ============================================
1128
+ ANIMATIONS
1129
+ ============================================ */
1130
+
1131
+ @keyframes fadeIn {
1132
+ from { opacity: 0; }
1133
+ to { opacity: 1; }
1134
+ }
1135
+
1136
+ .content-wrapper {
1137
+ animation: fadeIn 0.2s ease;
1138
+ }
1139
+ `;
1140
+ const scripts = `
1141
+ // ============================================
1142
+ // TREE NAVIGATION
1143
+ // ============================================
1144
+
1145
+ async function initTree() {
1146
+ const container = document.getElementById('tree-nav');
1147
+ if (!container) return;
1148
+
1149
+ try {
1150
+ const res = await fetch('/api/tree');
1151
+ const data = await res.json();
1152
+ container.innerHTML = renderTree(data.tree);
1153
+
1154
+ // Add click handlers for folders
1155
+ container.querySelectorAll('.tree-folder-header').forEach(header => {
1156
+ header.addEventListener('click', (e) => {
1157
+ const item = header.closest('.tree-item');
1158
+ const children = item.querySelector('.tree-children');
1159
+ const chevron = header.querySelector('.tree-chevron');
1160
+
1161
+ if (children) {
1162
+ children.classList.toggle('expanded');
1163
+ chevron?.classList.toggle('expanded');
1164
+ }
1165
+ });
1166
+ });
1167
+
1168
+ // Expand path to current page
1169
+ const currentSlug = document.body.dataset.currentSlug;
1170
+ if (currentSlug) {
1171
+ const activeLink = container.querySelector(\`[href="/\${currentSlug}"]\`);
1172
+ if (activeLink) {
1173
+ activeLink.classList.add('active');
1174
+ // Expand all parent folders
1175
+ let parent = activeLink.closest('.tree-children');
1176
+ while (parent) {
1177
+ parent.classList.add('expanded');
1178
+ const header = parent.previousElementSibling;
1179
+ header?.querySelector('.tree-chevron')?.classList.add('expanded');
1180
+ parent = parent.parentElement?.closest('.tree-children');
1181
+ }
1182
+ }
1183
+ }
1184
+ } catch (err) {
1185
+ console.error('Failed to load tree:', err);
1186
+ }
1187
+ }
1188
+
1189
+ function renderTree(nodes) {
1190
+ if (!nodes || nodes.length === 0) return '';
1191
+
1192
+ return '<ul class="tree-list">' + nodes.map(node => {
1193
+ if (node.isFolder) {
1194
+ return \`
1195
+ <li class="tree-item">
1196
+ <div class="tree-folder-header">
1197
+ <svg class="tree-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1198
+ <polyline points="9 18 15 12 9 6"/>
1199
+ </svg>
1200
+ <svg class="tree-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1201
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
1202
+ </svg>
1203
+ <span class="tree-name">\${escapeHtml(node.name)}</span>
1204
+ </div>
1205
+ <div class="tree-children">
1206
+ \${renderTree(node.children)}
1207
+ </div>
1208
+ </li>
1209
+ \`;
1210
+ } else {
1211
+ return \`
1212
+ <li class="tree-item">
1213
+ <a href="/\${node.slug}" class="tree-page-link">
1214
+ <svg class="tree-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1215
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
1216
+ <polyline points="14 2 14 8 20 8"/>
1217
+ </svg>
1218
+ <span class="tree-name">\${escapeHtml(node.name)}</span>
1219
+ </a>
1220
+ </li>
1221
+ \`;
1222
+ }
1223
+ }).join('') + '</ul>';
1224
+ }
1225
+
1226
+ // ============================================
1227
+ // SEARCH
1228
+ // ============================================
1229
+
1230
+ function initSearch() {
1231
+ const searchInput = document.getElementById('search-input');
1232
+ const searchResults = document.getElementById('search-results');
1233
+ let debounceTimer;
1234
+
1235
+ searchInput?.addEventListener('input', (e) => {
1236
+ clearTimeout(debounceTimer);
1237
+ const query = e.target.value.trim();
1238
+
1239
+ if (!query) {
1240
+ searchResults.classList.remove('active');
1241
+ return;
1242
+ }
1243
+
1244
+ debounceTimer = setTimeout(async () => {
1245
+ try {
1246
+ const res = await fetch('/api/search?q=' + encodeURIComponent(query));
1247
+ const data = await res.json();
1248
+
1249
+ if (data.results.length > 0) {
1250
+ searchResults.innerHTML = data.results.map(r => \`
1251
+ <a href="/\${r.slug}" class="search-result-item">
1252
+ <div class="search-result-title">\${escapeHtml(r.title)}</div>
1253
+ <div class="search-result-path">\${escapeHtml(r.path)}</div>
1254
+ </a>
1255
+ \`).join('');
1256
+ searchResults.classList.add('active');
1257
+ } else {
1258
+ searchResults.innerHTML = '<div class="search-result-item"><div class="search-result-title">No results found</div></div>';
1259
+ searchResults.classList.add('active');
1260
+ }
1261
+ } catch (err) {
1262
+ console.error('Search error:', err);
1263
+ }
1264
+ }, 150);
1265
+ });
1266
+
1267
+ searchInput?.addEventListener('blur', () => {
1268
+ setTimeout(() => searchResults?.classList.remove('active'), 200);
1269
+ });
1270
+
1271
+ searchInput?.addEventListener('keydown', (e) => {
1272
+ if (e.key === 'Escape') {
1273
+ searchResults?.classList.remove('active');
1274
+ searchInput.blur();
1275
+ }
1276
+ });
1277
+
1278
+ // Keyboard shortcut for search
1279
+ document.addEventListener('keydown', (e) => {
1280
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1281
+ e.preventDefault();
1282
+ searchInput?.focus();
1283
+ }
1284
+ });
1285
+ }
1286
+
1287
+ // ============================================
1288
+ // THEME TOGGLE
1289
+ // ============================================
1290
+
1291
+ function initTheme() {
1292
+ const savedTheme = localStorage.getItem('theme');
1293
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1294
+ const theme = savedTheme || (prefersDark ? 'dark' : 'light');
1295
+
1296
+ document.documentElement.setAttribute('data-theme', theme);
1297
+ updateThemeButton(theme);
1298
+ }
1299
+
1300
+ function toggleTheme() {
1301
+ const current = document.documentElement.getAttribute('data-theme');
1302
+ const next = current === 'dark' ? 'light' : 'dark';
1303
+
1304
+ document.documentElement.setAttribute('data-theme', next);
1305
+ localStorage.setItem('theme', next);
1306
+ updateThemeButton(next);
1307
+ }
1308
+
1309
+ function updateThemeButton(theme) {
1310
+ const btn = document.getElementById('theme-toggle');
1311
+ if (!btn) return;
1312
+
1313
+ const sunIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
1314
+ const moonIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
1315
+
1316
+ btn.innerHTML = (theme === 'dark' ? sunIcon : moonIcon) + '<span>' + (theme === 'dark' ? 'Light mode' : 'Dark mode') + '</span>';
1317
+ }
1318
+
1319
+ // ============================================
1320
+ // MOBILE NAVIGATION
1321
+ // ============================================
1322
+
1323
+ function initMobile() {
1324
+ const menuBtn = document.getElementById('mobile-menu-btn');
1325
+ const backlinksBtn = document.getElementById('mobile-backlinks-btn');
1326
+ const sidebarLeft = document.querySelector('.sidebar-left');
1327
+ const sidebarRight = document.querySelector('.sidebar-right');
1328
+ const overlay = document.getElementById('sidebar-overlay');
1329
+
1330
+ menuBtn?.addEventListener('click', () => {
1331
+ // Close right sidebar if open
1332
+ sidebarRight?.classList.remove('mobile-open');
1333
+ // Toggle left sidebar
1334
+ sidebarLeft?.classList.toggle('mobile-open');
1335
+ overlay?.classList.toggle('active', sidebarLeft?.classList.contains('mobile-open'));
1336
+ });
1337
+
1338
+ backlinksBtn?.addEventListener('click', () => {
1339
+ // Close left sidebar if open
1340
+ sidebarLeft?.classList.remove('mobile-open');
1341
+ // Toggle right sidebar
1342
+ sidebarRight?.classList.toggle('mobile-open');
1343
+ overlay?.classList.toggle('active', sidebarRight?.classList.contains('mobile-open'));
1344
+ });
1345
+
1346
+ overlay?.addEventListener('click', () => {
1347
+ sidebarLeft?.classList.remove('mobile-open');
1348
+ sidebarRight?.classList.remove('mobile-open');
1349
+ overlay.classList.remove('active');
1350
+ });
1351
+ }
1352
+
1353
+ // ============================================
1354
+ // UTILITIES
1355
+ // ============================================
1356
+
1357
+ function escapeHtml(text) {
1358
+ const div = document.createElement('div');
1359
+ div.textContent = text;
1360
+ return div.innerHTML;
1361
+ }
1362
+
1363
+ // ============================================
1364
+ // TABLE OF CONTENTS / SCROLL SPY
1365
+ // ============================================
1366
+
1367
+ function initTOC() {
1368
+ const tocContainer = document.getElementById('toc-content');
1369
+ const contentArea = document.querySelector('.content-area');
1370
+ if (!tocContainer || !contentArea) return;
1371
+
1372
+ // Get all headings from the article content
1373
+ const article = document.querySelector('.article .content');
1374
+ if (!article) return;
1375
+
1376
+ const headings = article.querySelectorAll('h1, h2, h3, h4, h5, h6');
1377
+ if (headings.length === 0) {
1378
+ tocContainer.innerHTML = '<p class="widget-empty">No headings</p>';
1379
+ return;
1380
+ }
1381
+
1382
+ // Build TOC
1383
+ let tocHtml = '<ul class="toc-list">';
1384
+ headings.forEach((heading, index) => {
1385
+ const id = heading.id || 'heading-' + index;
1386
+ heading.id = id;
1387
+ const level = parseInt(heading.tagName.charAt(1));
1388
+ const text = heading.textContent || '';
1389
+ tocHtml += \`<li class="toc-item"><a href="#\${id}" class="toc-link" data-level="\${level}">\${escapeHtml(text)}</a></li>\`;
1390
+ });
1391
+ tocHtml += '</ul>';
1392
+ tocContainer.innerHTML = tocHtml;
1393
+
1394
+ // Update heading count
1395
+ const countEl = document.getElementById('toc-count');
1396
+ if (countEl) countEl.textContent = headings.length.toString();
1397
+
1398
+ // Scroll spy
1399
+ const tocLinks = tocContainer.querySelectorAll('.toc-link');
1400
+ let ticking = false;
1401
+
1402
+ function updateActiveHeading() {
1403
+ const scrollTop = contentArea.scrollTop;
1404
+ const scrollHeight = contentArea.scrollHeight;
1405
+ const clientHeight = contentArea.clientHeight;
1406
+ const offset = 100;
1407
+
1408
+ // Check if we're at the bottom of the page (within 10px)
1409
+ const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10;
1410
+
1411
+ let activeIndex = 0;
1412
+
1413
+ if (isAtBottom && headings.length > 0) {
1414
+ // If at bottom, highlight the last heading
1415
+ activeIndex = headings.length - 1;
1416
+ } else {
1417
+ headings.forEach((heading, index) => {
1418
+ const rect = heading.getBoundingClientRect();
1419
+ const contentRect = contentArea.getBoundingClientRect();
1420
+ const relativeTop = rect.top - contentRect.top;
1421
+
1422
+ if (relativeTop < offset) {
1423
+ activeIndex = index;
1424
+ }
1425
+ });
1426
+ }
1427
+
1428
+ tocLinks.forEach((link, index) => {
1429
+ link.classList.toggle('active', index === activeIndex);
1430
+ });
1431
+ }
1432
+
1433
+ contentArea.addEventListener('scroll', () => {
1434
+ if (!ticking) {
1435
+ requestAnimationFrame(() => {
1436
+ updateActiveHeading();
1437
+ ticking = false;
1438
+ });
1439
+ ticking = true;
1440
+ }
1441
+ });
1442
+
1443
+ // Smooth scroll on click
1444
+ tocLinks.forEach(link => {
1445
+ link.addEventListener('click', (e) => {
1446
+ e.preventDefault();
1447
+ const targetId = link.getAttribute('href')?.slice(1);
1448
+ const target = document.getElementById(targetId || '');
1449
+ if (target) {
1450
+ const contentRect = contentArea.getBoundingClientRect();
1451
+ const targetRect = target.getBoundingClientRect();
1452
+ const scrollTop = contentArea.scrollTop + (targetRect.top - contentRect.top) - 80;
1453
+ contentArea.scrollTo({ top: scrollTop, behavior: 'smooth' });
1454
+ }
1455
+ });
1456
+ });
1457
+
1458
+ // Initial highlight
1459
+ updateActiveHeading();
1460
+ }
1461
+
1462
+ // ============================================
1463
+ // HOVER PREVIEW
1464
+ // ============================================
1465
+
1466
+ let previewPopover = null;
1467
+ let previewTimeout = null;
1468
+ let hideTimeout = null;
1469
+ let currentPreviewLink = null;
1470
+ let currentPreviewSlug = null;
1471
+ let isOverPreview = false;
1472
+ let isOverLink = false;
1473
+ const previewCache = new Map();
1474
+
1475
+ function initHoverPreview() {
1476
+ // Create preview popover element
1477
+ previewPopover = document.createElement('div');
1478
+ previewPopover.className = 'page-preview';
1479
+ previewPopover.innerHTML = '<div class="page-preview-loading">Loading...</div>';
1480
+ document.body.appendChild(previewPopover);
1481
+
1482
+ // Add hover listeners to all wiki links
1483
+ document.addEventListener('mouseover', handleLinkHover);
1484
+ document.addEventListener('mouseout', handleLinkOut);
1485
+
1486
+ // Handle hovering over the preview itself
1487
+ previewPopover.addEventListener('mouseenter', () => {
1488
+ isOverPreview = true;
1489
+ clearTimeout(hideTimeout);
1490
+ });
1491
+
1492
+ previewPopover.addEventListener('mouseleave', () => {
1493
+ isOverPreview = false;
1494
+ scheduleHide();
1495
+ });
1496
+
1497
+ // Handle clicking the preview
1498
+ previewPopover.addEventListener('click', (e) => {
1499
+ // Don't navigate if clicking a link inside (we'll prevent that separately)
1500
+ if (e.target.tagName === 'A') return;
1501
+
1502
+ // Navigate to the page
1503
+ if (currentPreviewSlug) {
1504
+ window.location.href = '/' + currentPreviewSlug;
1505
+ }
1506
+ });
1507
+ }
1508
+
1509
+ function handleLinkHover(e) {
1510
+ const link = e.target.closest('a[href^="/"]');
1511
+ if (!link || link.closest('.page-preview')) return;
1512
+
1513
+ // Ignore tree navigation links
1514
+ if (link.classList.contains('tree-page-link')) return;
1515
+
1516
+ // Ignore if it's a heading anchor
1517
+ if (link.getAttribute('href').includes('#')) return;
1518
+
1519
+ isOverLink = true;
1520
+ currentPreviewLink = link;
1521
+
1522
+ // Cancel any pending hide
1523
+ clearTimeout(hideTimeout);
1524
+
1525
+ // Show preview after delay
1526
+ clearTimeout(previewTimeout);
1527
+ previewTimeout = setTimeout(() => {
1528
+ if (currentPreviewLink === link && isOverLink) {
1529
+ showPreview(link);
1530
+ }
1531
+ }, 300);
1532
+ }
1533
+
1534
+ function handleLinkOut(e) {
1535
+ const link = e.target.closest('a[href^="/"]');
1536
+ if (link === currentPreviewLink) {
1537
+ isOverLink = false;
1538
+ clearTimeout(previewTimeout);
1539
+ scheduleHide();
1540
+ }
1541
+ }
1542
+
1543
+ function scheduleHide() {
1544
+ clearTimeout(hideTimeout);
1545
+ hideTimeout = setTimeout(() => {
1546
+ if (!isOverLink && !isOverPreview) {
1547
+ hidePreview();
1548
+ currentPreviewLink = null;
1549
+ currentPreviewSlug = null;
1550
+ }
1551
+ }, 200);
1552
+ }
1553
+
1554
+ async function showPreview(link) {
1555
+ const href = link.getAttribute('href');
1556
+ const slug = href.substring(1); // Remove leading /
1557
+
1558
+ if (!slug) return;
1559
+
1560
+ currentPreviewSlug = slug;
1561
+
1562
+ // Position the preview
1563
+ const rect = link.getBoundingClientRect();
1564
+ const previewWidth = 400;
1565
+ const previewMaxHeight = 300;
1566
+
1567
+ // Try to position below the link, but flip if too close to bottom
1568
+ let top = rect.bottom + window.scrollY + 8;
1569
+ let left = rect.left + window.scrollX;
1570
+
1571
+ // Adjust if too close to right edge
1572
+ if (left + previewWidth > window.innerWidth) {
1573
+ left = window.innerWidth - previewWidth - 20;
1574
+ }
1575
+
1576
+ // Adjust if too close to bottom
1577
+ if (rect.bottom + previewMaxHeight > window.innerHeight) {
1578
+ top = rect.top + window.scrollY - previewMaxHeight - 8;
1579
+ }
1580
+
1581
+ previewPopover.style.left = left + 'px';
1582
+ previewPopover.style.top = top + 'px';
1583
+
1584
+ // Check cache first
1585
+ if (previewCache.has(slug)) {
1586
+ const cached = previewCache.get(slug);
1587
+ renderPreview(cached, slug);
1588
+ previewPopover.classList.add('active');
1589
+ return;
1590
+ }
1591
+
1592
+ // Show loading state
1593
+ previewPopover.innerHTML = '<div class="page-preview-loading">Loading...</div>';
1594
+ previewPopover.classList.add('active');
1595
+
1596
+ try {
1597
+ const response = await fetch('/api/preview/' + slug);
1598
+ if (!response.ok) throw new Error('Preview not found');
1599
+
1600
+ const data = await response.json();
1601
+
1602
+ // Cache the result
1603
+ previewCache.set(slug, data);
1604
+
1605
+ // Only render if we're still hovering the same link
1606
+ if (currentPreviewLink && currentPreviewLink.getAttribute('href') === href) {
1607
+ renderPreview(data, slug);
1608
+ }
1609
+ } catch (error) {
1610
+ if (currentPreviewLink && currentPreviewLink.getAttribute('href') === href) {
1611
+ previewPopover.innerHTML = '<div class="page-preview-loading">Preview not available</div>';
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ function renderPreview(data, slug) {
1617
+ previewPopover.innerHTML = \`
1618
+ <div class="page-preview-title">\${escapeHtml(data.title)}</div>
1619
+ <div class="page-preview-content">\${data.content}</div>
1620
+ \`;
1621
+
1622
+ // Disable all links inside the preview
1623
+ const links = previewPopover.querySelectorAll('a');
1624
+ links.forEach(link => {
1625
+ link.addEventListener('click', (e) => {
1626
+ e.preventDefault();
1627
+ e.stopPropagation();
1628
+ });
1629
+ link.style.pointerEvents = 'none';
1630
+ });
1631
+ }
1632
+
1633
+ function hidePreview() {
1634
+ clearTimeout(previewTimeout);
1635
+ previewPopover.classList.remove('active');
1636
+ }
1637
+
1638
+ // ============================================
1639
+ // INIT
1640
+ // ============================================
1641
+
1642
+ document.addEventListener('DOMContentLoaded', () => {
1643
+ initTheme();
1644
+ initTree();
1645
+ initSearch();
1646
+ initMobile();
1647
+ initTOC();
1648
+ initHoverPreview();
1649
+ });
1650
+ `;
1651
+ function layout(title, content, options = {}) {
1652
+ const { vaultName = 'Wiki', currentSlug = '', backlinks = [], frontmatter = {}, lastModified } = options;
1653
+ const backlinksHtml = backlinks.length > 0
1654
+ ? `<ul class="backlinks-list">
1655
+ ${backlinks.map(bl => `<li><a href="/${bl.slug}">${escapeHtml(bl.title)}</a></li>`).join('')}
1656
+ </ul>`
1657
+ : '<p class="widget-empty">No pages link here</p>';
1658
+ // Build properties widget
1659
+ const propertyEntries = [];
1660
+ // Add last modified if available
1661
+ if (lastModified) {
1662
+ propertyEntries.push({
1663
+ key: 'Modified',
1664
+ value: lastModified.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
1665
+ });
1666
+ }
1667
+ // Process frontmatter
1668
+ for (const [key, value] of Object.entries(frontmatter)) {
1669
+ if (key === 'title' || key === 'aliases' || key === 'alias')
1670
+ continue; // Skip these
1671
+ let displayValue = '';
1672
+ if (key === 'tags' && Array.isArray(value)) {
1673
+ displayValue = value.map(tag => `<span class="properties-tag">${escapeHtml(String(tag))}</span>`).join('');
1674
+ }
1675
+ else if (Array.isArray(value)) {
1676
+ displayValue = value.map(v => {
1677
+ const str = String(v);
1678
+ return str.includes('[[') ? renderWikiLinks(str) : escapeHtml(str);
1679
+ }).join(', ');
1680
+ }
1681
+ else if (typeof value === 'object' && value !== null) {
1682
+ displayValue = escapeHtml(JSON.stringify(value));
1683
+ }
1684
+ else if (typeof value === 'boolean') {
1685
+ displayValue = value ? 'Yes' : 'No';
1686
+ }
1687
+ else {
1688
+ const stringValue = String(value);
1689
+ // Check if the value contains wiki links and render them
1690
+ displayValue = stringValue.includes('[[')
1691
+ ? renderWikiLinks(stringValue)
1692
+ : escapeHtml(stringValue);
1693
+ }
1694
+ // Capitalize first letter of key
1695
+ const displayKey = key.charAt(0).toUpperCase() + key.slice(1);
1696
+ propertyEntries.push({ key: displayKey, value: displayValue });
1697
+ }
1698
+ const propertiesHtml = propertyEntries.length > 0
1699
+ ? `<table class="properties-table">
1700
+ ${propertyEntries.map(({ key, value }) => `
1701
+ <tr>
1702
+ <td class="properties-key">${escapeHtml(key)}</td>
1703
+ <td class="properties-value">${value}</td>
1704
+ </tr>
1705
+ `).join('')}
1706
+ </table>`
1707
+ : '<p class="widget-empty">No properties</p>';
1708
+ return `<!DOCTYPE html>
1709
+ <html lang="en">
1710
+ <head>
1711
+ <meta charset="UTF-8">
1712
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1713
+ <title>${escapeHtml(title)} - ${escapeHtml(vaultName)}</title>
1714
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
1715
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1716
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1717
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;0,6..72,700;1,6..72,400&display=swap" rel="stylesheet">
1718
+ <style>${styles}</style>
1719
+ </head>
1720
+ <body data-current-slug="${escapeHtml(currentSlug)}">
1721
+ <!-- Mobile Header -->
1722
+ <header class="mobile-header">
1723
+ <button id="mobile-menu-btn" class="mobile-menu-btn" aria-label="Open navigation">
1724
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1725
+ <line x1="3" y1="12" x2="21" y2="12"/>
1726
+ <line x1="3" y1="6" x2="21" y2="6"/>
1727
+ <line x1="3" y1="18" x2="21" y2="18"/>
1728
+ </svg>
1729
+ </button>
1730
+ <span class="mobile-title">${escapeHtml(vaultName)}</span>
1731
+ <button id="mobile-backlinks-btn" class="mobile-backlinks-btn" aria-label="Open backlinks">
1732
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1733
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
1734
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
1735
+ </svg>
1736
+ </button>
1737
+ </header>
1738
+
1739
+ <!-- Sidebar Overlay -->
1740
+ <div id="sidebar-overlay" class="sidebar-overlay"></div>
1741
+
1742
+ <div class="wiki-layout">
1743
+ <!-- Left Sidebar -->
1744
+ <aside class="sidebar-left">
1745
+ <div class="sidebar-header">
1746
+ <div class="vault-title">
1747
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1748
+ <path d="M12 2L2 7l10 5 10-5-10-5z"/>
1749
+ <path d="M2 17l10 5 10-5"/>
1750
+ <path d="M2 12l10 5 10-5"/>
1751
+ </svg>
1752
+ ${escapeHtml(vaultName)}
1753
+ </div>
1754
+ <div class="search-container">
1755
+ <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1756
+ <circle cx="11" cy="11" r="8"/>
1757
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
1758
+ </svg>
1759
+ <input type="text" id="search-input" class="search-input" placeholder="Search... (⌘K)" autocomplete="off">
1760
+ <div id="search-results" class="search-results"></div>
1761
+ </div>
1762
+ </div>
1763
+ <nav id="tree-nav" class="tree-container">
1764
+ <div style="padding: 1rem; color: var(--text-muted); font-size: 0.85rem;">Loading...</div>
1765
+ </nav>
1766
+ </aside>
1767
+
1768
+ <!-- Main Content -->
1769
+ <main class="content-area">
1770
+ <div class="content-wrapper">
1771
+ ${content}
1772
+ </div>
1773
+ </main>
1774
+
1775
+ <!-- Right Sidebar - Widgets -->
1776
+ <aside class="sidebar-right">
1777
+ <!-- Properties Widget -->
1778
+ <div class="widget">
1779
+ <div class="widget-header">
1780
+ <span class="widget-title">Properties</span>
1781
+ </div>
1782
+ <div class="widget-content">
1783
+ ${propertiesHtml}
1784
+ </div>
1785
+ </div>
1786
+
1787
+ <!-- On This Page Widget -->
1788
+ <div class="widget">
1789
+ <div class="widget-header">
1790
+ <span class="widget-title">On This Page</span>
1791
+ <span class="widget-count" id="toc-count"></span>
1792
+ </div>
1793
+ <div class="widget-content" id="toc-content">
1794
+ <p class="widget-empty">Loading...</p>
1795
+ </div>
1796
+ </div>
1797
+
1798
+ <!-- Backlinks Widget -->
1799
+ <div class="widget">
1800
+ <div class="widget-header">
1801
+ <span class="widget-title">Backlinks</span>
1802
+ <span class="widget-count">${backlinks.length || ''}</span>
1803
+ </div>
1804
+ <div class="widget-content">
1805
+ ${backlinksHtml}
1806
+ </div>
1807
+ </div>
1808
+
1809
+ <div class="theme-toggle-container">
1810
+ <button id="theme-toggle" class="theme-toggle" onclick="toggleTheme()">
1811
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
1812
+ <span>Light mode</span>
1813
+ </button>
1814
+ </div>
1815
+ </aside>
1816
+ </div>
1817
+
1818
+ <script>${scripts}</script>
1819
+ </body>
1820
+ </html>`;
1821
+ }
1822
+ export function renderPage(options) {
1823
+ const { title, content, frontmatter, backlinks = [], slug, lastModified, vaultName } = options;
1824
+ const articleContent = `
1825
+ <article class="article">
1826
+ <header class="article-header">
1827
+ <h1 class="article-title">${escapeHtml(title)}</h1>
1828
+ </header>
1829
+ <div class="content">
1830
+ ${content}
1831
+ </div>
1832
+ </article>
1833
+ `;
1834
+ return layout(title, articleContent, { vaultName, currentSlug: slug, backlinks, frontmatter, lastModified });
1835
+ }
1836
+ export function renderHomePage(options) {
1837
+ const { pageCount, recentPages, allPages, vaultName } = options;
1838
+ // Group pages by folder
1839
+ const folders = new Map();
1840
+ for (const page of allPages) {
1841
+ const parts = page.relativePath.split('/');
1842
+ const folder = parts.length > 1 ? parts[0] : 'Root';
1843
+ if (!folders.has(folder)) {
1844
+ folders.set(folder, []);
1845
+ }
1846
+ folders.get(folder).push(page);
1847
+ }
1848
+ const sortedFolders = Array.from(folders.entries()).sort((a, b) => a[0].localeCompare(b[0]));
1849
+ const content = `
1850
+ <div class="home-header">
1851
+ <h1 class="home-title">${escapeHtml(vaultName || 'Wiki')}</h1>
1852
+ <p class="home-stats">${pageCount} pages</p>
1853
+ </div>
1854
+
1855
+ <h2 class="section-title">Recently Modified</h2>
1856
+ <div class="page-grid">
1857
+ ${recentPages.slice(0, 8).map(page => `
1858
+ <a href="/${page.slug}" class="page-card">
1859
+ <div class="page-card-title">${escapeHtml(page.title)}</div>
1860
+ <div class="page-card-path">${escapeHtml(page.relativePath)}</div>
1861
+ </a>
1862
+ `).join('')}
1863
+ </div>
1864
+
1865
+ ${sortedFolders.map(([folder, pages]) => `
1866
+ <h2 class="section-title">${escapeHtml(folder)}</h2>
1867
+ <div class="page-grid">
1868
+ ${pages.slice(0, 12).map(page => `
1869
+ <a href="/${page.slug}" class="page-card">
1870
+ <div class="page-card-title">${escapeHtml(page.title)}</div>
1871
+ <div class="page-card-path">${escapeHtml(page.relativePath)}</div>
1872
+ </a>
1873
+ `).join('')}
1874
+ ${pages.length > 12 ? `<div class="page-card" style="opacity: 0.6; text-align: center;">+${pages.length - 12} more</div>` : ''}
1875
+ </div>
1876
+ `).join('')}
1877
+ `;
1878
+ return layout('Home', content, { vaultName });
1879
+ }
1880
+ export function render404Page(slug, vaultName) {
1881
+ const content = `
1882
+ <div class="error-page">
1883
+ <div class="error-code">404</div>
1884
+ <p class="error-message">Page not found: <code>${escapeHtml(decodeURIComponent(slug))}</code></p>
1885
+ <p>This page doesn't exist yet.</p>
1886
+ <p><a href="/" class="error-link">← Back to home</a></p>
1887
+ </div>
1888
+ `;
1889
+ return layout('Page Not Found', content, { vaultName });
1890
+ }
1891
+ function escapeHtml(text) {
1892
+ return text
1893
+ .replace(/&/g, '&amp;')
1894
+ .replace(/</g, '&lt;')
1895
+ .replace(/>/g, '&gt;')
1896
+ .replace(/"/g, '&quot;')
1897
+ .replace(/'/g, '&#039;');
1898
+ }
1899
+ /**
1900
+ * Convert wiki links [[Page]] or [[Page|Alias]] to HTML links
1901
+ */
1902
+ function renderWikiLinks(text) {
1903
+ // Match [[Page]] or [[Page|Alias]]
1904
+ return text.replace(/\[\[([^\]]+?)\]\]/g, (match, content) => {
1905
+ const parts = content.split('|');
1906
+ const pageName = parts[0].trim();
1907
+ const displayText = parts[1] ? parts[1].trim() : pageName;
1908
+ // Convert page name to slug (simple version - just URL encode)
1909
+ const slug = encodeURIComponent(pageName);
1910
+ return `<a href="/${slug}">${escapeHtml(displayText)}</a>`;
1911
+ });
1912
+ }
1913
+ //# sourceMappingURL=page.js.map