kyp-mem 0.2.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.
@@ -0,0 +1,1293 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>KYP-MEM — Know Your Project</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
11
+ <style>
12
+ :root {
13
+ --bg-void: #06060c;
14
+ --bg-primary: #0a0a14;
15
+ --bg-secondary: #0d0d1a;
16
+ --bg-tertiary: #050510;
17
+ --bg-hover: #141428;
18
+ --bg-active: #1a1a35;
19
+ --bg-card: #0f0f1e;
20
+
21
+ --neon-cyan: #00fff5;
22
+ --neon-green: #39ff14;
23
+ --neon-magenta: #ff00e0;
24
+ --neon-purple: #bf5af2;
25
+ --neon-orange: #ff6b35;
26
+ --neon-yellow: #ffd700;
27
+ --neon-blue: #4d9fff;
28
+ --neon-red: #ff3366;
29
+
30
+ --glow-cyan: 0 0 10px #00fff540, 0 0 20px #00fff520;
31
+ --glow-green: 0 0 10px #39ff1440, 0 0 20px #39ff1420;
32
+ --glow-magenta: 0 0 10px #ff00e040, 0 0 20px #ff00e020;
33
+
34
+ --text-primary: #e0e0f0;
35
+ --text-secondary: #8888a8;
36
+ --text-muted: #4a4a68;
37
+ --border: #1a1a30;
38
+ --border-glow: #1a1a4080;
39
+
40
+ --font: 'Inter', -apple-system, sans-serif;
41
+ --font-mono: 'JetBrains Mono', monospace;
42
+ }
43
+
44
+ * { margin: 0; padding: 0; box-sizing: border-box; }
45
+
46
+ body {
47
+ font-family: var(--font);
48
+ background: var(--bg-void);
49
+ color: var(--text-primary);
50
+ height: 100vh;
51
+ overflow: hidden;
52
+ }
53
+
54
+ /* Subtle grid background */
55
+ body::before {
56
+ content: '';
57
+ position: fixed;
58
+ inset: 0;
59
+ background-image:
60
+ linear-gradient(rgba(0,255,245,0.03) 1px, transparent 1px),
61
+ linear-gradient(90deg, rgba(0,255,245,0.03) 1px, transparent 1px);
62
+ background-size: 40px 40px;
63
+ pointer-events: none;
64
+ z-index: 0;
65
+ }
66
+
67
+ .layout {
68
+ display: grid;
69
+ grid-template-columns: var(--sidebar-w, 260px) 5px 1fr 5px var(--right-w, 280px);
70
+ grid-template-rows: 52px 1fr;
71
+ height: 100vh;
72
+ position: relative;
73
+ z-index: 1;
74
+ }
75
+
76
+ .layout.no-right-panel {
77
+ grid-template-columns: var(--sidebar-w, 260px) 5px 1fr;
78
+ }
79
+ .layout.no-right-panel .right-panel,
80
+ .layout.no-right-panel #resize-right { display: none; }
81
+
82
+ /* ============ RESIZE HANDLES ============ */
83
+ .resize-handle {
84
+ background: transparent;
85
+ cursor: col-resize;
86
+ position: relative;
87
+ z-index: 10;
88
+ transition: background 0.2s;
89
+ }
90
+
91
+ .resize-handle::before {
92
+ content: '';
93
+ position: absolute;
94
+ inset: 0 -3px;
95
+ z-index: 1;
96
+ }
97
+
98
+ .resize-handle::after {
99
+ content: '';
100
+ position: absolute;
101
+ top: 50%;
102
+ left: 50%;
103
+ transform: translate(-50%, -50%);
104
+ width: 3px;
105
+ height: 32px;
106
+ border-radius: 2px;
107
+ background: var(--text-muted);
108
+ opacity: 0;
109
+ transition: opacity 0.2s, background 0.2s, box-shadow 0.2s;
110
+ }
111
+
112
+ .resize-handle:hover::after,
113
+ .resize-handle.dragging::after {
114
+ opacity: 1;
115
+ background: var(--neon-cyan);
116
+ box-shadow: 0 0 8px rgba(0, 255, 245, 0.4);
117
+ }
118
+
119
+ .resize-handle:hover,
120
+ .resize-handle.dragging {
121
+ background: rgba(0, 255, 245, 0.08);
122
+ }
123
+
124
+ body.resizing {
125
+ cursor: col-resize !important;
126
+ user-select: none !important;
127
+ }
128
+
129
+ body.resizing * {
130
+ cursor: col-resize !important;
131
+ pointer-events: none !important;
132
+ }
133
+
134
+ body.resizing .resize-handle {
135
+ pointer-events: auto !important;
136
+ }
137
+
138
+ /* ============ HEADER ============ */
139
+ .header {
140
+ grid-column: 1 / -1;
141
+ background: var(--bg-tertiary);
142
+ border-bottom: 1px solid var(--border);
143
+ display: flex;
144
+ align-items: center;
145
+ padding: 0 20px;
146
+ gap: 16px;
147
+ position: relative;
148
+ }
149
+
150
+ .header::after {
151
+ content: '';
152
+ position: absolute;
153
+ bottom: -1px;
154
+ left: 0;
155
+ right: 0;
156
+ height: 1px;
157
+ background: linear-gradient(90deg, transparent, var(--neon-cyan), var(--neon-magenta), var(--neon-purple), transparent);
158
+ opacity: 0.4;
159
+ }
160
+
161
+ .logo {
162
+ font-family: var(--font-mono);
163
+ font-weight: 700;
164
+ font-size: 15px;
165
+ color: var(--neon-cyan);
166
+ text-shadow: var(--glow-cyan);
167
+ letter-spacing: 1px;
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ .logo .logo-sub {
172
+ font-size: 10px;
173
+ font-weight: 400;
174
+ color: var(--text-muted);
175
+ text-shadow: none;
176
+ letter-spacing: 0.5px;
177
+ margin-left: 8px;
178
+ }
179
+
180
+ .breadcrumb {
181
+ font-family: var(--font-mono);
182
+ font-size: 12px;
183
+ color: var(--text-secondary);
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .breadcrumb .sep { color: var(--text-muted); margin: 0 6px; }
190
+ .breadcrumb .current { color: var(--neon-cyan); font-weight: 600; }
191
+
192
+ .header-actions {
193
+ margin-left: auto;
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 10px;
197
+ }
198
+
199
+ .toggle-btn {
200
+ background: var(--bg-hover);
201
+ border: 1px solid var(--border);
202
+ color: var(--text-secondary);
203
+ font-family: var(--font-mono);
204
+ font-size: 11px;
205
+ padding: 5px 10px;
206
+ border-radius: 6px;
207
+ cursor: pointer;
208
+ transition: all 0.2s;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 5px;
212
+ }
213
+
214
+ .toggle-btn:hover {
215
+ border-color: var(--neon-cyan);
216
+ color: var(--neon-cyan);
217
+ }
218
+
219
+ .toggle-btn.active {
220
+ border-color: var(--neon-cyan);
221
+ color: var(--neon-cyan);
222
+ background: rgba(0, 255, 245, 0.08);
223
+ box-shadow: 0 0 8px rgba(0, 255, 245, 0.15);
224
+ }
225
+
226
+ .toggle-btn .dot {
227
+ width: 6px;
228
+ height: 6px;
229
+ border-radius: 50%;
230
+ background: var(--text-muted);
231
+ transition: all 0.2s;
232
+ }
233
+
234
+ .toggle-btn.active .dot {
235
+ background: var(--neon-green);
236
+ box-shadow: 0 0 6px var(--neon-green);
237
+ }
238
+
239
+ .search-box {
240
+ position: relative;
241
+ }
242
+
243
+ .search-box input {
244
+ background: var(--bg-secondary);
245
+ border: 1px solid var(--border);
246
+ color: var(--text-primary);
247
+ font-family: var(--font-mono);
248
+ padding: 6px 12px 6px 28px;
249
+ border-radius: 6px;
250
+ font-size: 12px;
251
+ width: 220px;
252
+ outline: none;
253
+ transition: all 0.2s;
254
+ }
255
+
256
+ .search-box input::placeholder { color: var(--text-muted); }
257
+
258
+ .search-box input:focus {
259
+ border-color: var(--neon-cyan);
260
+ box-shadow: 0 0 12px rgba(0, 255, 245, 0.15);
261
+ width: 280px;
262
+ }
263
+
264
+ .search-box .search-icon {
265
+ position: absolute;
266
+ left: 9px;
267
+ top: 50%;
268
+ transform: translateY(-50%);
269
+ color: var(--text-muted);
270
+ font-size: 12px;
271
+ }
272
+
273
+ .search-box .search-hint {
274
+ position: absolute;
275
+ right: 8px;
276
+ top: 50%;
277
+ transform: translateY(-50%);
278
+ font-family: var(--font-mono);
279
+ font-size: 10px;
280
+ color: var(--text-muted);
281
+ background: var(--bg-hover);
282
+ padding: 1px 5px;
283
+ border-radius: 3px;
284
+ border: 1px solid var(--border);
285
+ }
286
+
287
+ .search-results {
288
+ position: absolute;
289
+ top: calc(100% + 6px);
290
+ right: 0;
291
+ width: 420px;
292
+ max-height: 420px;
293
+ overflow-y: auto;
294
+ background: var(--bg-card);
295
+ border: 1px solid var(--border);
296
+ border-radius: 10px;
297
+ z-index: 100;
298
+ display: none;
299
+ backdrop-filter: blur(12px);
300
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 1px var(--neon-cyan);
301
+ }
302
+
303
+ .search-results.active { display: block; }
304
+
305
+ .search-result {
306
+ padding: 12px 16px;
307
+ cursor: pointer;
308
+ border-bottom: 1px solid var(--border);
309
+ transition: background 0.15s;
310
+ }
311
+
312
+ .search-result:last-child { border-bottom: none; }
313
+ .search-result:hover { background: var(--bg-hover); }
314
+
315
+ .search-result .sr-title {
316
+ font-size: 13px;
317
+ font-weight: 600;
318
+ color: var(--neon-cyan);
319
+ }
320
+
321
+ .search-result .sr-path {
322
+ font-family: var(--font-mono);
323
+ font-size: 10px;
324
+ color: var(--text-muted);
325
+ margin-top: 3px;
326
+ }
327
+
328
+ .search-result .sr-snippet {
329
+ font-size: 12px;
330
+ color: var(--text-secondary);
331
+ margin-top: 4px;
332
+ line-height: 1.4;
333
+ }
334
+
335
+ /* ============ SIDEBAR ============ */
336
+ .sidebar {
337
+ background: var(--bg-secondary);
338
+ border-right: 1px solid var(--border);
339
+ display: flex;
340
+ flex-direction: column;
341
+ overflow: hidden;
342
+ }
343
+
344
+ .sidebar-scroll {
345
+ flex: 1;
346
+ overflow-y: auto;
347
+ padding: 8px 0;
348
+ }
349
+
350
+ .sidebar-section { padding: 0 10px; }
351
+
352
+ .sidebar-section-title {
353
+ font-family: var(--font-mono);
354
+ font-size: 10px;
355
+ font-weight: 600;
356
+ text-transform: uppercase;
357
+ letter-spacing: 1.5px;
358
+ color: var(--text-muted);
359
+ padding: 10px 8px 8px;
360
+ }
361
+
362
+ .tree-item {
363
+ display: flex;
364
+ align-items: center;
365
+ padding: 5px 8px;
366
+ border-radius: 5px;
367
+ cursor: pointer;
368
+ font-size: 13px;
369
+ gap: 6px;
370
+ user-select: none;
371
+ transition: all 0.12s;
372
+ position: relative;
373
+ }
374
+
375
+ .tree-item:hover { background: var(--bg-hover); }
376
+
377
+ .tree-item.active {
378
+ background: rgba(0, 255, 245, 0.06);
379
+ }
380
+
381
+ .tree-item.active::before {
382
+ content: '';
383
+ position: absolute;
384
+ left: 0;
385
+ top: 4px;
386
+ bottom: 4px;
387
+ width: 2px;
388
+ background: var(--neon-cyan);
389
+ border-radius: 1px;
390
+ box-shadow: 0 0 6px var(--neon-cyan);
391
+ }
392
+
393
+ .tree-item .arrow {
394
+ width: 14px;
395
+ font-size: 9px;
396
+ color: var(--text-muted);
397
+ text-align: center;
398
+ flex-shrink: 0;
399
+ transition: transform 0.15s;
400
+ }
401
+
402
+ .tree-item .arrow.open { transform: rotate(90deg); }
403
+
404
+ .tree-item .icon {
405
+ flex-shrink: 0;
406
+ font-size: 13px;
407
+ }
408
+
409
+ .tree-item .folder-icon { color: var(--neon-yellow); }
410
+ .tree-item .note-icon { color: var(--neon-purple); }
411
+
412
+ .tree-item .name {
413
+ overflow: hidden;
414
+ text-overflow: ellipsis;
415
+ white-space: nowrap;
416
+ font-size: 13px;
417
+ color: var(--text-primary);
418
+ }
419
+
420
+ .tree-item.active .name { color: var(--neon-cyan); }
421
+
422
+ .tree-children {
423
+ padding-left: 14px;
424
+ overflow: hidden;
425
+ }
426
+
427
+ .tree-children.collapsed { display: none; }
428
+
429
+ .stats-bar {
430
+ padding: 10px 14px;
431
+ border-top: 1px solid var(--border);
432
+ font-family: var(--font-mono);
433
+ font-size: 10px;
434
+ color: var(--text-muted);
435
+ display: flex;
436
+ gap: 14px;
437
+ flex-wrap: wrap;
438
+ }
439
+
440
+ .stat-val { color: var(--neon-green); font-weight: 600; }
441
+
442
+ /* ============ CONTENT ============ */
443
+ .content {
444
+ overflow-y: auto;
445
+ padding: 36px 52px;
446
+ background: var(--bg-primary);
447
+ }
448
+
449
+ .note-properties {
450
+ display: flex;
451
+ flex-wrap: wrap;
452
+ gap: 6px;
453
+ margin-bottom: 24px;
454
+ padding-bottom: 18px;
455
+ border-bottom: 1px solid var(--border);
456
+ }
457
+
458
+ .tag {
459
+ font-family: var(--font-mono);
460
+ font-size: 11px;
461
+ font-weight: 500;
462
+ padding: 3px 10px;
463
+ border-radius: 4px;
464
+ background: rgba(255, 0, 224, 0.1);
465
+ color: var(--neon-magenta);
466
+ border: 1px solid rgba(255, 0, 224, 0.2);
467
+ }
468
+
469
+ .prop-item {
470
+ font-family: var(--font-mono);
471
+ font-size: 11px;
472
+ padding: 3px 10px;
473
+ border-radius: 4px;
474
+ background: var(--bg-card);
475
+ color: var(--text-secondary);
476
+ border: 1px solid var(--border);
477
+ }
478
+
479
+ .prop-item .prop-key { color: var(--text-muted); }
480
+
481
+ /* ============ MARKDOWN ============ */
482
+ .md-body h1 {
483
+ font-size: 28px;
484
+ font-weight: 700;
485
+ margin: 0 0 20px;
486
+ color: var(--neon-cyan);
487
+ text-shadow: 0 0 20px rgba(0,255,245,0.15);
488
+ }
489
+
490
+ .md-body h2 {
491
+ font-size: 20px;
492
+ font-weight: 600;
493
+ margin: 32px 0 12px;
494
+ color: var(--neon-yellow);
495
+ padding-bottom: 8px;
496
+ border-bottom: 1px solid var(--border);
497
+ }
498
+
499
+ .md-body h3 {
500
+ font-size: 16px;
501
+ font-weight: 600;
502
+ margin: 24px 0 8px;
503
+ color: var(--neon-purple);
504
+ }
505
+
506
+ .md-body p {
507
+ line-height: 1.75;
508
+ margin: 10px 0;
509
+ color: var(--text-secondary);
510
+ }
511
+
512
+ .md-body ul, .md-body ol { padding-left: 24px; margin: 8px 0; }
513
+
514
+ .md-body li {
515
+ line-height: 1.75;
516
+ color: var(--text-secondary);
517
+ margin: 3px 0;
518
+ }
519
+
520
+ .md-body li::marker { color: var(--neon-cyan); }
521
+
522
+ .md-body a { color: var(--neon-blue); text-decoration: none; }
523
+ .md-body a:hover { text-decoration: underline; text-shadow: 0 0 8px rgba(77,159,255,0.3); }
524
+
525
+ .md-body strong { color: var(--text-primary); font-weight: 600; }
526
+
527
+ .md-body code {
528
+ background: var(--bg-card);
529
+ padding: 2px 7px;
530
+ border-radius: 4px;
531
+ font-family: var(--font-mono);
532
+ font-size: 12px;
533
+ color: var(--neon-orange);
534
+ border: 1px solid var(--border);
535
+ }
536
+
537
+ .md-body pre {
538
+ background: var(--bg-tertiary);
539
+ border: 1px solid var(--border);
540
+ border-radius: 8px;
541
+ padding: 18px;
542
+ overflow-x: auto;
543
+ margin: 16px 0;
544
+ position: relative;
545
+ }
546
+
547
+ .md-body pre::before {
548
+ content: '';
549
+ position: absolute;
550
+ top: 0;
551
+ left: 0;
552
+ right: 0;
553
+ height: 1px;
554
+ background: linear-gradient(90deg, var(--neon-cyan), var(--neon-magenta), transparent);
555
+ opacity: 0.3;
556
+ }
557
+
558
+ .md-body pre code {
559
+ background: none;
560
+ padding: 0;
561
+ border: none;
562
+ color: var(--text-primary);
563
+ font-size: 13px;
564
+ line-height: 1.6;
565
+ }
566
+
567
+ .md-body table {
568
+ width: 100%;
569
+ border-collapse: collapse;
570
+ margin: 16px 0;
571
+ font-size: 13px;
572
+ }
573
+
574
+ .md-body th {
575
+ text-align: left;
576
+ padding: 10px 14px;
577
+ border-bottom: 2px solid var(--neon-cyan);
578
+ color: var(--neon-cyan);
579
+ font-family: var(--font-mono);
580
+ font-weight: 600;
581
+ font-size: 12px;
582
+ text-transform: uppercase;
583
+ letter-spacing: 0.5px;
584
+ }
585
+
586
+ .md-body td {
587
+ padding: 10px 14px;
588
+ border-bottom: 1px solid var(--border);
589
+ color: var(--text-secondary);
590
+ }
591
+
592
+ .md-body tr:hover td { background: var(--bg-hover); }
593
+
594
+ .md-body blockquote {
595
+ border-left: 3px solid var(--neon-magenta);
596
+ padding-left: 16px;
597
+ margin: 16px 0;
598
+ color: var(--text-muted);
599
+ box-shadow: -3px 0 8px rgba(255,0,224,0.1);
600
+ }
601
+
602
+ .md-body hr {
603
+ border: none;
604
+ height: 1px;
605
+ background: linear-gradient(90deg, transparent, var(--border), transparent);
606
+ margin: 24px 0;
607
+ }
608
+
609
+ .wikilink {
610
+ color: var(--neon-purple);
611
+ cursor: pointer;
612
+ text-decoration: none;
613
+ border-bottom: 1px dashed rgba(191, 90, 242, 0.4);
614
+ transition: all 0.15s;
615
+ font-weight: 500;
616
+ }
617
+
618
+ .wikilink:hover {
619
+ color: var(--neon-magenta);
620
+ text-shadow: 0 0 8px rgba(255,0,224,0.3);
621
+ border-bottom-color: var(--neon-magenta);
622
+ }
623
+
624
+ /* ============ EMPTY STATE ============ */
625
+ .empty-state {
626
+ display: flex;
627
+ flex-direction: column;
628
+ align-items: center;
629
+ justify-content: center;
630
+ height: 100%;
631
+ gap: 16px;
632
+ }
633
+
634
+ .empty-state .es-logo {
635
+ font-family: var(--font-mono);
636
+ font-size: 32px;
637
+ font-weight: 700;
638
+ color: var(--neon-cyan);
639
+ text-shadow: var(--glow-cyan), 0 0 40px rgba(0,255,245,0.1);
640
+ letter-spacing: 3px;
641
+ }
642
+
643
+ .empty-state .es-tagline {
644
+ font-family: var(--font-mono);
645
+ font-size: 12px;
646
+ color: var(--text-muted);
647
+ letter-spacing: 2px;
648
+ text-transform: uppercase;
649
+ }
650
+
651
+ .empty-state .es-hint {
652
+ font-size: 13px;
653
+ color: var(--text-muted);
654
+ margin-top: 20px;
655
+ }
656
+
657
+ .empty-state .es-hint kbd {
658
+ font-family: var(--font-mono);
659
+ font-size: 11px;
660
+ background: var(--bg-card);
661
+ border: 1px solid var(--border);
662
+ padding: 2px 6px;
663
+ border-radius: 3px;
664
+ color: var(--text-secondary);
665
+ }
666
+
667
+ /* ============ RIGHT PANEL ============ */
668
+ .right-panel {
669
+ background: var(--bg-secondary);
670
+ border-left: 1px solid var(--border);
671
+ overflow-y: auto;
672
+ padding: 16px;
673
+ }
674
+
675
+ .rp-section { margin-bottom: 24px; }
676
+
677
+ .rp-section-title {
678
+ font-family: var(--font-mono);
679
+ font-size: 10px;
680
+ font-weight: 600;
681
+ text-transform: uppercase;
682
+ letter-spacing: 1.5px;
683
+ color: var(--text-muted);
684
+ margin-bottom: 10px;
685
+ display: flex;
686
+ align-items: center;
687
+ gap: 6px;
688
+ }
689
+
690
+ .rp-section-title .rp-count {
691
+ background: var(--bg-hover);
692
+ color: var(--text-muted);
693
+ font-size: 9px;
694
+ padding: 1px 5px;
695
+ border-radius: 8px;
696
+ }
697
+
698
+ .rp-item {
699
+ display: flex;
700
+ align-items: center;
701
+ padding: 6px 8px;
702
+ border-radius: 5px;
703
+ cursor: pointer;
704
+ font-size: 12px;
705
+ gap: 8px;
706
+ margin-bottom: 2px;
707
+ transition: all 0.12s;
708
+ }
709
+
710
+ .rp-item:hover {
711
+ background: var(--bg-hover);
712
+ }
713
+
714
+ .rp-item .rp-score {
715
+ font-family: var(--font-mono);
716
+ font-size: 11px;
717
+ color: var(--neon-green);
718
+ min-width: 34px;
719
+ text-shadow: 0 0 6px rgba(57,255,20,0.2);
720
+ }
721
+
722
+ .rp-item .rp-title {
723
+ overflow: hidden;
724
+ text-overflow: ellipsis;
725
+ white-space: nowrap;
726
+ color: var(--neon-purple);
727
+ font-weight: 500;
728
+ }
729
+
730
+ .rp-item .rp-backlink-title {
731
+ color: var(--neon-cyan);
732
+ }
733
+
734
+ /* ============ GRAPH ============ */
735
+ #graph-section {
736
+ margin-bottom: 16px;
737
+ }
738
+
739
+ #graph-section.hidden { display: none; }
740
+
741
+ #graph-container {
742
+ width: 100%;
743
+ height: 220px;
744
+ border: 1px solid var(--border);
745
+ border-radius: 8px;
746
+ overflow: hidden;
747
+ background: var(--bg-tertiary);
748
+ position: relative;
749
+ }
750
+
751
+ #graph-container::before {
752
+ content: '';
753
+ position: absolute;
754
+ inset: 0;
755
+ background-image:
756
+ radial-gradient(rgba(0,255,245,0.03) 1px, transparent 1px);
757
+ background-size: 16px 16px;
758
+ pointer-events: none;
759
+ }
760
+
761
+ #graph-container svg { width: 100%; height: 100%; position: relative; z-index: 1; }
762
+
763
+ .graph-node { cursor: pointer; }
764
+
765
+ .graph-node circle {
766
+ fill: var(--neon-purple);
767
+ filter: drop-shadow(0 0 3px rgba(191,90,242,0.5));
768
+ transition: all 0.2s;
769
+ }
770
+
771
+ .graph-node:hover circle {
772
+ fill: var(--neon-cyan);
773
+ filter: drop-shadow(0 0 6px rgba(0,255,245,0.6));
774
+ }
775
+
776
+ .graph-node.active circle {
777
+ fill: var(--neon-cyan);
778
+ r: 7;
779
+ filter: drop-shadow(0 0 8px rgba(0,255,245,0.7));
780
+ }
781
+
782
+ .graph-node text {
783
+ fill: var(--text-secondary);
784
+ font-family: var(--font-mono);
785
+ font-size: 9px;
786
+ }
787
+
788
+ .graph-link {
789
+ stroke: rgba(191, 90, 242, 0.3);
790
+ stroke-width: 1;
791
+ }
792
+
793
+ /* ============ SCROLLBAR ============ */
794
+ ::-webkit-scrollbar { width: 5px; }
795
+ ::-webkit-scrollbar-track { background: transparent; }
796
+ ::-webkit-scrollbar-thumb { background: var(--bg-active); border-radius: 3px; }
797
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
798
+
799
+ /* ============ TRANSITIONS ============ */
800
+ .fade-in {
801
+ animation: fadeIn 0.2s ease;
802
+ }
803
+
804
+ @keyframes fadeIn {
805
+ from { opacity: 0; transform: translateY(4px); }
806
+ to { opacity: 1; transform: translateY(0); }
807
+ }
808
+
809
+ /* Pulse animation for active graph node */
810
+ @keyframes pulse {
811
+ 0%, 100% { opacity: 1; }
812
+ 50% { opacity: 0.6; }
813
+ }
814
+ </style>
815
+ </head>
816
+ <body>
817
+
818
+ <div class="layout" id="layout">
819
+ <!-- Header -->
820
+ <div class="header">
821
+ <div class="logo">KYP-MEM <span class="logo-sub">know your project</span></div>
822
+ <div class="breadcrumb" id="breadcrumb"></div>
823
+ <div class="header-actions">
824
+ <button class="toggle-btn active" id="graph-toggle" title="Toggle graph view">
825
+ <span class="dot"></span>
826
+ <span>GRAPH</span>
827
+ </button>
828
+ <div class="search-box">
829
+ <span class="search-icon">&#9906;</span>
830
+ <input type="text" id="search-input" placeholder="Search...">
831
+ <span class="search-hint">&#8984;K</span>
832
+ <div class="search-results" id="search-results"></div>
833
+ </div>
834
+ </div>
835
+ </div>
836
+
837
+ <!-- Left sidebar -->
838
+ <div class="sidebar">
839
+ <div class="sidebar-scroll">
840
+ <div class="sidebar-section">
841
+ <div class="sidebar-section-title">Explorer</div>
842
+ <div id="file-tree"></div>
843
+ </div>
844
+ </div>
845
+ <div class="stats-bar" id="stats-bar"></div>
846
+ </div>
847
+
848
+ <!-- Resize handle: sidebar | content -->
849
+ <div class="resize-handle" id="resize-left"></div>
850
+
851
+ <!-- Content -->
852
+ <div class="content" id="content">
853
+ <div class="empty-state">
854
+ <div class="es-logo">KYP-MEM</div>
855
+ <div class="es-tagline">Know Your Project Memory</div>
856
+ <div class="es-hint">Select a note from the sidebar or press <kbd>&#8984;K</kbd> to search</div>
857
+ </div>
858
+ </div>
859
+
860
+ <!-- Resize handle: content | right panel -->
861
+ <div class="resize-handle" id="resize-right"></div>
862
+
863
+ <!-- Right panel -->
864
+ <div class="right-panel" id="right-panel">
865
+ <div id="graph-section">
866
+ <div class="rp-section-title">Graph View</div>
867
+ <div id="graph-container"></div>
868
+ </div>
869
+ <div id="rp-backlinks" class="rp-section" style="display:none;">
870
+ <div class="rp-section-title">Backlinks <span class="rp-count" id="bl-count"></span></div>
871
+ <div id="rp-backlinks-list"></div>
872
+ </div>
873
+ <div id="rp-related" class="rp-section" style="display:none;">
874
+ <div class="rp-section-title">Related <span class="rp-count" id="rel-count"></span></div>
875
+ <div id="rp-related-list"></div>
876
+ </div>
877
+ <div id="rp-outlinks" class="rp-section" style="display:none;">
878
+ <div class="rp-section-title">Outgoing Links <span class="rp-count" id="out-count"></span></div>
879
+ <div id="rp-outlinks-list"></div>
880
+ </div>
881
+ </div>
882
+ </div>
883
+
884
+ <script>
885
+ let currentPath = null;
886
+ let treeData = null;
887
+ let allNotes = {};
888
+ let graphVisible = true;
889
+
890
+ async function fetchJSON(url) {
891
+ const r = await fetch(url);
892
+ return r.json();
893
+ }
894
+
895
+ // --- Graph Toggle ---
896
+ document.getElementById('graph-toggle').addEventListener('click', () => {
897
+ graphVisible = !graphVisible;
898
+ const btn = document.getElementById('graph-toggle');
899
+ const section = document.getElementById('graph-section');
900
+ const layout = document.getElementById('layout');
901
+ btn.classList.toggle('active', graphVisible);
902
+
903
+ if (!graphVisible) {
904
+ section.classList.add('hidden');
905
+ layout.classList.add('no-right-panel');
906
+ } else {
907
+ section.classList.remove('hidden');
908
+ layout.classList.remove('no-right-panel');
909
+ if (currentPath) loadNote(currentPath);
910
+ }
911
+ });
912
+
913
+ // --- Keyboard shortcut ---
914
+ document.addEventListener('keydown', (e) => {
915
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
916
+ e.preventDefault();
917
+ document.getElementById('search-input').focus();
918
+ }
919
+ });
920
+
921
+ // --- File Tree ---
922
+ function renderTree(node, container) {
923
+ if (node.type === 'folder' && node.name !== 'vault') {
924
+ const item = document.createElement('div');
925
+ item.className = 'tree-item';
926
+ item.innerHTML = `<span class="arrow open">&#9654;</span><span class="icon folder-icon">&#9776;</span><span class="name">${node.name}</span>`;
927
+
928
+ const children = document.createElement('div');
929
+ children.className = 'tree-children';
930
+
931
+ item.addEventListener('click', (e) => {
932
+ e.stopPropagation();
933
+ item.querySelector('.arrow').classList.toggle('open');
934
+ children.classList.toggle('collapsed');
935
+ });
936
+
937
+ container.appendChild(item);
938
+ container.appendChild(children);
939
+ (node.children || []).forEach(c => renderTree(c, children));
940
+
941
+ } else if (node.type === 'note') {
942
+ const item = document.createElement('div');
943
+ item.className = 'tree-item';
944
+ item.dataset.path = node.path;
945
+ item.innerHTML = `<span class="arrow" style="visibility:hidden">&#9654;</span><span class="icon note-icon">&#9671;</span><span class="name">${node.name.replace('.md', '')}</span>`;
946
+ item.addEventListener('click', () => loadNote(node.path));
947
+ container.appendChild(item);
948
+
949
+ } else if (node.children) {
950
+ node.children.forEach(c => renderTree(c, container));
951
+ }
952
+ }
953
+
954
+ // --- Note rendering ---
955
+ async function loadNote(path) {
956
+ currentPath = path;
957
+ const note = await fetchJSON(`/api/note/${path}`);
958
+ if (note.error) return;
959
+
960
+ document.querySelectorAll('.tree-item').forEach(el => {
961
+ el.classList.toggle('active', el.dataset.path === path);
962
+ });
963
+
964
+ // Breadcrumb
965
+ const parts = path.replace('.md', '').split('/');
966
+ document.getElementById('breadcrumb').innerHTML = parts.map((p, i) =>
967
+ i < parts.length - 1
968
+ ? `<span>${p}</span><span class="sep">/</span>`
969
+ : `<span class="current">${p}</span>`
970
+ ).join('');
971
+
972
+ // Content
973
+ const contentEl = document.getElementById('content');
974
+ let html = '<div class="fade-in">';
975
+
976
+ html += '<div class="note-properties">';
977
+ (note.tags || []).forEach(t => { html += `<span class="tag">#${t}</span>`; });
978
+ Object.entries(note.properties || {}).forEach(([k, v]) => {
979
+ html += `<span class="prop-item"><span class="prop-key">${k}:</span> ${v}</span>`;
980
+ });
981
+ if (note.created) html += `<span class="prop-item"><span class="prop-key">created:</span> ${note.created}</span>`;
982
+ if (note.updated && note.updated !== note.created) html += `<span class="prop-item"><span class="prop-key">updated:</span> ${note.updated}</span>`;
983
+ html += '</div>';
984
+
985
+ let md = note.content || '';
986
+ md = md.replace(/\[\[([^\]]+)\]\]/g, (_, link) => {
987
+ const display = link.includes('#') ? link.split('#').pop() : link;
988
+ return `<span class="wikilink" data-link="${link}">${display}</span>`;
989
+ });
990
+
991
+ html += `<div class="md-body">${marked.parse(md)}</div>`;
992
+ html += '</div>';
993
+ contentEl.innerHTML = html;
994
+ contentEl.scrollTop = 0;
995
+
996
+ contentEl.querySelectorAll('.wikilink').forEach(el => {
997
+ el.addEventListener('click', () => {
998
+ const target = findNotePath(el.dataset.link.split('#')[0]);
999
+ if (target) loadNote(target);
1000
+ });
1001
+ });
1002
+
1003
+ renderRightPanel(note);
1004
+ if (graphVisible) renderGraph(note);
1005
+ }
1006
+
1007
+ function findNotePath(name) {
1008
+ const lower = name.toLowerCase();
1009
+ for (const [path, note] of Object.entries(allNotes)) {
1010
+ const stem = path.split('/').pop().replace('.md', '').toLowerCase();
1011
+ if (stem === lower || note.title.toLowerCase() === lower) return path;
1012
+ }
1013
+ return null;
1014
+ }
1015
+
1016
+ // --- Right Panel ---
1017
+ function renderRightPanel(note) {
1018
+ const blSection = document.getElementById('rp-backlinks');
1019
+ const blList = document.getElementById('rp-backlinks-list');
1020
+ blList.innerHTML = '';
1021
+ if (note.backlinks && note.backlinks.length) {
1022
+ blSection.style.display = '';
1023
+ document.getElementById('bl-count').textContent = note.backlinks.length;
1024
+ note.backlinks.forEach(path => {
1025
+ const n = allNotes[path];
1026
+ const item = document.createElement('div');
1027
+ item.className = 'rp-item';
1028
+ item.innerHTML = `<span class="rp-title rp-backlink-title">${n ? n.title : path}</span>`;
1029
+ item.addEventListener('click', () => loadNote(path));
1030
+ blList.appendChild(item);
1031
+ });
1032
+ } else {
1033
+ blSection.style.display = 'none';
1034
+ }
1035
+
1036
+ const relSection = document.getElementById('rp-related');
1037
+ const relList = document.getElementById('rp-related-list');
1038
+ relList.innerHTML = '';
1039
+ if (note.related && note.related.length) {
1040
+ relSection.style.display = '';
1041
+ document.getElementById('rel-count').textContent = note.related.length;
1042
+ note.related.forEach(r => {
1043
+ const item = document.createElement('div');
1044
+ item.className = 'rp-item';
1045
+ item.innerHTML = `<span class="rp-score">${r.score.toFixed(2)}</span><span class="rp-title">${r.title}</span>`;
1046
+ item.addEventListener('click', () => loadNote(r.path));
1047
+ relList.appendChild(item);
1048
+ });
1049
+ } else {
1050
+ relSection.style.display = 'none';
1051
+ }
1052
+
1053
+ const outSection = document.getElementById('rp-outlinks');
1054
+ const outList = document.getElementById('rp-outlinks-list');
1055
+ outList.innerHTML = '';
1056
+ if (note.links && note.links.length) {
1057
+ outSection.style.display = '';
1058
+ document.getElementById('out-count').textContent = note.links.length;
1059
+ note.links.forEach(link => {
1060
+ const path = findNotePath(link);
1061
+ const item = document.createElement('div');
1062
+ item.className = 'rp-item';
1063
+ item.innerHTML = `<span class="rp-title">${link}</span>`;
1064
+ if (path) item.addEventListener('click', () => loadNote(path));
1065
+ else item.style.opacity = '0.4';
1066
+ outList.appendChild(item);
1067
+ });
1068
+ } else {
1069
+ outSection.style.display = 'none';
1070
+ }
1071
+ }
1072
+
1073
+ // --- Graph ---
1074
+ function renderGraph(note) {
1075
+ const container = document.getElementById('graph-container');
1076
+ container.innerHTML = '';
1077
+
1078
+ const nodes = new Map();
1079
+ const links = [];
1080
+
1081
+ nodes.set(note.path, { id: note.path, title: note.title, active: true });
1082
+
1083
+ (note.links || []).forEach(link => {
1084
+ const path = findNotePath(link);
1085
+ if (path && !nodes.has(path)) {
1086
+ const n = allNotes[path];
1087
+ nodes.set(path, { id: path, title: n ? n.title : link, active: false });
1088
+ }
1089
+ if (path) links.push({ source: note.path, target: path });
1090
+ });
1091
+
1092
+ (note.backlinks || []).forEach(path => {
1093
+ if (!nodes.has(path)) {
1094
+ const n = allNotes[path];
1095
+ nodes.set(path, { id: path, title: n ? n.title : path, active: false });
1096
+ }
1097
+ links.push({ source: path, target: note.path });
1098
+ });
1099
+
1100
+ (note.related || []).forEach(r => {
1101
+ if (!nodes.has(r.path)) {
1102
+ nodes.set(r.path, { id: r.path, title: r.title, active: false });
1103
+ }
1104
+ });
1105
+
1106
+ const nodeArray = Array.from(nodes.values());
1107
+ if (nodeArray.length < 2) {
1108
+ container.innerHTML = '<svg></svg>';
1109
+ return;
1110
+ }
1111
+
1112
+ const width = container.clientWidth;
1113
+ const height = container.clientHeight;
1114
+
1115
+ const svg = d3.select(container).append('svg')
1116
+ .attr('viewBox', `0 0 ${width} ${height}`);
1117
+
1118
+ const defs = svg.append('defs');
1119
+ const grad = defs.append('radialGradient').attr('id', 'node-glow');
1120
+ grad.append('stop').attr('offset', '0%').attr('stop-color', 'var(--neon-cyan)').attr('stop-opacity', 0.3);
1121
+ grad.append('stop').attr('offset', '100%').attr('stop-color', 'var(--neon-cyan)').attr('stop-opacity', 0);
1122
+
1123
+ const simulation = d3.forceSimulation(nodeArray)
1124
+ .force('link', d3.forceLink(links).id(d => d.id).distance(55))
1125
+ .force('charge', d3.forceManyBody().strength(-120))
1126
+ .force('center', d3.forceCenter(width / 2, height / 2))
1127
+ .force('collision', d3.forceCollide().radius(25));
1128
+
1129
+ const link = svg.selectAll('.graph-link')
1130
+ .data(links).enter().append('line')
1131
+ .attr('class', 'graph-link');
1132
+
1133
+ const node = svg.selectAll('.graph-node')
1134
+ .data(nodeArray).enter().append('g')
1135
+ .attr('class', d => 'graph-node' + (d.active ? ' active' : ''))
1136
+ .on('click', (e, d) => loadNote(d.id))
1137
+ .call(d3.drag()
1138
+ .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
1139
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
1140
+ .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
1141
+ );
1142
+
1143
+ node.filter(d => d.active).append('circle').attr('r', 14).style('fill', 'url(#node-glow)').style('stroke', 'none');
1144
+ node.append('circle').attr('r', d => d.active ? 6 : 4);
1145
+ node.append('text').text(d => d.title).attr('dx', 12).attr('dy', 3);
1146
+
1147
+ simulation.on('tick', () => {
1148
+ link
1149
+ .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
1150
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
1151
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
1152
+ });
1153
+ }
1154
+
1155
+ // --- Search ---
1156
+ let searchTimeout;
1157
+ const searchInput = document.getElementById('search-input');
1158
+ const searchResults = document.getElementById('search-results');
1159
+
1160
+ searchInput.addEventListener('input', () => {
1161
+ clearTimeout(searchTimeout);
1162
+ const q = searchInput.value.trim();
1163
+ if (!q) { searchResults.classList.remove('active'); return; }
1164
+ searchTimeout = setTimeout(async () => {
1165
+ const results = await fetchJSON(`/api/search?q=${encodeURIComponent(q)}`);
1166
+ searchResults.innerHTML = '';
1167
+ if (results.length === 0) {
1168
+ searchResults.innerHTML = '<div class="search-result"><div class="sr-title" style="color:var(--text-muted)">No results</div></div>';
1169
+ } else {
1170
+ results.forEach(r => {
1171
+ const div = document.createElement('div');
1172
+ div.className = 'search-result';
1173
+ div.innerHTML = `<div class="sr-title">${r.title}</div><div class="sr-path">${r.path}</div>${r.snippet ? `<div class="sr-snippet">${r.snippet}</div>` : ''}`;
1174
+ div.addEventListener('click', () => {
1175
+ loadNote(r.path);
1176
+ searchResults.classList.remove('active');
1177
+ searchInput.value = '';
1178
+ });
1179
+ searchResults.appendChild(div);
1180
+ });
1181
+ }
1182
+ searchResults.classList.add('active');
1183
+ }, 200);
1184
+ });
1185
+
1186
+ searchInput.addEventListener('focus', () => {
1187
+ document.querySelector('.search-hint').style.display = 'none';
1188
+ });
1189
+
1190
+ searchInput.addEventListener('blur', () => {
1191
+ if (!searchInput.value) document.querySelector('.search-hint').style.display = '';
1192
+ });
1193
+
1194
+ document.addEventListener('click', (e) => {
1195
+ if (!e.target.closest('.search-box')) searchResults.classList.remove('active');
1196
+ });
1197
+
1198
+ // --- Resizable Panels ---
1199
+ function initResize() {
1200
+ const layout = document.getElementById('layout');
1201
+ const leftHandle = document.getElementById('resize-left');
1202
+ const rightHandle = document.getElementById('resize-right');
1203
+
1204
+ let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 260;
1205
+ let rightW = parseInt(localStorage.getItem('kyp-right-w')) || 280;
1206
+
1207
+ layout.style.setProperty('--sidebar-w', sidebarW + 'px');
1208
+ layout.style.setProperty('--right-w', rightW + 'px');
1209
+
1210
+ function makeDraggable(handle, opts) {
1211
+ handle.addEventListener('mousedown', (e) => {
1212
+ e.preventDefault();
1213
+ const startX = e.clientX;
1214
+ const startW = opts.getWidth();
1215
+ handle.classList.add('dragging');
1216
+ document.body.classList.add('resizing');
1217
+
1218
+ function onMove(e) {
1219
+ const delta = e.clientX - startX;
1220
+ const adjusted = opts.invert ? startW - delta : startW + delta;
1221
+ const clamped = Math.max(opts.min, Math.min(opts.max, adjusted));
1222
+ opts.setWidth(clamped);
1223
+ }
1224
+
1225
+ function onUp() {
1226
+ handle.classList.remove('dragging');
1227
+ document.body.classList.remove('resizing');
1228
+ opts.save();
1229
+ if (graphVisible && currentPath) loadNote(currentPath);
1230
+ document.removeEventListener('mousemove', onMove);
1231
+ document.removeEventListener('mouseup', onUp);
1232
+ }
1233
+
1234
+ document.addEventListener('mousemove', onMove);
1235
+ document.addEventListener('mouseup', onUp);
1236
+ });
1237
+ }
1238
+
1239
+ makeDraggable(leftHandle, {
1240
+ getWidth: () => sidebarW,
1241
+ setWidth: (w) => { sidebarW = w; layout.style.setProperty('--sidebar-w', w + 'px'); },
1242
+ save: () => localStorage.setItem('kyp-sidebar-w', sidebarW),
1243
+ invert: false,
1244
+ min: 180,
1245
+ max: 450,
1246
+ });
1247
+
1248
+ makeDraggable(rightHandle, {
1249
+ getWidth: () => rightW,
1250
+ setWidth: (w) => { rightW = w; layout.style.setProperty('--right-w', w + 'px'); },
1251
+ save: () => localStorage.setItem('kyp-right-w', rightW),
1252
+ invert: true,
1253
+ min: 200,
1254
+ max: 500,
1255
+ });
1256
+ }
1257
+
1258
+ // --- Init ---
1259
+ async function init() {
1260
+ treeData = await fetchJSON('/api/tree');
1261
+ const stats = await fetchJSON('/api/stats');
1262
+
1263
+ function walk(node) {
1264
+ if (node.type === 'note') {
1265
+ allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
1266
+ }
1267
+ (node.children || []).forEach(walk);
1268
+ }
1269
+ walk(treeData);
1270
+
1271
+ const promises = Object.keys(allNotes).map(async path => {
1272
+ try {
1273
+ const note = await fetchJSON(`/api/note/${path}`);
1274
+ if (!note.error) allNotes[path] = { title: note.title, tags: note.tags };
1275
+ } catch {}
1276
+ });
1277
+ await Promise.all(promises);
1278
+
1279
+ renderTree(treeData, document.getElementById('file-tree'));
1280
+
1281
+ document.getElementById('stats-bar').innerHTML = `
1282
+ <span><span class="stat-val">${stats.notes}</span> notes</span>
1283
+ <span><span class="stat-val">${stats.folders}</span> folders</span>
1284
+ <span><span class="stat-val">${stats.tags}</span> tags</span>
1285
+ <span><span class="stat-val">${stats.links}</span> links</span>
1286
+ `;
1287
+ }
1288
+
1289
+ initResize();
1290
+ init();
1291
+ </script>
1292
+ </body>
1293
+ </html>