pi-para 0.6.7 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/ARCHITECTURE.md +4 -6
  2. package/CHANGELOG.md +19 -0
  3. package/CONFIGURATION.md +1 -16
  4. package/README.md +2 -13
  5. package/SETUP.md +1 -7
  6. package/STRUCTURE.md +7 -18
  7. package/dist/cli-legacy.d.ts +2 -1
  8. package/dist/cli-legacy.d.ts.map +1 -1
  9. package/dist/cli-legacy.js +13 -25
  10. package/dist/cli-legacy.js.map +1 -1
  11. package/dist/cli.d.ts +2 -24
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +29 -271
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +1 -28
  17. package/dist/commands.js.map +1 -1
  18. package/dist/config.d.ts +1 -5
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +0 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -64
  24. package/dist/index.js.map +1 -1
  25. package/dist/link-utils.d.ts +1 -1
  26. package/dist/link-utils.js +1 -1
  27. package/dist/processor.d.ts +7 -0
  28. package/dist/processor.d.ts.map +1 -1
  29. package/dist/processor.js +29 -15
  30. package/dist/processor.js.map +1 -1
  31. package/dist/settings.d.ts +0 -3
  32. package/dist/settings.d.ts.map +1 -1
  33. package/dist/settings.js +0 -11
  34. package/dist/settings.js.map +1 -1
  35. package/dist/tools/edit-read.d.ts +18 -0
  36. package/dist/tools/edit-read.d.ts.map +1 -0
  37. package/dist/tools/edit-read.js +161 -0
  38. package/dist/tools/edit-read.js.map +1 -0
  39. package/dist/tools/ingest.d.ts +12 -0
  40. package/dist/tools/ingest.d.ts.map +1 -0
  41. package/dist/tools/ingest.js +48 -0
  42. package/dist/tools/ingest.js.map +1 -0
  43. package/dist/tools/lint.d.ts +10 -0
  44. package/dist/tools/lint.d.ts.map +1 -0
  45. package/dist/tools/lint.js +57 -0
  46. package/dist/tools/lint.js.map +1 -0
  47. package/dist/tools/migrate.d.ts +6 -0
  48. package/dist/tools/migrate.d.ts.map +1 -0
  49. package/dist/tools/migrate.js +36 -0
  50. package/dist/tools/migrate.js.map +1 -0
  51. package/dist/tools/move.d.ts +9 -0
  52. package/dist/tools/move.d.ts.map +1 -0
  53. package/dist/tools/move.js +61 -0
  54. package/dist/tools/move.js.map +1 -0
  55. package/dist/tools/query.d.ts +12 -0
  56. package/dist/tools/query.d.ts.map +1 -0
  57. package/dist/tools/query.js +47 -0
  58. package/dist/tools/query.js.map +1 -0
  59. package/dist/tools/schemas.d.ts +109 -0
  60. package/dist/tools/schemas.d.ts.map +1 -0
  61. package/dist/tools/schemas.js +80 -0
  62. package/dist/tools/schemas.js.map +1 -0
  63. package/dist/tools/shared.d.ts +14 -0
  64. package/dist/tools/shared.d.ts.map +1 -0
  65. package/dist/tools/shared.js +25 -0
  66. package/dist/tools/shared.js.map +1 -0
  67. package/dist/tools/summarize.d.ts +8 -0
  68. package/dist/tools/summarize.d.ts.map +1 -0
  69. package/dist/tools/summarize.js +82 -0
  70. package/dist/tools/summarize.js.map +1 -0
  71. package/dist/tools/write.d.ts +23 -0
  72. package/dist/tools/write.d.ts.map +1 -0
  73. package/dist/tools/write.js +215 -0
  74. package/dist/tools/write.js.map +1 -0
  75. package/dist/tools.d.ts +1 -8
  76. package/dist/tools.d.ts.map +1 -1
  77. package/dist/tools.js +13 -795
  78. package/dist/tools.js.map +1 -1
  79. package/dist/wiki.d.ts.map +1 -1
  80. package/dist/wiki.js +1 -0
  81. package/dist/wiki.js.map +1 -1
  82. package/package.json +2 -6
  83. package/src/cli-legacy.ts +14 -26
  84. package/src/cli.ts +29 -293
  85. package/src/commands.ts +0 -21
  86. package/src/config.ts +1 -6
  87. package/src/index.ts +5 -66
  88. package/src/link-utils.ts +1 -1
  89. package/src/processor.ts +28 -13
  90. package/src/settings.ts +0 -14
  91. package/src/tools/edit-read.ts +193 -0
  92. package/src/tools/ingest.ts +63 -0
  93. package/src/tools/lint.ts +74 -0
  94. package/src/tools/migrate.ts +44 -0
  95. package/src/tools/move.ts +79 -0
  96. package/src/tools/query.ts +63 -0
  97. package/src/tools/schemas.ts +157 -0
  98. package/src/tools/shared.ts +35 -0
  99. package/src/tools/summarize.ts +89 -0
  100. package/src/tools/write.ts +252 -0
  101. package/src/tools.ts +18 -1046
  102. package/src/wiki.ts +1 -0
  103. package/dist/daemon.d.ts +0 -49
  104. package/dist/daemon.d.ts.map +0 -1
  105. package/dist/daemon.js +0 -226
  106. package/dist/daemon.js.map +0 -1
  107. package/dist/link-discovery.d.ts +0 -40
  108. package/dist/link-discovery.d.ts.map +0 -1
  109. package/dist/link-discovery.js +0 -122
  110. package/dist/link-discovery.js.map +0 -1
  111. package/dist/maintainer.d.ts +0 -24
  112. package/dist/maintainer.d.ts.map +0 -1
  113. package/dist/maintainer.js +0 -358
  114. package/dist/maintainer.js.map +0 -1
  115. package/dist/watcher.d.ts +0 -25
  116. package/dist/watcher.d.ts.map +0 -1
  117. package/dist/watcher.js +0 -84
  118. package/dist/watcher.js.map +0 -1
  119. package/dist/webui/client/dist/index.html +0 -1885
  120. package/dist/webui/server.d.ts +0 -31
  121. package/dist/webui/server.d.ts.map +0 -1
  122. package/dist/webui/server.js +0 -566
  123. package/dist/webui/server.js.map +0 -1
  124. package/scripts/build-assets.mjs +0 -17
  125. package/src/daemon.ts +0 -288
  126. package/src/link-discovery.ts +0 -170
  127. package/src/maintainer.ts +0 -404
  128. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  129. package/src/python/minimax_lm.py +0 -198
  130. package/src/python/rlm_capture.py +0 -215
  131. package/src/watcher.ts +0 -101
  132. package/src/webui/client/dist/index.html +0 -1885
  133. package/src/webui/server.ts +0 -765
@@ -1,1885 +0,0 @@
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">
6
- <title>pi-para Wiki</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
- <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
11
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
12
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
13
- <script src="https://d3js.org/d3.v7.min.js"></script>
14
- <style>
15
- /* ============================================================
16
- DESIGN TOKENS
17
- Semantic color tokens, type scale, spacing scale, shadows.
18
- Dark-first with light-mode override via [data-theme="light"].
19
- ============================================================ */
20
- :root {
21
- /* --- Color tokens (dark, default) --- */
22
- --bg: #0f172a;
23
- --bg-alt: #1e293b;
24
- --surface: #334155;
25
- --surface-hover: #3e4d66;
26
- --text: #f1f5f9;
27
- --text-secondary: #94a3b8;
28
- --text-muted: #64748b;
29
- --accent: #60a5fa;
30
- --accent-hover: #93c5fd;
31
- --accent-subtle: rgba(96,165,250,0.12);
32
- --green: #34d399;
33
- --green-subtle: rgba(52,211,153,0.12);
34
- --red: #f87171;
35
- --red-subtle: rgba(248,113,113,0.12);
36
- --yellow: #fbbf24;
37
- --yellow-subtle: rgba(251,191,36,0.12);
38
- --border: #334155;
39
- --border-subtle: rgba(255,255,255,0.06);
40
- --ring: rgba(96,165,250,0.5);
41
- --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
42
- --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
43
- --shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
44
- --overlay: rgba(0,0,0,0.6);
45
-
46
- /* --- Typography --- */
47
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
48
- --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
49
-
50
- /* Type scale (major-third ~1.25) */
51
- --text-xs: 0.75rem; /* 12px */
52
- --text-sm: 0.875rem; /* 14px */
53
- --text-base: 1rem; /* 16px */
54
- --text-lg: 1.125rem; /* 18px */
55
- --text-xl: 1.25rem; /* 20px */
56
- --text-2xl: 1.5rem; /* 24px */
57
- --text-3xl: 1.875rem; /* 30px */
58
-
59
- --leading-tight: 1.25;
60
- --leading-normal: 1.6;
61
- --leading-relaxed: 1.75;
62
-
63
- /* --- Spacing (4px base) --- */
64
- --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
65
- --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;
66
- --sp-12: 48px; --sp-16: 64px;
67
-
68
- /* --- Radii --- */
69
- --radius-sm: 4px;
70
- --radius-md: 8px;
71
- --radius-lg: 12px;
72
- --radius-full: 9999px;
73
-
74
- /* --- Transitions --- */
75
- --duration-fast: 150ms;
76
- --duration-normal: 200ms;
77
- --duration-slow: 300ms;
78
- --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
79
- --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
80
- }
81
-
82
- /* --- Light mode override --- */
83
- [data-theme="light"] {
84
- --bg: #f8fafc;
85
- --bg-alt: #f1f5f9;
86
- --surface: #e2e8f0;
87
- --surface-hover: #cbd5e1;
88
- --text: #0f172a;
89
- --text-secondary: #475569;
90
- --text-muted: #94a3b8;
91
- --accent: #2563eb;
92
- --accent-hover: #1d4ed8;
93
- --accent-subtle: rgba(37,99,235,0.08);
94
- --green: #16a34a;
95
- --green-subtle: rgba(22,163,74,0.08);
96
- --red: #dc2626;
97
- --red-subtle: rgba(220,38,38,0.08);
98
- --yellow: #d97706;
99
- --yellow-subtle: rgba(217,119,6,0.08);
100
- --border: #e2e8f0;
101
- --border-subtle: rgba(0,0,0,0.04);
102
- --ring: rgba(37,99,235,0.4);
103
- --shadow-sm: 0 1px 2px rgba(0,0,0,0.06);
104
- --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
105
- --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
106
- --overlay: rgba(0,0,0,0.4);
107
- }
108
-
109
- /* ============================================================
110
- RESET & BASE
111
- ============================================================ */
112
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
113
-
114
- body {
115
- font-family: var(--font-sans);
116
- font-size: var(--text-base);
117
- line-height: var(--leading-normal);
118
- background: var(--bg);
119
- color: var(--text);
120
- -webkit-font-smoothing: antialiased;
121
- -moz-osx-font-smoothing: grayscale;
122
- }
123
-
124
- a { color: var(--accent); text-decoration: none; }
125
- a:hover { text-decoration: underline; }
126
-
127
- /* Global focus ring */
128
- :focus-visible {
129
- outline: 2px solid var(--ring);
130
- outline-offset: 2px;
131
- border-radius: var(--radius-sm);
132
- }
133
-
134
- /* Skip link */
135
- .skip-link {
136
- position: absolute;
137
- top: -100%;
138
- left: var(--sp-4);
139
- padding: var(--sp-2) var(--sp-4);
140
- background: var(--accent);
141
- color: var(--bg);
142
- border-radius: var(--radius-md);
143
- font-weight: 600;
144
- z-index: 1000;
145
- transition: top var(--duration-fast) var(--ease-out);
146
- }
147
- .skip-link:focus { top: var(--sp-2); }
148
-
149
- /* Reduced motion */
150
- @media (prefers-reduced-motion: reduce) {
151
- *, *::before, *::after {
152
- animation-duration: 0.01ms !important;
153
- transition-duration: 0.01ms !important;
154
- }
155
- }
156
-
157
- /* ============================================================
158
- LAYOUT SHELL
159
- ============================================================ */
160
- .app { display: flex; height: 100vh; overflow: hidden; }
161
-
162
- /* --- Sidebar --- */
163
- .sidebar {
164
- width: 280px;
165
- background: var(--bg-alt);
166
- border-right: 1px solid var(--border);
167
- overflow-y: auto;
168
- flex-shrink: 0;
169
- display: flex;
170
- flex-direction: column;
171
- transition: transform var(--duration-normal) var(--ease-out);
172
- }
173
- .sidebar-header {
174
- padding: var(--sp-4) var(--sp-4) var(--sp-3);
175
- display: flex;
176
- align-items: center;
177
- gap: var(--sp-3);
178
- border-bottom: 1px solid var(--border);
179
- flex-shrink: 0;
180
- }
181
- .sidebar-header .logo {
182
- display: flex;
183
- align-items: center;
184
- gap: var(--sp-2);
185
- }
186
- .sidebar-header .logo svg { color: var(--accent); flex-shrink: 0; }
187
- .sidebar-header .logo-text {
188
- font-size: var(--text-sm);
189
- font-weight: 700;
190
- letter-spacing: -0.01em;
191
- color: var(--text);
192
- }
193
- .sidebar-header .logo-sub {
194
- font-size: var(--text-xs);
195
- color: var(--text-muted);
196
- font-weight: 400;
197
- }
198
- .sidebar-nav { flex: 1; overflow-y: auto; padding: var(--sp-2) 0; }
199
-
200
- /* Sidebar sections */
201
- .sidebar-section { margin-bottom: var(--sp-1); }
202
- .sidebar-section-header {
203
- display: flex;
204
- align-items: center;
205
- gap: var(--sp-2);
206
- padding: var(--sp-2) var(--sp-4);
207
- font-size: var(--text-xs);
208
- font-weight: 600;
209
- color: var(--text-muted);
210
- text-transform: uppercase;
211
- letter-spacing: 0.05em;
212
- cursor: pointer;
213
- user-select: none;
214
- border: none;
215
- background: none;
216
- width: 100%;
217
- text-align: left;
218
- border-radius: 0;
219
- min-height: 36px;
220
- }
221
- .sidebar-section-header:hover { color: var(--text-secondary); }
222
- .sidebar-section-header svg {
223
- transition: transform var(--duration-fast) var(--ease-out);
224
- flex-shrink: 0;
225
- }
226
- .sidebar-section.open .sidebar-section-header svg {
227
- transform: rotate(90deg);
228
- }
229
- .sidebar-section-header .count {
230
- margin-left: auto;
231
- background: var(--surface);
232
- padding: 0 var(--sp-2);
233
- border-radius: var(--radius-full);
234
- font-size: 11px;
235
- line-height: 20px;
236
- min-width: 20px;
237
- text-align: center;
238
- }
239
-
240
- .sidebar-list { list-style: none; display: none; padding-bottom: var(--sp-2); }
241
- .sidebar-section.open .sidebar-list { display: block; }
242
-
243
- .sidebar-item {
244
- display: flex;
245
- align-items: center;
246
- gap: var(--sp-2);
247
- padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-8);
248
- cursor: pointer;
249
- font-size: var(--text-sm);
250
- color: var(--text-secondary);
251
- white-space: nowrap;
252
- overflow: hidden;
253
- text-overflow: ellipsis;
254
- border-radius: var(--radius-md);
255
- margin: 0 var(--sp-2);
256
- min-height: 36px;
257
- transition: background var(--duration-fast) var(--ease-out),
258
- color var(--duration-fast) var(--ease-out);
259
- }
260
- .sidebar-item:hover {
261
- background: var(--surface);
262
- color: var(--text);
263
- }
264
- .sidebar-item.active {
265
- background: var(--accent-subtle);
266
- color: var(--accent);
267
- font-weight: 500;
268
- }
269
-
270
- /* --- Top bar / Nav --- */
271
- .topbar {
272
- display: flex;
273
- align-items: center;
274
- gap: var(--sp-2);
275
- padding: var(--sp-2) var(--sp-4);
276
- background: var(--bg-alt);
277
- border-bottom: 1px solid var(--border);
278
- flex-shrink: 0;
279
- min-height: 52px;
280
- flex-wrap: wrap;
281
- }
282
-
283
- .nav-btn {
284
- display: inline-flex;
285
- align-items: center;
286
- gap: var(--sp-2);
287
- padding: var(--sp-2) var(--sp-3);
288
- background: transparent;
289
- border: 1px solid transparent;
290
- color: var(--text-secondary);
291
- border-radius: var(--radius-md);
292
- cursor: pointer;
293
- font-size: var(--text-sm);
294
- font-family: var(--font-sans);
295
- font-weight: 500;
296
- min-height: 36px;
297
- min-width: 36px;
298
- justify-content: center;
299
- transition: background var(--duration-fast) var(--ease-out),
300
- color var(--duration-fast) var(--ease-out),
301
- border-color var(--duration-fast) var(--ease-out);
302
- }
303
- .nav-btn:hover {
304
- background: var(--surface);
305
- color: var(--text);
306
- }
307
- .nav-btn.active {
308
- background: var(--accent-subtle);
309
- color: var(--accent);
310
- border-color: var(--accent);
311
- }
312
- .nav-btn svg { flex-shrink: 0; }
313
-
314
- /* Hamburger - hidden on desktop */
315
- .hamburger { display: none; }
316
-
317
- /* Search input in topbar */
318
- .search-wrapper {
319
- flex: 1;
320
- min-width: 180px;
321
- max-width: 400px;
322
- margin-left: auto;
323
- position: relative;
324
- }
325
- .search-wrapper svg {
326
- position: absolute;
327
- left: var(--sp-3);
328
- top: 50%;
329
- transform: translateY(-50%);
330
- color: var(--text-muted);
331
- pointer-events: none;
332
- }
333
- .search-input {
334
- width: 100%;
335
- padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-10);
336
- background: var(--bg);
337
- border: 1px solid var(--border);
338
- color: var(--text);
339
- border-radius: var(--radius-md);
340
- font-size: var(--text-sm);
341
- font-family: var(--font-sans);
342
- min-height: 36px;
343
- transition: border-color var(--duration-fast) var(--ease-out),
344
- box-shadow var(--duration-fast) var(--ease-out);
345
- }
346
- .search-input::placeholder { color: var(--text-muted); }
347
- .search-input:focus {
348
- outline: none;
349
- border-color: var(--accent);
350
- box-shadow: 0 0 0 3px var(--ring);
351
- }
352
-
353
- /* Scope filter */
354
- .scope-filter {
355
- display: inline-flex;
356
- align-items: center;
357
- gap: var(--sp-1);
358
- }
359
- .scope-filter select {
360
- padding: var(--sp-1) var(--sp-3) var(--sp-1) var(--sp-2);
361
- background: var(--bg);
362
- border: 1px solid var(--border);
363
- color: var(--text-secondary);
364
- border-radius: var(--radius-md);
365
- font-size: var(--text-xs);
366
- font-family: var(--font-sans);
367
- font-weight: 500;
368
- min-height: 32px;
369
- cursor: pointer;
370
- transition: border-color var(--duration-fast) var(--ease-out);
371
- appearance: none;
372
- padding-right: var(--sp-6);
373
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
374
- background-repeat: no-repeat;
375
- background-position: right 8px center;
376
- }
377
- .scope-filter select:focus {
378
- outline: none;
379
- border-color: var(--accent);
380
- box-shadow: 0 0 0 3px var(--ring);
381
- }
382
- .scope-filter-label {
383
- font-size: var(--text-xs);
384
- color: var(--text-muted);
385
- font-weight: 500;
386
- }
387
- .scope-badge {
388
- display: inline-flex;
389
- align-items: center;
390
- gap: var(--sp-1);
391
- padding: 1px var(--sp-2);
392
- background: var(--accent-subtle);
393
- color: var(--accent);
394
- border-radius: var(--radius-sm);
395
- font-size: 11px;
396
- font-weight: 600;
397
- margin-left: var(--sp-2);
398
- }
399
-
400
- /* Theme toggle */
401
- .theme-toggle {
402
- display: inline-flex;
403
- align-items: center;
404
- justify-content: center;
405
- width: 36px;
406
- height: 36px;
407
- background: transparent;
408
- border: 1px solid transparent;
409
- color: var(--text-secondary);
410
- border-radius: var(--radius-md);
411
- cursor: pointer;
412
- transition: background var(--duration-fast) var(--ease-out),
413
- color var(--duration-fast) var(--ease-out);
414
- }
415
- .theme-toggle:hover { background: var(--surface); color: var(--text); }
416
-
417
- /* --- Main content area --- */
418
- .main-wrapper {
419
- flex: 1;
420
- display: flex;
421
- flex-direction: column;
422
- overflow: hidden;
423
- min-width: 0;
424
- }
425
- .main-scroll {
426
- flex: 1;
427
- overflow-y: auto;
428
- scrollbar-width: thin;
429
- scrollbar-color: var(--surface) transparent;
430
- }
431
- .main-scroll::-webkit-scrollbar { width: 8px; }
432
- .main-scroll::-webkit-scrollbar-track { background: transparent; }
433
- .main-scroll::-webkit-scrollbar-thumb {
434
- background: var(--surface);
435
- border-radius: var(--radius-full);
436
- border: 2px solid transparent;
437
- background-clip: padding-box;
438
- }
439
- .main-scroll::-webkit-scrollbar-thumb:hover { background: var(--surface-hover); }
440
- .main {
441
- max-width: 860px;
442
- width: 100%;
443
- margin: 0 auto;
444
- padding: var(--sp-6) var(--sp-8);
445
- }
446
- .main.main-wide {
447
- max-width: none;
448
- }
449
-
450
- /* ============================================================
451
- RESPONSIVE
452
- ============================================================ */
453
- @media (max-width: 768px) {
454
- .sidebar {
455
- position: fixed;
456
- top: 0; left: 0; bottom: 0;
457
- z-index: 200;
458
- transform: translateX(-100%);
459
- box-shadow: var(--shadow-lg);
460
- }
461
- .sidebar.open { transform: translateX(0); }
462
- .hamburger { display: inline-flex; }
463
- .main { padding: var(--sp-4); }
464
- .topbar { padding: var(--sp-2) var(--sp-3); gap: var(--sp-1); }
465
- .topbar .scope-filter { display: none; }
466
- .search-wrapper { max-width: none; min-width: 120px; }
467
- }
468
- @media (max-width: 480px) {
469
- .nav-btn span.nav-label { display: none; }
470
- .main { padding: var(--sp-3); }
471
- }
472
-
473
- /* Overlay */
474
- .overlay {
475
- display: none;
476
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
477
- background: var(--overlay);
478
- z-index: 199;
479
- opacity: 0;
480
- transition: opacity var(--duration-normal) var(--ease-out);
481
- }
482
- .overlay.show { display: block; opacity: 1; }
483
-
484
- /* ============================================================
485
- CONTENT TYPOGRAPHY
486
- ============================================================ */
487
- .content { padding: var(--sp-4) 0; }
488
- .content h1 {
489
- font-size: var(--text-2xl);
490
- font-weight: 700;
491
- line-height: var(--leading-tight);
492
- margin-bottom: var(--sp-2);
493
- letter-spacing: -0.02em;
494
- }
495
- .content h2 {
496
- font-size: var(--text-xl);
497
- font-weight: 600;
498
- margin: var(--sp-6) 0 var(--sp-2);
499
- color: var(--accent);
500
- line-height: var(--leading-tight);
501
- }
502
- .content h3 {
503
- font-size: var(--text-lg);
504
- font-weight: 600;
505
- margin: var(--sp-4) 0 var(--sp-1);
506
- line-height: var(--leading-tight);
507
- }
508
- .content h4 {
509
- font-size: var(--text-base);
510
- font-weight: 600;
511
- margin: var(--sp-3) 0 var(--sp-1);
512
- }
513
- .content p { margin: var(--sp-2) 0; line-height: var(--leading-relaxed); }
514
- .content ul, .content ol { margin: var(--sp-2) 0 var(--sp-2) var(--sp-6); }
515
- .content li { margin: var(--sp-1) 0; line-height: var(--leading-relaxed); }
516
- .content code {
517
- background: var(--surface);
518
- padding: 2px 6px;
519
- border-radius: var(--radius-sm);
520
- font-family: var(--font-mono);
521
- font-size: 0.9em;
522
- }
523
- .content pre {
524
- background: var(--bg-alt);
525
- border: 1px solid var(--border);
526
- padding: var(--sp-4);
527
- border-radius: var(--radius-md);
528
- overflow-x: auto;
529
- margin: var(--sp-3) 0;
530
- }
531
- .content pre code { background: none; padding: 0; font-size: var(--text-sm); }
532
- .content blockquote {
533
- border-left: 3px solid var(--accent);
534
- padding-left: var(--sp-4);
535
- color: var(--text-secondary);
536
- margin: var(--sp-3) 0;
537
- font-style: italic;
538
- }
539
- .content a.wikilink {
540
- color: var(--accent);
541
- background: var(--accent-subtle);
542
- padding: 1px 4px;
543
- border-radius: var(--radius-sm);
544
- font-weight: 500;
545
- }
546
- .content a.wikilink:hover { background: var(--accent); color: var(--bg); text-decoration: none; }
547
-
548
- /* ============================================================
549
- COMPONENTS
550
- ============================================================ */
551
-
552
- /* --- Tags / Chips --- */
553
- .tags { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin: var(--sp-3) 0; }
554
- .tag {
555
- display: inline-flex;
556
- align-items: center;
557
- gap: var(--sp-1);
558
- background: var(--surface);
559
- padding: var(--sp-1) var(--sp-3);
560
- border-radius: var(--radius-full);
561
- font-size: var(--text-xs);
562
- font-weight: 500;
563
- color: var(--text-secondary);
564
- line-height: 1.5;
565
- }
566
- .tag.para {
567
- background: var(--accent-subtle);
568
- color: var(--accent);
569
- font-weight: 600;
570
- }
571
- .tag.scope { background: var(--green-subtle); color: var(--green); }
572
-
573
- /* --- Buttons --- */
574
- .btn {
575
- display: inline-flex;
576
- align-items: center;
577
- gap: var(--sp-2);
578
- padding: var(--sp-2) var(--sp-4);
579
- border: none;
580
- border-radius: var(--radius-md);
581
- cursor: pointer;
582
- font-size: var(--text-sm);
583
- font-weight: 500;
584
- font-family: var(--font-sans);
585
- min-height: 36px;
586
- transition: background var(--duration-fast) var(--ease-out),
587
- opacity var(--duration-fast) var(--ease-out),
588
- box-shadow var(--duration-fast) var(--ease-out);
589
- }
590
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
591
- .btn svg { flex-shrink: 0; }
592
-
593
- .btn-primary { background: var(--accent); color: #fff; }
594
- .btn-primary:hover:not(:disabled) { background: var(--accent-hover); box-shadow: var(--shadow-sm); }
595
-
596
- .btn-secondary {
597
- background: var(--surface);
598
- color: var(--text);
599
- border: 1px solid var(--border);
600
- }
601
- .btn-secondary:hover:not(:disabled) { background: var(--surface-hover); }
602
-
603
- .btn-danger { background: var(--red); color: #fff; }
604
- .btn-danger:hover:not(:disabled) { opacity: 0.9; }
605
-
606
- .btn-ghost {
607
- background: transparent;
608
- color: var(--text-secondary);
609
- }
610
- .btn-ghost:hover:not(:disabled) { background: var(--surface); color: var(--text); }
611
-
612
- /* Spinner inside buttons */
613
- .spinner {
614
- display: inline-block;
615
- width: 16px; height: 16px;
616
- border: 2px solid currentColor;
617
- border-right-color: transparent;
618
- border-radius: var(--radius-full);
619
- animation: spin 0.6s linear infinite;
620
- }
621
- @keyframes spin { to { transform: rotate(360deg); } }
622
-
623
- /* --- Cards --- */
624
- .card {
625
- background: var(--bg-alt);
626
- border: 1px solid var(--border);
627
- border-radius: var(--radius-lg);
628
- padding: var(--sp-4);
629
- transition: box-shadow var(--duration-fast) var(--ease-out),
630
- border-color var(--duration-fast) var(--ease-out);
631
- }
632
- .card:hover { border-color: var(--accent); box-shadow: var(--shadow-sm); }
633
-
634
- /* --- Page header --- */
635
- .page-header {
636
- display: flex;
637
- justify-content: space-between;
638
- align-items: flex-start;
639
- gap: var(--sp-4);
640
- flex-wrap: wrap;
641
- margin-bottom: var(--sp-2);
642
- }
643
- .page-header h1 { margin: 0; }
644
- .page-actions { display: flex; gap: var(--sp-2); flex-shrink: 0; }
645
-
646
- /* --- Editor --- */
647
- .editor textarea {
648
- width: 100%;
649
- min-height: 400px;
650
- background: var(--bg);
651
- color: var(--text);
652
- border: 1px solid var(--border);
653
- border-radius: var(--radius-md);
654
- padding: var(--sp-4);
655
- font-family: var(--font-mono);
656
- font-size: var(--text-sm);
657
- line-height: var(--leading-relaxed);
658
- resize: vertical;
659
- transition: border-color var(--duration-fast) var(--ease-out);
660
- }
661
- .editor textarea:focus {
662
- outline: none;
663
- border-color: var(--accent);
664
- box-shadow: 0 0 0 3px var(--ring);
665
- }
666
- .editor-actions {
667
- margin-top: var(--sp-3);
668
- display: flex;
669
- gap: var(--sp-2);
670
- align-items: center;
671
- }
672
-
673
- /* --- Search results --- */
674
- .search-results { list-style: none; }
675
- .search-result {
676
- padding: var(--sp-4);
677
- border: 1px solid var(--border);
678
- border-radius: var(--radius-md);
679
- margin-bottom: var(--sp-2);
680
- cursor: pointer;
681
- transition: border-color var(--duration-fast) var(--ease-out),
682
- background var(--duration-fast) var(--ease-out);
683
- }
684
- .search-result:hover {
685
- border-color: var(--accent);
686
- background: var(--accent-subtle);
687
- }
688
- .search-result-title {
689
- font-weight: 600;
690
- font-size: var(--text-base);
691
- color: var(--text);
692
- }
693
- .search-result-meta {
694
- display: flex;
695
- align-items: center;
696
- gap: var(--sp-2);
697
- margin-top: var(--sp-1);
698
- font-size: var(--text-xs);
699
- color: var(--text-muted);
700
- }
701
- .search-result-meta .score-bar {
702
- width: 40px; height: 4px;
703
- background: var(--surface);
704
- border-radius: var(--radius-full);
705
- overflow: hidden;
706
- }
707
- .search-result-meta .score-bar-fill {
708
- height: 100%;
709
- background: var(--accent);
710
- border-radius: var(--radius-full);
711
- }
712
- .search-result-snippet {
713
- font-size: var(--text-sm);
714
- color: var(--text-secondary);
715
- margin-top: var(--sp-2);
716
- line-height: var(--leading-relaxed);
717
- }
718
-
719
- /* --- Log entries --- */
720
- .log-entry {
721
- padding: var(--sp-3) 0;
722
- border-bottom: 1px solid var(--border-subtle);
723
- display: flex;
724
- align-items: baseline;
725
- gap: var(--sp-3);
726
- flex-wrap: wrap;
727
- }
728
- .log-date {
729
- font-size: var(--text-xs);
730
- font-weight: 600;
731
- color: var(--accent);
732
- font-family: var(--font-mono);
733
- flex-shrink: 0;
734
- }
735
- .log-op {
736
- font-size: var(--text-xs);
737
- font-weight: 600;
738
- padding: 1px var(--sp-2);
739
- border-radius: var(--radius-sm);
740
- background: var(--surface);
741
- color: var(--text-secondary);
742
- text-transform: uppercase;
743
- }
744
- .log-summary { font-size: var(--text-sm); color: var(--text); flex: 1; }
745
- .log-pages {
746
- font-size: var(--text-xs);
747
- color: var(--text-muted);
748
- width: 100%;
749
- padding-left: 0;
750
- }
751
-
752
- /* --- Session entries --- */
753
- .session-entry {
754
- padding: var(--sp-4);
755
- border: 1px solid var(--border);
756
- border-radius: var(--radius-md);
757
- margin-bottom: var(--sp-2);
758
- }
759
- .session-header {
760
- display: flex;
761
- justify-content: space-between;
762
- align-items: center;
763
- gap: var(--sp-3);
764
- }
765
- .session-project {
766
- font-weight: 600;
767
- font-size: var(--text-sm);
768
- color: var(--text);
769
- }
770
- .session-date {
771
- font-size: var(--text-xs);
772
- font-family: var(--font-mono);
773
- color: var(--text-muted);
774
- }
775
- .session-summary {
776
- font-size: var(--text-sm);
777
- color: var(--text-secondary);
778
- margin-top: var(--sp-2);
779
- line-height: var(--leading-relaxed);
780
- }
781
-
782
- /* --- Graph --- */
783
- .graph-container {
784
- width: 100%;
785
- height: calc(100vh - 180px);
786
- background: var(--bg-alt);
787
- border: 1px solid var(--border);
788
- border-radius: var(--radius-lg);
789
- overflow: hidden;
790
- position: relative;
791
- }
792
- .graph-container svg { width: 100%; height: 100%; }
793
- .graph-legend {
794
- position: absolute;
795
- bottom: var(--sp-4);
796
- left: var(--sp-4);
797
- display: flex;
798
- gap: var(--sp-4);
799
- padding: var(--sp-2) var(--sp-3);
800
- background: var(--bg);
801
- border: 1px solid var(--border);
802
- border-radius: var(--radius-md);
803
- font-size: var(--text-xs);
804
- color: var(--text-secondary);
805
- box-shadow: var(--shadow-md);
806
- }
807
- .graph-legend-item { display: flex; align-items: center; gap: var(--sp-1); }
808
- .graph-legend-dot {
809
- width: 10px; height: 10px;
810
- border-radius: var(--radius-full);
811
- flex-shrink: 0;
812
- }
813
-
814
- /* --- Move modal --- */
815
- .modal-backdrop {
816
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
817
- background: var(--overlay);
818
- z-index: 300;
819
- display: flex;
820
- align-items: center;
821
- justify-content: center;
822
- padding: var(--sp-4);
823
- }
824
- .modal {
825
- background: var(--bg-alt);
826
- border: 1px solid var(--border);
827
- padding: var(--sp-6);
828
- border-radius: var(--radius-lg);
829
- box-shadow: var(--shadow-lg);
830
- min-width: 320px;
831
- max-width: 440px;
832
- width: 100%;
833
- }
834
- .modal h3 {
835
- font-size: var(--text-lg);
836
- font-weight: 600;
837
- margin-bottom: var(--sp-4);
838
- }
839
- .modal select {
840
- width: 100%;
841
- padding: var(--sp-2) var(--sp-3);
842
- background: var(--bg);
843
- color: var(--text);
844
- border: 1px solid var(--border);
845
- border-radius: var(--radius-md);
846
- font-size: var(--text-sm);
847
- font-family: var(--font-sans);
848
- min-height: 36px;
849
- margin-bottom: var(--sp-4);
850
- }
851
- .modal select:focus {
852
- outline: none;
853
- border-color: var(--accent);
854
- box-shadow: 0 0 0 3px var(--ring);
855
- }
856
- .modal-actions {
857
- display: flex;
858
- gap: var(--sp-2);
859
- justify-content: flex-end;
860
- }
861
-
862
- /* --- Toast --- */
863
- .toast {
864
- position: fixed;
865
- bottom: var(--sp-6);
866
- right: var(--sp-6);
867
- padding: var(--sp-3) var(--sp-4);
868
- border-radius: var(--radius-md);
869
- font-size: var(--text-sm);
870
- font-weight: 500;
871
- box-shadow: var(--shadow-lg);
872
- z-index: 400;
873
- display: flex;
874
- align-items: center;
875
- gap: var(--sp-2);
876
- animation: toast-in var(--duration-normal) var(--ease-out);
877
- color: #fff;
878
- }
879
- .toast.success { background: var(--green); }
880
- .toast.error { background: var(--red); }
881
- @keyframes toast-in {
882
- from { opacity: 0; transform: translateY(8px); }
883
- to { opacity: 1; transform: translateY(0); }
884
- }
885
-
886
- /* --- Empty states --- */
887
- .empty-state {
888
- text-align: center;
889
- padding: var(--sp-12) var(--sp-6);
890
- color: var(--text-muted);
891
- }
892
- .empty-state svg { color: var(--text-muted); margin-bottom: var(--sp-3); opacity: 0.5; }
893
- .empty-state p { font-size: var(--text-sm); margin-top: var(--sp-2); }
894
-
895
- /* --- Home page grid --- */
896
- .stats-row {
897
- display: flex;
898
- gap: var(--sp-3);
899
- margin-bottom: var(--sp-6);
900
- flex-wrap: wrap;
901
- }
902
- .stat-card {
903
- flex: 1;
904
- min-width: 100px;
905
- background: var(--bg-alt);
906
- border: 1px solid var(--border);
907
- border-radius: var(--radius-md);
908
- padding: var(--sp-3) var(--sp-4);
909
- text-align: center;
910
- }
911
- .stat-number {
912
- font-size: var(--text-2xl);
913
- font-weight: 700;
914
- color: var(--accent);
915
- }
916
- .stat-label {
917
- font-size: var(--text-xs);
918
- color: var(--text-muted);
919
- text-transform: uppercase;
920
- letter-spacing: 0.05em;
921
- margin-top: var(--sp-1);
922
- }
923
-
924
- .home-section { margin-bottom: var(--sp-6); }
925
- .home-section h2 {
926
- font-size: var(--text-base);
927
- font-weight: 600;
928
- color: var(--text-secondary);
929
- text-transform: uppercase;
930
- letter-spacing: 0.03em;
931
- margin-bottom: var(--sp-3);
932
- padding-bottom: var(--sp-2);
933
- border-bottom: 1px solid var(--border-subtle);
934
- }
935
- .home-page-list { list-style: none; }
936
- .home-page-item {
937
- display: flex;
938
- align-items: center;
939
- gap: var(--sp-2);
940
- padding: var(--sp-2) var(--sp-3);
941
- border-radius: var(--radius-md);
942
- cursor: pointer;
943
- font-size: var(--text-sm);
944
- color: var(--text);
945
- transition: background var(--duration-fast) var(--ease-out);
946
- }
947
- .home-page-item:hover { background: var(--surface); }
948
- .home-page-item svg { color: var(--text-muted); flex-shrink: 0; }
949
-
950
- /* --- Breadcrumb --- */
951
- .breadcrumb {
952
- display: flex;
953
- align-items: center;
954
- gap: var(--sp-1);
955
- font-size: var(--text-xs);
956
- color: var(--text-muted);
957
- margin-bottom: var(--sp-3);
958
- }
959
- .breadcrumb a {
960
- color: var(--text-muted);
961
- font-weight: 500;
962
- }
963
- .breadcrumb a:hover { color: var(--accent); text-decoration: none; }
964
- .breadcrumb svg { flex-shrink: 0; }
965
-
966
- /* Page metadata bar */
967
- .page-meta {
968
- display: flex;
969
- align-items: center;
970
- gap: var(--sp-3);
971
- font-size: var(--text-xs);
972
- color: var(--text-muted);
973
- margin-top: var(--sp-2);
974
- padding-top: var(--sp-2);
975
- border-top: 1px solid var(--border-subtle);
976
- }
977
- .page-meta svg { flex-shrink: 0; }
978
- </style>
979
- </head>
980
- <body>
981
- <a href="#main-content" class="skip-link">Skip to main content</a>
982
- <div id="root"></div>
983
- <script>
984
- "use strict";
985
- const h = React.createElement;
986
- const { useState, useEffect, useCallback, useRef, useMemo } = React;
987
- const API = "/api";
988
-
989
- /* ============================================================
990
- SVG ICONS (Lucide-style, 24x24 stroke-based)
991
- ============================================================ */
992
- const Icon = ({ name, size = 18, className = "" }) => {
993
- const icons = {
994
- home: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M9 22V12h6v10",
995
- network: "M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h3a2 2 0 0 1 2 2v1.27A2 2 0 0 1 19 12a2 2 0 0 1-1 1.73V16a2 2 0 0 1-2 2h-3v1.27A2 2 0 0 1 12 22a2 2 0 0 1-1-3.73V18H8a2 2 0 0 1-2-2v-2.27A2 2 0 0 1 5 12a2 2 0 0 1 1-3.73V7h3V5.73A2 2 0 0 1 10 4a2 2 0 0 1 2-2z",
996
- fileText: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8",
997
- messageSquare: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",
998
- edit: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7 M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z",
999
- move: "M5 12h14 M12 5l7 7-7 7",
1000
- menu: "M3 12h18 M3 6h18 M3 18h18",
1001
- search: "M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16z M21 21l-4.35-4.35",
1002
- chevronRight: "M9 18l6-6-6-6",
1003
- chevronDown: "M6 9l6 6 6-6",
1004
- x: "M18 6L6 18 M6 6l12 12",
1005
- sun: "M12 1v2 M12 21v2 M4.22 4.22l1.42 1.42 M18.36 18.36l1.42 1.42 M1 12h2 M21 12h2 M4.22 19.78l1.42-1.42 M18.36 5.64l1.42-1.42 M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10z",
1006
- moon: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z",
1007
- save: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z M17 21v-8H7v8 M7 3v5h8",
1008
- check: "M20 6L9 17l-5-5",
1009
- folder: "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",
1010
- archive: "M21 8v13H3V8 M1 3h22v5H1z M10 12h4",
1011
- book: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20 M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15z",
1012
- layers: "M12 2L2 7l10 5 10-5-10-5z M2 17l10 5 10-5 M2 12l10 5 10-5",
1013
- clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z M12 6v6l4 2",
1014
- doc: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6",
1015
- inbox: "M22 12h-6l-2 3H10l-2-3H2",
1016
- };
1017
-
1018
- const d = icons[name];
1019
- if (!d) return null;
1020
-
1021
- const paths = d.split(" M").map((p, i) => i === 0 ? p : "M" + p);
1022
-
1023
- return h("svg", {
1024
- width: size, height: size, viewBox: "0 0 24 24",
1025
- fill: "none", stroke: "currentColor", strokeWidth: 2,
1026
- strokeLinecap: "round", strokeLinejoin: "round",
1027
- className, "aria-hidden": "true"
1028
- }, ...paths.map((p, i) => h("path", { key: i, d: p })));
1029
- };
1030
-
1031
- /* ============================================================
1032
- THEME MANAGEMENT
1033
- ============================================================ */
1034
- function getInitialTheme() {
1035
- const stored = localStorage.getItem("pi-para-theme");
1036
- if (stored === "light" || stored === "dark") return stored;
1037
- return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
1038
- }
1039
-
1040
- function useTheme() {
1041
- const [theme, setTheme] = useState(getInitialTheme);
1042
- useEffect(() => {
1043
- document.documentElement.setAttribute("data-theme", theme);
1044
- localStorage.setItem("pi-para-theme", theme);
1045
- }, [theme]);
1046
- const toggle = useCallback(() => setTheme(t => t === "dark" ? "light" : "dark"), []);
1047
- return [theme, toggle];
1048
- }
1049
-
1050
- /* ============================================================
1051
- TOAST HOOK
1052
- ============================================================ */
1053
- function useToast() {
1054
- const [toast, setToast] = useState(null);
1055
- const timerRef = useRef(null);
1056
-
1057
- const show = useCallback((message, type = "success") => {
1058
- if (timerRef.current) clearTimeout(timerRef.current);
1059
- setToast({ message, type });
1060
- timerRef.current = setTimeout(() => setToast(null), 3500);
1061
- }, []);
1062
-
1063
- return [toast, show];
1064
- }
1065
-
1066
- /* ============================================================
1067
- APP
1068
- ============================================================ */
1069
- function App() {
1070
- const [pages, setPages] = useState({});
1071
- const [currentPage, setCurrentPage] = useState(null);
1072
- const [view, setView] = useState("home");
1073
- const [sidebarOpen, setSidebarOpen] = useState(false);
1074
- const [searchQuery, setSearchQuery] = useState("");
1075
- const [searchResults, setSearchResults] = useState([]);
1076
- const [logEntries, setLogEntries] = useState([]);
1077
- const [sessions, setSessions] = useState([]);
1078
- const [graphData, setGraphData] = useState({ nodes: [], edges: [] });
1079
- const [maxNodes, setMaxNodes] = useState(100);
1080
- const [editing, setEditing] = useState(false);
1081
- const [editBody, setEditBody] = useState("");
1082
- const [moveTarget, setMoveTarget] = useState(null);
1083
- const [loading, setLoading] = useState({});
1084
- const [scopeFilter, setScopeFilter] = useState("all");
1085
- const [theme, toggleTheme] = useTheme();
1086
- const [toast, showToast] = useToast();
1087
- const searchTimerRef = useRef(null);
1088
-
1089
- const setLoadingKey = (key, val) => setLoading(prev => ({ ...prev, [key]: val }));
1090
-
1091
- const loadPages = useCallback(async () => {
1092
- try {
1093
- const r = await fetch(API + "/pages");
1094
- if (r.ok) setPages(await r.json());
1095
- } catch (e) { console.error("Failed to load pages", e); }
1096
- }, []);
1097
-
1098
- useEffect(() => { loadPages(); }, [loadPages]);
1099
-
1100
- // Reload graph when scope filter changes while graph view is active
1101
- useEffect(() => { if (view === "graph") loadGraph(maxNodes); }, [scopeFilter]);
1102
-
1103
- const openPage = async (cat, slug) => {
1104
- setLoadingKey("page", true);
1105
- try {
1106
- const r = await fetch(API + "/pages/" + cat + "/" + slug);
1107
- if (r.ok) {
1108
- setCurrentPage(await r.json());
1109
- setView("page");
1110
- setEditing(false);
1111
- setSidebarOpen(false);
1112
- }
1113
- } catch (e) { showToast("Failed to load page", "error"); }
1114
- setLoadingKey("page", false);
1115
- };
1116
-
1117
- const savePage = async () => {
1118
- if (!currentPage) return;
1119
- setLoadingKey("save", true);
1120
- try {
1121
- const r = await fetch(API + "/pages/" + currentPage.category + "/" + currentPage.slug, {
1122
- method: "PUT",
1123
- headers: { "Content-Type": "application/json" },
1124
- body: JSON.stringify({ body: editBody })
1125
- });
1126
- if (r.ok) {
1127
- const p = await r.json();
1128
- setCurrentPage(p);
1129
- setEditing(false);
1130
- loadPages();
1131
- showToast("Page saved successfully");
1132
- } else {
1133
- showToast("Failed to save page", "error");
1134
- }
1135
- } catch (e) { showToast("Failed to save page", "error"); }
1136
- setLoadingKey("save", false);
1137
- };
1138
-
1139
- const doSearch = async (q) => {
1140
- if (!q.trim()) return setSearchResults([]);
1141
- setLoadingKey("search", true);
1142
- try {
1143
- const r = await fetch(API + "/search?q=" + encodeURIComponent(q));
1144
- if (r.ok) setSearchResults(await r.json());
1145
- } catch (e) { console.error("Search failed", e); }
1146
- setLoadingKey("search", false);
1147
- };
1148
-
1149
- // Debounced search
1150
- const handleSearchInput = (e) => {
1151
- const q = e.target.value;
1152
- setSearchQuery(q);
1153
- if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
1154
- if (q.length > 1) {
1155
- setView("search");
1156
- searchTimerRef.current = setTimeout(() => doSearch(q), 250);
1157
- }
1158
- };
1159
-
1160
- const loadLog = async () => {
1161
- setLoadingKey("log", true);
1162
- const r = await fetch(API + "/log");
1163
- if (r.ok) setLogEntries(await r.json());
1164
- setLoadingKey("log", false);
1165
- };
1166
-
1167
- const loadSessions = async () => {
1168
- setLoadingKey("sessions", true);
1169
- const r = await fetch(API + "/sessions");
1170
- if (r.ok) setSessions(await r.json());
1171
- setLoadingKey("sessions", false);
1172
- };
1173
-
1174
- const loadGraph = async (nodes) => {
1175
- setLoadingKey("graph", true);
1176
- const params = [];
1177
- if (scopeFilter !== "all") params.push("scope=" + encodeURIComponent(scopeFilter));
1178
- const mn = nodes ?? maxNodes;
1179
- if (mn !== 100) params.push("maxNodes=" + mn);
1180
- const qs = params.length ? "?" + params.join("&") : "";
1181
- const r = await fetch(API + "/graph" + qs);
1182
- if (r.ok) setGraphData(await r.json());
1183
- setLoadingKey("graph", false);
1184
- };
1185
-
1186
- const doMovePage = async (newCat) => {
1187
- if (!moveTarget) return;
1188
- setLoadingKey("move", true);
1189
- try {
1190
- const r = await fetch(API + "/pages/" + moveTarget.category + "/" + moveTarget.slug + "/move", {
1191
- method: "POST",
1192
- headers: { "Content-Type": "application/json" },
1193
- body: JSON.stringify({ to: newCat })
1194
- });
1195
- if (r.ok) {
1196
- setMoveTarget(null);
1197
- loadPages();
1198
- if (currentPage?.slug === moveTarget.slug) openPage(newCat, moveTarget.slug);
1199
- showToast("Page moved to " + newCat);
1200
- } else {
1201
- showToast("Failed to move page", "error");
1202
- }
1203
- } catch (e) { showToast("Failed to move page", "error"); }
1204
- setLoadingKey("move", false);
1205
- };
1206
-
1207
- const renderMd = (md) => {
1208
- let html = marked.parse(md || "");
1209
- html = html.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (m, slug, label) =>
1210
- '<a href="#" class="wikilink" data-slug="' + slug + '">' + (label || slug) + '</a>');
1211
- return html;
1212
- };
1213
-
1214
- // Extract unique scopes from all pages
1215
- const allScopes = useMemo(() => {
1216
- const scopes = new Set();
1217
- for (const cat of ["projects", "areas", "resources", "archives"]) {
1218
- for (const p of (pages[cat] || [])) {
1219
- if (p.scope) p.scope.forEach(s => { if (s !== "global") scopes.add(s); });
1220
- }
1221
- }
1222
- return [...scopes].sort();
1223
- }, [pages]);
1224
-
1225
- // Filter pages by scope
1226
- const filteredPages = useMemo(() => {
1227
- if (scopeFilter === "all") return pages;
1228
- const result = {};
1229
- for (const cat of ["projects", "areas", "resources", "archives"]) {
1230
- result[cat] = (pages[cat] || []).filter(p =>
1231
- !p.scope || p.scope.includes(scopeFilter) || p.scope.includes("global")
1232
- );
1233
- }
1234
- return result;
1235
- }, [pages, scopeFilter]);
1236
-
1237
- const totalPages = useMemo(() =>
1238
- ["projects", "areas", "resources", "archives"].reduce((s, c) => (filteredPages[c] || []).length + s, 0),
1239
- [filteredPages]
1240
- );
1241
-
1242
- const categories = ["projects", "areas", "resources", "archives"];
1243
-
1244
- return h("div", { className: "app" },
1245
- // Overlay for mobile sidebar
1246
- h("div", {
1247
- className: "overlay" + (sidebarOpen ? " show" : ""),
1248
- onClick: () => setSidebarOpen(false),
1249
- "aria-hidden": "true"
1250
- }),
1251
-
1252
- // Sidebar
1253
- h("nav", {
1254
- className: "sidebar" + (sidebarOpen ? " open" : ""),
1255
- "aria-label": "Wiki navigation"
1256
- },
1257
- h("div", { className: "sidebar-header" },
1258
- h("div", { className: "logo" },
1259
- h(Icon, { name: "layers", size: 20 }),
1260
- h("div", null,
1261
- h("div", { className: "logo-text" }, "pi-para"),
1262
- h("div", { className: "logo-sub" }, "Knowledge Wiki")
1263
- )
1264
- )
1265
- ),
1266
- h("div", { className: "sidebar-nav" },
1267
- categories.map(cat =>
1268
- h(SidebarSection, {
1269
- key: cat, cat,
1270
- pages: filteredPages[cat] || [],
1271
- currentPage, openPage
1272
- })
1273
- )
1274
- )
1275
- ),
1276
-
1277
- // Main wrapper
1278
- h("div", { className: "main-wrapper" },
1279
- // Top bar
1280
- h("header", { className: "topbar", role: "banner" },
1281
- h("button", {
1282
- className: "nav-btn hamburger",
1283
- onClick: () => setSidebarOpen(!sidebarOpen),
1284
- "aria-label": "Toggle navigation menu",
1285
- "aria-expanded": sidebarOpen
1286
- }, h(Icon, { name: "menu" })),
1287
-
1288
- h("button", {
1289
- className: "nav-btn" + (view === "home" ? " active" : ""),
1290
- onClick: () => setView("home"),
1291
- "aria-label": "Home"
1292
- }, h(Icon, { name: "home" }), h("span", { className: "nav-label" }, "Home")),
1293
-
1294
- h("button", {
1295
- className: "nav-btn" + (view === "graph" ? " active" : ""),
1296
- onClick: () => { setView("graph"); loadGraph(); },
1297
-
1298
- "aria-label": "Graph view"
1299
- }, h(Icon, { name: "network" }), h("span", { className: "nav-label" }, "Graph")),
1300
-
1301
- h("button", {
1302
- className: "nav-btn" + (view === "log" ? " active" : ""),
1303
- onClick: () => { setView("log"); loadLog(); },
1304
- "aria-label": "Activity log"
1305
- }, h(Icon, { name: "fileText" }), h("span", { className: "nav-label" }, "Log")),
1306
-
1307
- h("button", {
1308
- className: "nav-btn" + (view === "sessions" ? " active" : ""),
1309
- onClick: () => { setView("sessions"); loadSessions(); },
1310
- "aria-label": "Session digests"
1311
- }, h(Icon, { name: "messageSquare" }), h("span", { className: "nav-label" }, "Sessions")),
1312
-
1313
- // Scope filter
1314
- allScopes.length > 0 && h("div", { className: "scope-filter" },
1315
- h("select", {
1316
- value: scopeFilter,
1317
- onChange: e => setScopeFilter(e.target.value),
1318
- "aria-label": "Filter by project scope"
1319
- },
1320
- h("option", { value: "all" }, "All scopes"),
1321
- allScopes.map(s => h("option", { key: s, value: s }, s))
1322
- )
1323
- ),
1324
-
1325
- h("div", { className: "search-wrapper" },
1326
- h(Icon, { name: "search", size: 16 }),
1327
- h("input", {
1328
- className: "search-input",
1329
- type: "search",
1330
- placeholder: "Search wiki\u2026",
1331
- value: searchQuery,
1332
- onChange: handleSearchInput,
1333
- onKeyDown: e => { if (e.key === "Enter") doSearch(searchQuery); },
1334
- "aria-label": "Search wiki pages"
1335
- })
1336
- ),
1337
-
1338
- h("button", {
1339
- className: "theme-toggle",
1340
- onClick: toggleTheme,
1341
- "aria-label": "Toggle " + (theme === "dark" ? "light" : "dark") + " mode",
1342
- title: "Toggle theme"
1343
- }, h(Icon, { name: theme === "dark" ? "sun" : "moon", size: 16 }))
1344
- ),
1345
-
1346
- // Main content — scroll wrapper + centered content
1347
- h("div", { className: "main-scroll" },
1348
- h("main", { id: "main-content", className: "main" + (view === "graph" ? " main-wide" : ""), role: "main" },
1349
- view === "home" && h(HomePage, { pages: filteredPages, openPage, totalPages, scopeFilter }),
1350
- view === "page" && currentPage && h(PageView, {
1351
- page: currentPage, editing, editBody,
1352
- setEditing, setEditBody, savePage, renderMd,
1353
- setMoveTarget, openPage, loading, setView
1354
- }),
1355
- view === "search" && h(SearchView, { results: searchResults, openPage, query: searchQuery, loading: loading.search }),
1356
- view === "graph" && h(GraphView, { data: graphData, openPage, loading: loading.graph, maxNodes, onMaxNodesChange: (n) => { setMaxNodes(n); loadGraph(n); } }),
1357
- view === "log" && h(LogView, { entries: logEntries, loading: loading.log }),
1358
- view === "sessions" && h(SessionsView, { sessions, loading: loading.sessions })
1359
- )
1360
- )
1361
- ),
1362
-
1363
- // Move modal
1364
- moveTarget && h(MoveModal, {
1365
- page: moveTarget,
1366
- onMove: doMovePage,
1367
- onClose: () => setMoveTarget(null),
1368
- loading: loading.move
1369
- }),
1370
-
1371
- // Toast
1372
- toast && h("div", {
1373
- className: "toast " + toast.type,
1374
- role: "status",
1375
- "aria-live": "polite"
1376
- },
1377
- h(Icon, { name: toast.type === "success" ? "check" : "x", size: 16 }),
1378
- toast.message
1379
- )
1380
- );
1381
- }
1382
-
1383
- /* ============================================================
1384
- SIDEBAR SECTION
1385
- ============================================================ */
1386
- function SidebarSection({ cat, pages, currentPage, openPage }) {
1387
- const [open, setOpen] = useState(cat === "projects" || cat === "areas");
1388
- const iconName = { projects: "layers", areas: "folder", resources: "book", archives: "archive" }[cat] || "folder";
1389
-
1390
- return h("div", { className: "sidebar-section" + (open ? " open" : "") },
1391
- h("button", {
1392
- className: "sidebar-section-header",
1393
- onClick: () => setOpen(!open),
1394
- "aria-expanded": open,
1395
- "aria-label": cat + " (" + pages.length + " pages)"
1396
- },
1397
- h(Icon, { name: "chevronRight", size: 14 }),
1398
- h(Icon, { name: iconName, size: 14 }),
1399
- h("span", null, cat),
1400
- h("span", { className: "count" }, pages.length)
1401
- ),
1402
- h("ul", { className: "sidebar-list", role: "list" },
1403
- pages.map(p =>
1404
- h("li", {
1405
- key: p.slug,
1406
- className: "sidebar-item" + (currentPage?.slug === p.slug ? " active" : ""),
1407
- onClick: () => openPage(cat, p.slug),
1408
- role: "listitem",
1409
- tabIndex: 0,
1410
- onKeyDown: e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openPage(cat, p.slug); } },
1411
- "aria-current": currentPage?.slug === p.slug ? "page" : undefined
1412
- }, p.title || p.slug)
1413
- )
1414
- )
1415
- );
1416
- }
1417
-
1418
- /* ============================================================
1419
- HOME PAGE
1420
- ============================================================ */
1421
- function HomePage({ pages, openPage, totalPages, scopeFilter }) {
1422
- const cats = ["projects", "areas", "resources", "archives"];
1423
-
1424
- return h("div", { className: "content" },
1425
- h("h1", null, "Wiki Overview",
1426
- scopeFilter && scopeFilter !== "all" && h("span", { className: "scope-badge" }, scopeFilter)
1427
- ),
1428
-
1429
- // Stats row
1430
- h("div", { className: "stats-row" },
1431
- h("div", { className: "stat-card" },
1432
- h("div", { className: "stat-number" }, totalPages),
1433
- h("div", { className: "stat-label" }, "Total Pages")
1434
- ),
1435
- cats.map(cat =>
1436
- h("div", { key: cat, className: "stat-card" },
1437
- h("div", { className: "stat-number" }, (pages[cat] || []).length),
1438
- h("div", { className: "stat-label" }, cat)
1439
- )
1440
- )
1441
- ),
1442
-
1443
- // Category lists
1444
- cats.map(cat => {
1445
- const items = pages[cat] || [];
1446
- if (!items.length) return null;
1447
- return h("div", { key: cat, className: "home-section" },
1448
- h("h2", null, cat),
1449
- h("ul", { className: "home-page-list", role: "list" },
1450
- items.map(p =>
1451
- h("li", {
1452
- key: p.slug,
1453
- className: "home-page-item",
1454
- onClick: () => openPage(cat, p.slug),
1455
- tabIndex: 0,
1456
- role: "listitem",
1457
- onKeyDown: e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openPage(cat, p.slug); } }
1458
- },
1459
- h(Icon, { name: "doc", size: 14 }),
1460
- h("span", null, p.title || p.slug)
1461
- )
1462
- )
1463
- )
1464
- );
1465
- }),
1466
-
1467
- totalPages === 0 && h("div", { className: "empty-state" },
1468
- h(Icon, { name: "inbox", size: 48 }),
1469
- h("h3", null, "No pages yet"),
1470
- h("p", null, "Start capturing knowledge and pages will appear here.")
1471
- )
1472
- );
1473
- }
1474
-
1475
- /* ============================================================
1476
- PAGE VIEW
1477
- ============================================================ */
1478
- function PageView({ page, editing, editBody, setEditing, setEditBody, savePage, renderMd, setMoveTarget, openPage, loading, setView }) {
1479
- const contentRef = useRef(null);
1480
-
1481
- useEffect(() => {
1482
- if (!contentRef.current) return;
1483
- const handleClick = (e) => {
1484
- const el = e.target.closest(".wikilink");
1485
- if (!el) return;
1486
- e.preventDefault();
1487
- const slug = el.getAttribute("data-slug");
1488
- if (!slug) return;
1489
- const cats = ["projects", "areas", "resources", "archives"];
1490
- for (const c of cats) {
1491
- fetch(API + "/pages/" + c + "/" + slug)
1492
- .then(r => { if (r.ok) return r.json(); })
1493
- .then(p => { if (p) openPage(c, slug); });
1494
- }
1495
- };
1496
- contentRef.current.addEventListener("click", handleClick);
1497
- return () => { if (contentRef.current) contentRef.current.removeEventListener("click", handleClick); };
1498
- }, [page, openPage]);
1499
-
1500
- if (editing) return h("div", { className: "content" },
1501
- h("div", { className: "breadcrumb" },
1502
- h("a", { href: "#", onClick: e => { e.preventDefault(); setView("home"); } }, "Home"),
1503
- h(Icon, { name: "chevronRight", size: 12 }),
1504
- h("span", null, page.frontmatter.para),
1505
- h(Icon, { name: "chevronRight", size: 12 }),
1506
- h("span", null, "Editing")
1507
- ),
1508
- h("h1", null, "Editing: " + page.frontmatter.title),
1509
- h("div", { className: "editor" },
1510
- h("label", { htmlFor: "page-editor", style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-secondary)", marginBottom: "var(--sp-2)" } },
1511
- "Page content (Markdown)"
1512
- ),
1513
- h("textarea", {
1514
- id: "page-editor",
1515
- value: editBody,
1516
- onChange: e => setEditBody(e.target.value),
1517
- "aria-label": "Page content editor"
1518
- }),
1519
- h("div", { className: "editor-actions" },
1520
- h("button", {
1521
- className: "btn btn-primary",
1522
- onClick: savePage,
1523
- disabled: loading.save,
1524
- "aria-label": "Save page"
1525
- },
1526
- loading.save ? h("span", { className: "spinner" }) : h(Icon, { name: "save", size: 16 }),
1527
- loading.save ? "Saving\u2026" : "Save"
1528
- ),
1529
- h("button", {
1530
- className: "btn btn-secondary",
1531
- onClick: () => setEditing(false)
1532
- }, "Cancel")
1533
- )
1534
- )
1535
- );
1536
-
1537
- return h("div", { className: "content" },
1538
- // Breadcrumb
1539
- h("div", { className: "breadcrumb" },
1540
- h("a", { href: "#", onClick: e => { e.preventDefault(); setView("home"); } }, "Home"),
1541
- h(Icon, { name: "chevronRight", size: 12 }),
1542
- h("span", null, page.frontmatter.para)
1543
- ),
1544
-
1545
- // Header with actions
1546
- h("div", { className: "page-header" },
1547
- h("h1", null, page.frontmatter.title),
1548
- h("div", { className: "page-actions" },
1549
- h("button", {
1550
- className: "btn btn-secondary",
1551
- onClick: () => { setEditBody(page.body); setEditing(true); },
1552
- "aria-label": "Edit page"
1553
- }, h(Icon, { name: "edit", size: 16 }), "Edit"),
1554
- h("button", {
1555
- className: "btn btn-secondary",
1556
- onClick: () => setMoveTarget(page),
1557
- "aria-label": "Move page to different category"
1558
- }, h(Icon, { name: "move", size: 16 }), "Move")
1559
- )
1560
- ),
1561
-
1562
- // Tags
1563
- h("div", { className: "tags", role: "list", "aria-label": "Page tags" },
1564
- h("span", { className: "tag para", role: "listitem" }, page.frontmatter.para),
1565
- ...(page.frontmatter.scope || []).map(s =>
1566
- h("span", { key: s, className: "tag scope", role: "listitem" }, s)
1567
- ),
1568
- ...(page.frontmatter.tags || []).map(t =>
1569
- h("span", { key: t, className: "tag", role: "listitem" }, t)
1570
- )
1571
- ),
1572
-
1573
- // Rendered body
1574
- h("div", {
1575
- ref: contentRef,
1576
- className: "content",
1577
- dangerouslySetInnerHTML: { __html: renderMd(page.body) }
1578
- }),
1579
-
1580
- // Metadata footer
1581
- page.frontmatter.updated && h("div", { className: "page-meta" },
1582
- h(Icon, { name: "clock", size: 14 }),
1583
- "Updated: " + new Date(page.frontmatter.updated).toLocaleDateString("en-US", {
1584
- year: "numeric", month: "short", day: "numeric"
1585
- })
1586
- )
1587
- );
1588
- }
1589
-
1590
- /* ============================================================
1591
- SEARCH VIEW
1592
- ============================================================ */
1593
- function SearchView({ results, openPage, query, loading }) {
1594
- return h("div", { className: "content" },
1595
- h("h1", null, "Search Results"),
1596
- h("p", { style: { color: "var(--text-secondary)", marginBottom: "var(--sp-4)" } },
1597
- loading ? "Searching\u2026" :
1598
- results.length + " result" + (results.length !== 1 ? "s" : "") + (query ? ' for "' + query + '"' : "")
1599
- ),
1600
-
1601
- loading && h("div", { style: { textAlign: "center", padding: "var(--sp-8)" } },
1602
- h("span", { className: "spinner", style: { width: 24, height: 24 } })
1603
- ),
1604
-
1605
- !loading && results.length === 0 && query && h("div", { className: "empty-state" },
1606
- h(Icon, { name: "search", size: 48 }),
1607
- h("h3", null, "No results found"),
1608
- h("p", null, "Try different keywords or check your spelling.")
1609
- ),
1610
-
1611
- !loading && h("ul", { className: "search-results", role: "list" },
1612
- results.map((r, i) =>
1613
- h("li", {
1614
- key: i,
1615
- className: "search-result",
1616
- onClick: () => openPage(r.category, r.slug),
1617
- tabIndex: 0,
1618
- role: "listitem",
1619
- onKeyDown: e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openPage(r.category, r.slug); } }
1620
- },
1621
- h("div", { className: "search-result-title" }, r.title || r.slug),
1622
- h("div", { className: "search-result-meta" },
1623
- h("span", null, r.category + "/" + r.slug),
1624
- h("div", { className: "score-bar" },
1625
- h("div", { className: "score-bar-fill", style: { width: ((r.score || 0) * 100) + "%" } })
1626
- ),
1627
- h("span", null, ((r.score || 0) * 100).toFixed(0) + "%")
1628
- ),
1629
- r.snippet && h("div", { className: "search-result-snippet" }, r.snippet.slice(0, 250))
1630
- )
1631
- )
1632
- )
1633
- );
1634
- }
1635
-
1636
- /* ============================================================
1637
- GRAPH VIEW
1638
- ============================================================ */
1639
- function GraphView({ data, openPage, loading, onMaxNodesChange, maxNodes }) {
1640
- const ref = useRef(null);
1641
-
1642
- useEffect(() => {
1643
- if (!ref.current || !data.nodes.length) return;
1644
-
1645
- const container = ref.current;
1646
- const w = container.clientWidth;
1647
- const ht = container.clientHeight;
1648
-
1649
- d3.select(container).select("svg").remove();
1650
-
1651
- const svg = d3.select(container).append("svg")
1652
- .attr("viewBox", [0, 0, w, ht])
1653
- .attr("role", "img")
1654
- .attr("aria-label", "Wiki page relationship graph with " + data.nodes.length + " nodes and " + data.edges.length + " connections");
1655
-
1656
- const g = svg.append("g");
1657
-
1658
- svg.call(d3.zoom()
1659
- .scaleExtent([0.3, 4])
1660
- .on("zoom", e => g.attr("transform", e.transform))
1661
- );
1662
-
1663
- const colors = {
1664
- projects: "#60a5fa",
1665
- areas: "#34d399",
1666
- resources: "#fbbf24",
1667
- archives: "#94a3b8"
1668
- };
1669
-
1670
- const sim = d3.forceSimulation(data.nodes)
1671
- .force("link", d3.forceLink(data.edges).id(d => d.id).distance(100))
1672
- .force("charge", d3.forceManyBody().strength(-300))
1673
- .force("center", d3.forceCenter(w / 2, ht / 2))
1674
- .force("collision", d3.forceCollide().radius(30));
1675
-
1676
- const link = g.selectAll("line").data(data.edges).join("line")
1677
- .attr("stroke", "var(--border)")
1678
- .attr("stroke-width", 1.5)
1679
- .attr("stroke-opacity", 0.5);
1680
-
1681
- const nodeGroup = g.selectAll("g.node").data(data.nodes).join("g")
1682
- .attr("class", "node")
1683
- .attr("cursor", "pointer")
1684
- .call(d3.drag()
1685
- .on("start", (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
1686
- .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y; })
1687
- .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
1688
- )
1689
- .on("click", (e, d) => openPage(d.category, d.slug));
1690
-
1691
- nodeGroup.append("circle")
1692
- .attr("r", 8)
1693
- .attr("fill", d => colors[d.category] || "#60a5fa")
1694
- .attr("stroke", "var(--bg)")
1695
- .attr("stroke-width", 2);
1696
-
1697
- nodeGroup.append("text")
1698
- .text(d => d.title || d.id)
1699
- .attr("font-size", 11)
1700
- .attr("font-family", "var(--font-sans)")
1701
- .attr("fill", "var(--text)")
1702
- .attr("dx", 14)
1703
- .attr("dy", 4)
1704
- .attr("pointer-events", "none");
1705
-
1706
- sim.on("tick", () => {
1707
- link
1708
- .attr("x1", d => d.source.x).attr("y1", d => d.source.y)
1709
- .attr("x2", d => d.target.x).attr("y2", d => d.target.y);
1710
- nodeGroup.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
1711
- });
1712
-
1713
- return () => sim.stop();
1714
- }, [data]);
1715
-
1716
- return h("div", { className: "content" },
1717
- h("h1", null, "Knowledge Graph"),
1718
- h("div", { style: { display: "flex", alignItems: "center", gap: "var(--sp-4)", marginBottom: "var(--sp-3)", flexWrap: "wrap" } },
1719
- h("p", { style: { color: "var(--text-secondary)", margin: 0 } },
1720
- data.nodes.length + " pages \u00b7 " + data.edges.length + " connections"
1721
- ),
1722
- h("label", { style: { display: "flex", alignItems: "center", gap: "var(--sp-2)", fontSize: "0.85rem", color: "var(--text-secondary)" } },
1723
- "Max nodes",
1724
- h("input", {
1725
- type: "range", min: 10, max: 200, step: 10, value: maxNodes,
1726
- onChange: (e) => onMaxNodesChange(parseInt(e.target.value)),
1727
- style: { width: 120 },
1728
- "aria-label": "Maximum nodes to display"
1729
- }),
1730
- h("span", { style: { minWidth: 30, textAlign: "right" } }, maxNodes)
1731
- )
1732
- ),
1733
-
1734
- loading && h("div", { style: { textAlign: "center", padding: "var(--sp-8)" } },
1735
- h("span", { className: "spinner", style: { width: 24, height: 24 } })
1736
- ),
1737
-
1738
- !loading && data.nodes.length === 0
1739
- ? h("div", { className: "empty-state" },
1740
- h(Icon, { name: "network", size: 48 }),
1741
- h("h3", null, "No connections yet"),
1742
- h("p", null, "Add [[wikilinks]] between pages to see the graph.")
1743
- )
1744
- : h("div", { style: { position: "relative" } },
1745
- h("div", { ref, className: "graph-container" }),
1746
- h("div", { className: "graph-legend", "aria-label": "Graph legend" },
1747
- ["projects", "areas", "resources", "archives"].map(cat =>
1748
- h("div", { key: cat, className: "graph-legend-item" },
1749
- h("div", {
1750
- className: "graph-legend-dot",
1751
- style: { background: { projects: "#60a5fa", areas: "#34d399", resources: "#fbbf24", archives: "#94a3b8" }[cat] }
1752
- }),
1753
- h("span", null, cat)
1754
- )
1755
- )
1756
- )
1757
- )
1758
- );
1759
- }
1760
-
1761
- /* ============================================================
1762
- LOG VIEW
1763
- ============================================================ */
1764
- function LogView({ entries, loading }) {
1765
- return h("div", { className: "content" },
1766
- h("h1", null, "Activity Log"),
1767
- h("p", { style: { color: "var(--text-secondary)", marginBottom: "var(--sp-4)" } },
1768
- entries.length + " entr" + (entries.length !== 1 ? "ies" : "y")
1769
- ),
1770
-
1771
- loading && h("div", { style: { textAlign: "center", padding: "var(--sp-8)" } },
1772
- h("span", { className: "spinner", style: { width: 24, height: 24 } })
1773
- ),
1774
-
1775
- !loading && entries.length === 0 && h("div", { className: "empty-state" },
1776
- h(Icon, { name: "fileText", size: 48 }),
1777
- h("h3", null, "No activity yet"),
1778
- h("p", null, "Activity will appear here as pages are created and updated.")
1779
- ),
1780
-
1781
- !loading && h("div", { role: "list", "aria-label": "Activity log entries" },
1782
- entries.map((e, i) =>
1783
- h("div", { key: i, className: "log-entry", role: "listitem" },
1784
- h("span", { className: "log-date" }, e.date || ""),
1785
- h("span", { className: "log-op" }, e.operation || ""),
1786
- h("span", { className: "log-summary" }, e.summary || ""),
1787
- e.pages?.length
1788
- ? h("span", { className: "log-pages" }, e.pages.join(", "))
1789
- : null
1790
- )
1791
- )
1792
- )
1793
- );
1794
- }
1795
-
1796
- /* ============================================================
1797
- SESSIONS VIEW
1798
- ============================================================ */
1799
- function SessionsView({ sessions, loading }) {
1800
- return h("div", { className: "content" },
1801
- h("h1", null, "Session Digests"),
1802
- h("p", { style: { color: "var(--text-secondary)", marginBottom: "var(--sp-4)" } },
1803
- sessions.length + " session" + (sessions.length !== 1 ? "s" : "")
1804
- ),
1805
-
1806
- loading && h("div", { style: { textAlign: "center", padding: "var(--sp-8)" } },
1807
- h("span", { className: "spinner", style: { width: 24, height: 24 } })
1808
- ),
1809
-
1810
- !loading && sessions.length === 0 && h("div", { className: "empty-state" },
1811
- h(Icon, { name: "messageSquare", size: 48 }),
1812
- h("h3", null, "No session digests"),
1813
- h("p", null, "Session digests will appear as the daemon captures knowledge.")
1814
- ),
1815
-
1816
- !loading && sessions.map((s, i) =>
1817
- h("div", { key: i, className: "session-entry" },
1818
- h("div", { className: "session-header" },
1819
- h("span", { className: "session-project" }, s.project || "unknown"),
1820
- h("span", { className: "session-date" }, s.date || "")
1821
- ),
1822
- h("p", { className: "session-summary" }, s.summary || ""),
1823
- s.capturedPages?.length
1824
- ? h("div", { className: "tags", style: { marginTop: "var(--sp-2)" } },
1825
- s.capturedPages.map(p =>
1826
- h("span", { key: p, className: "tag" }, "[[" + p + "]]")
1827
- )
1828
- )
1829
- : null
1830
- )
1831
- )
1832
- );
1833
- }
1834
-
1835
- /* ============================================================
1836
- MOVE MODAL
1837
- ============================================================ */
1838
- function MoveModal({ page, onMove, onClose, loading }) {
1839
- const cats = ["projects", "areas", "resources", "archives"].filter(c => c !== page.category);
1840
- const [cat, setCat] = useState(cats[0]);
1841
-
1842
- return h("div", {
1843
- className: "modal-backdrop",
1844
- onClick: e => { if (e.target === e.currentTarget) onClose(); },
1845
- role: "dialog",
1846
- "aria-modal": "true",
1847
- "aria-labelledby": "move-modal-title"
1848
- },
1849
- h("div", { className: "modal" },
1850
- h("h3", { id: "move-modal-title" }, "Move Page"),
1851
- h("p", { style: { fontSize: "var(--text-sm)", color: "var(--text-secondary)", marginBottom: "var(--sp-4)" } },
1852
- "Move \"" + page.frontmatter.title + "\" from " + page.category + " to:"
1853
- ),
1854
- h("label", { htmlFor: "move-category", style: { display: "block", fontSize: "var(--text-sm)", fontWeight: 500, marginBottom: "var(--sp-1)" } },
1855
- "Target category"
1856
- ),
1857
- h("select", {
1858
- id: "move-category",
1859
- value: cat,
1860
- onChange: e => setCat(e.target.value)
1861
- },
1862
- cats.map(c => h("option", { key: c, value: c }, c))
1863
- ),
1864
- h("div", { className: "modal-actions" },
1865
- h("button", { className: "btn btn-secondary", onClick: onClose }, "Cancel"),
1866
- h("button", {
1867
- className: "btn btn-primary",
1868
- onClick: () => onMove(cat),
1869
- disabled: loading
1870
- },
1871
- loading ? h("span", { className: "spinner" }) : null,
1872
- loading ? "Moving\u2026" : "Move"
1873
- )
1874
- )
1875
- )
1876
- );
1877
- }
1878
-
1879
- /* ============================================================
1880
- MOUNT
1881
- ============================================================ */
1882
- ReactDOM.createRoot(document.getElementById("root")).render(h(App));
1883
- </script>
1884
- </body>
1885
- </html>