kyp-mem 0.4.3 → 0.5.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.
@@ -1,2931 +1,1845 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-density="regular">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>KYP-MEM</title>
7
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">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
9
10
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
11
  <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
11
12
  <style>
12
13
  :root {
13
- --bg-void: #09090b;
14
- --bg-primary: #0d0d10;
15
- --bg-secondary: #0f0f12;
16
- --bg-tertiary: #08080a;
17
- --bg-hover: #15151c;
18
- --bg-active: #1c1c26;
19
- --bg-card: #111116;
20
- --bg-surface: #0b0b0e;
21
-
22
- --neon-cyan: #D97757;
23
- --neon-green: #5bb98c;
24
- --neon-magenta: #c47ad7;
25
- --neon-purple: #a78bfa;
26
- --neon-orange: #e8935a;
27
- --neon-yellow: #d4a853;
28
- --neon-blue: #7da8d4;
29
- --neon-red: #d47171;
30
-
31
- --glow-cyan: 0 0 8px #D9775730;
32
- --glow-green: 0 0 8px #5bb98c30;
33
- --glow-magenta: 0 0 8px #c47ad730;
34
-
35
- --text-primary: #d8d5cf;
36
- --text-secondary: #807b73;
37
- --text-muted: #55514a;
38
- --border: rgba(255,255,255,0.06);
39
- --border-subtle: rgba(255,255,255,0.03);
40
-
41
- --font: 'Inter', -apple-system, sans-serif;
42
- --font-mono: 'JetBrains Mono', monospace;
43
-
44
- --radius-sm: 4px;
45
- --radius: 8px;
46
- --radius-lg: 12px;
47
- }
48
-
49
- * { margin: 0; padding: 0; box-sizing: border-box; }
50
-
14
+ --bg: oklch(0.165 0.008 60);
15
+ --bg-2: oklch(0.195 0.009 60);
16
+ --panel: oklch(0.215 0.010 60);
17
+ --panel-2: oklch(0.245 0.010 60);
18
+ --line: oklch(0.305 0.012 55);
19
+ --line-2: oklch(0.385 0.014 55);
20
+ --fg: oklch(0.945 0.012 75);
21
+ --muted: oklch(0.68 0.012 65);
22
+ --dim: oklch(0.50 0.010 60);
23
+ --accent: oklch(0.78 0.13 45);
24
+ --accent-dim: oklch(0.55 0.10 45);
25
+ --warn: oklch(0.82 0.13 80);
26
+ --pin: oklch(0.78 0.13 30);
27
+ --link: oklch(0.78 0.10 220);
28
+ --r-sm: 3px;
29
+ --r: 5px;
30
+ --r-lg: 8px;
31
+ --pad-y: 14px;
32
+ --pad-x: 18px;
33
+ --row-y: 10px;
34
+ --gap: 14px;
35
+ --fz: 13px;
36
+ --fz-sm: 11.5px;
37
+ --fz-xs: 10.5px;
38
+ --fz-lg: 15px;
39
+ --fz-xl: 22px;
40
+ }
41
+ html[data-density="dense"] {
42
+ --pad-y: 10px; --pad-x: 14px; --row-y: 6px; --gap: 10px;
43
+ --fz: 12px; --fz-sm: 11px; --fz-xs: 10px; --fz-lg: 14px; --fz-xl: 20px;
44
+ }
45
+ html[data-density="comfy"] {
46
+ --pad-y: 18px; --pad-x: 22px; --row-y: 14px; --gap: 18px;
47
+ --fz: 14px; --fz-sm: 12px; --fz-xs: 11px; --fz-lg: 16px; --fz-xl: 24px;
48
+ }
49
+
50
+ * { box-sizing: border-box; margin: 0; padding: 0; }
51
+ html, body, #app { height: 100%; }
51
52
  body {
52
- font-family: var(--font);
53
- background: var(--bg-void);
54
- background-image: radial-gradient(circle at 50% 0%, var(--bg-secondary) 0%, var(--bg-void) 70%);
55
- color: var(--text-primary);
56
- height: 100vh;
57
- overflow: hidden;
53
+ background: var(--bg);
54
+ color: var(--fg);
55
+ font: 400 var(--fz)/1.5 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
56
+ font-feature-settings: "calt" 1, "liga" 0, "zero" 1, "ss01" 1;
58
57
  -webkit-font-smoothing: antialiased;
58
+ overflow: hidden;
59
59
  }
60
-
61
- body::before {
62
- content: '';
63
- position: fixed;
64
- inset: 0;
65
- background-image:
66
- linear-gradient(rgba(217,119,87,0.012) 1px, transparent 1px),
67
- linear-gradient(90deg, rgba(217,119,87,0.012) 1px, transparent 1px);
68
- background-size: 48px 48px;
69
- pointer-events: none;
70
- z-index: 0;
71
- }
72
-
73
- .layout {
60
+ ::selection { background: color-mix(in oklch, var(--accent) 35%, transparent); color: var(--fg); }
61
+ @keyframes kypBlink { 0%, 55% { opacity: 0.7; } 56%, 100% { opacity: 0; } }
62
+ @keyframes kypCaret { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
63
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
64
+ ::-webkit-scrollbar-track { background: transparent; }
65
+ ::-webkit-scrollbar-thumb { background: var(--line); border-radius: 10px; border: 2px solid var(--bg); }
66
+ ::-webkit-scrollbar-thumb:hover { background: var(--line-2); }
67
+ button, input, select, textarea { font: inherit; color: inherit; }
68
+ button { background: none; border: 0; padding: 0; cursor: pointer; }
69
+ a, .lnk { color: var(--link); text-decoration: none; }
70
+ a:hover, .lnk:hover { text-decoration: underline; text-underline-offset: 3px; }
71
+ .acc { color: var(--accent); }
72
+ .mut { color: var(--muted); }
73
+ .dim { color: var(--dim); }
74
+ .tab-nums { font-variant-numeric: tabular-nums; }
75
+
76
+ /* Layout */
77
+ #app {
74
78
  display: grid;
75
- grid-template-columns: var(--sidebar-w, 256px) 1px 1fr 1px var(--right-w, 272px);
76
- grid-template-rows: 48px 1fr;
79
+ grid-template-rows: 44px 1fr 24px;
77
80
  height: 100vh;
78
- position: relative;
79
- z-index: 1;
81
+ min-height: 0;
80
82
  }
81
-
82
- .layout.no-right-panel {
83
- grid-template-columns: var(--sidebar-w, 256px) 1px 1fr;
83
+ #main {
84
+ display: grid;
85
+ grid-template-columns: var(--sidebar-w, 236px) auto minmax(0, 1fr);
86
+ min-height: 0;
87
+ border-top: 1px solid var(--line);
88
+ }
89
+ #main.sidebar-hidden {
90
+ grid-template-columns: 0 0 minmax(0, 1fr);
84
91
  }
85
- .layout.no-right-panel .right-panel,
86
- .layout.no-right-panel #resize-right { display: none; }
92
+ #main.sidebar-hidden #sidebar,
93
+ #main.sidebar-hidden #resize-handle { display: none; }
87
94
 
88
- /* ============ RESIZE HANDLES ============ */
89
- .resize-handle {
90
- background: var(--border-subtle);
95
+ /* Resize handle */
96
+ #resize-handle {
97
+ width: 1px;
98
+ background: var(--line);
91
99
  cursor: col-resize;
92
100
  position: relative;
93
101
  z-index: 10;
94
- transition: background 0.3s;
102
+ transition: background 0.2s;
95
103
  }
96
-
97
- .resize-handle::before {
104
+ #resize-handle::before {
98
105
  content: '';
99
106
  position: absolute;
100
107
  inset: 0 -4px;
101
108
  z-index: 1;
102
109
  }
103
-
104
- .resize-handle:hover,
105
- .resize-handle.dragging {
106
- background: var(--neon-cyan);
107
- box-shadow: 0 0 12px rgba(217,119,87,0.2);
110
+ #resize-handle:hover, #resize-handle.dragging {
111
+ background: var(--accent);
112
+ box-shadow: 0 0 8px color-mix(in oklch, var(--accent) 40%, transparent);
108
113
  }
109
-
110
114
  body.resizing { cursor: col-resize !important; user-select: none !important; }
111
115
  body.resizing * { cursor: col-resize !important; pointer-events: none !important; }
112
- body.resizing .resize-handle { pointer-events: auto !important; }
113
-
114
- /* ============ HEADER ============ */
115
- .header {
116
- grid-column: 1 / -1;
117
- background: rgba(8,8,10,0.6);
118
- backdrop-filter: blur(12px);
119
- -webkit-backdrop-filter: blur(12px);
120
- border-bottom: 1px solid var(--border);
121
- display: flex;
116
+ body.resizing #resize-handle { pointer-events: auto !important; }
117
+
118
+ /* TopBar */
119
+ #topbar {
120
+ display: grid;
121
+ grid-template-columns: minmax(0, 1fr) auto;
122
122
  align-items: center;
123
- padding: 0 20px;
124
- gap: 16px;
125
- position: relative;
123
+ padding: 0 12px;
124
+ gap: 12px;
126
125
  }
127
-
128
- .header::after {
129
- content: '';
130
- position: absolute;
131
- bottom: 0;
132
- left: 0;
133
- right: 0;
134
- height: 1px;
135
- background: linear-gradient(90deg, transparent, var(--neon-cyan), var(--neon-magenta), transparent);
136
- opacity: 0.2;
126
+ .topbar-left { display: flex; align-items: center; gap: 14px; min-width: 0; }
127
+ .topbar-right { display: flex; align-items: center; gap: 8px; }
128
+ .toggle-btn {
129
+ width: 22px; height: 22px;
130
+ display: inline-flex; align-items: center; justify-content: center;
131
+ color: var(--muted); border-radius: var(--r-sm);
132
+ border: 1px solid transparent;
137
133
  }
134
+ .toggle-btn:hover { border-color: var(--line); color: var(--fg); }
138
135
 
136
+ /* Logo */
139
137
  .logo {
140
- font-family: var(--font-mono);
141
- font-weight: 700;
142
- font-size: 13px;
143
- color: var(--neon-cyan);
144
- letter-spacing: 2px;
145
- flex-shrink: 0;
146
- }
147
-
148
- .logo .logo-sub {
149
- font-size: 10px;
150
- font-weight: 400;
151
- color: var(--text-muted);
152
- letter-spacing: 0.5px;
153
- margin-left: 10px;
154
- }
155
-
156
- .breadcrumb {
157
- font-family: var(--font-mono);
158
- font-size: 11px;
159
- color: var(--text-secondary);
160
- overflow: hidden;
161
- text-overflow: ellipsis;
138
+ display: inline-flex; align-items: center; gap: 8px;
139
+ font-weight: 700; letter-spacing: 0.08em;
140
+ color: var(--accent); font-size: var(--fz-lg);
162
141
  white-space: nowrap;
163
142
  }
164
-
165
- .breadcrumb .sep { color: var(--text-muted); margin: 0 5px; }
166
- .breadcrumb .current { color: var(--neon-cyan); font-weight: 500; }
167
-
168
- .header-actions {
169
- margin-left: auto;
170
- display: flex;
171
- align-items: center;
172
- gap: 8px;
173
- }
174
-
175
- .header-btn {
176
- background: transparent;
177
- border: 1px solid var(--border);
178
- color: var(--text-muted);
179
- font-family: var(--font-mono);
180
- font-size: 10px;
181
- padding: 5px 10px;
182
- border-radius: var(--radius);
183
- cursor: pointer;
184
- transition: all 0.2s;
185
- display: flex;
186
- align-items: center;
187
- gap: 5px;
188
- letter-spacing: 0.5px;
189
- }
190
-
191
- .header-btn:hover {
192
- border-color: var(--text-muted);
193
- color: var(--text-secondary);
194
- }
195
-
196
- .header-btn.active {
197
- border-color: rgba(217,119,87,0.3);
198
- color: var(--neon-cyan);
199
- background: rgba(217,119,87,0.05);
200
- }
201
-
202
- .header-btn .dot {
203
- width: 5px;
204
- height: 5px;
205
- border-radius: 50%;
206
- background: var(--text-muted);
207
- transition: all 0.2s;
143
+ .logo-mark {
144
+ display: inline-block; width: 8px; height: 8px;
145
+ background: currentColor; border-radius: 1.5px;
146
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent), 0 0 10px color-mix(in srgb, var(--accent) 60%, transparent);
147
+ }
148
+
149
+ /* Breadcrumb */
150
+ .breadcrumb { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fz-sm); color: var(--muted); }
151
+ .breadcrumb .bc-last { color: var(--fg); }
152
+ .breadcrumb .bc-sep { color: var(--dim); }
153
+
154
+ /* View switcher */
155
+ .view-switch {
156
+ display: inline-flex; border: 1px solid var(--line);
157
+ border-radius: var(--r-sm); padding: 2px; background: var(--panel);
158
+ }
159
+ .view-switch button {
160
+ padding: 2px 10px; font-size: var(--fz-sm);
161
+ color: var(--muted); background: transparent; border-radius: 3px;
162
+ }
163
+ .view-switch button.active {
164
+ color: var(--accent);
165
+ background: color-mix(in oklch, var(--accent) 14%, var(--panel));
166
+ }
167
+
168
+ /* Ghost button */
169
+ .ghost-btn {
170
+ padding: 3px 9px; border: 1px solid var(--line);
171
+ background: var(--panel); color: var(--muted);
172
+ border-radius: var(--r-sm); font-size: var(--fz-sm);
173
+ white-space: nowrap; transition: all 0.12s;
174
+ }
175
+ .ghost-btn:hover { border-color: var(--line-2); background: var(--panel-2); color: var(--fg); }
176
+
177
+ /* Search bar */
178
+ .search-bar {
179
+ display: inline-flex; align-items: center; gap: 8px;
180
+ padding: 3px 8px 3px 10px; border: 1px solid var(--line);
181
+ background: var(--panel); border-radius: var(--r-sm);
182
+ width: 220px; font-size: var(--fz-sm); position: relative;
183
+ }
184
+ .search-bar .s-prompt { color: var(--accent); }
185
+ .search-bar input {
186
+ flex: 1; min-width: 0; background: transparent; border: 0; outline: 0;
187
+ color: var(--fg); font-size: var(--fz-sm); caret-color: var(--accent);
188
+ }
189
+ .search-bar input::placeholder { color: var(--dim); }
190
+ .search-bar .s-caret {
191
+ width: 6px; height: 12px; background: var(--accent);
192
+ opacity: 0.6; margin-right: 2px;
193
+ animation: kypCaret 1.05s steps(1) infinite;
194
+ }
195
+ .search-bar .s-hint {
196
+ font-size: var(--fz-xs); border: 1px solid var(--line);
197
+ border-radius: 3px; padding: 0 5px; color: var(--dim);
198
+ font-variant-numeric: tabular-nums;
199
+ }
200
+ .search-dropdown {
201
+ position: absolute; top: calc(100% + 6px); left: 0; right: 0;
202
+ background: var(--panel); border: 1px solid var(--line);
203
+ border-radius: var(--r); max-height: 320px; overflow-y: auto;
204
+ z-index: 100; display: none; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
205
+ }
206
+ .search-dropdown.active { display: block; }
207
+ .search-result {
208
+ padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--line);
209
+ transition: background 0.1s;
208
210
  }
211
+ .search-result:last-child { border-bottom: none; }
212
+ .search-result:hover { background: var(--panel-2); }
213
+ .search-result .sr-title { font-size: var(--fz-sm); color: var(--accent); font-weight: 500; }
214
+ .search-result .sr-path { font-size: var(--fz-xs); color: var(--dim); margin-top: 2px; }
215
+ .search-result .sr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 3px; }
209
216
 
210
- .header-btn.active .dot {
211
- background: var(--neon-green);
212
- box-shadow: 0 0 6px var(--neon-green);
217
+ /* Sidebar */
218
+ #sidebar {
219
+ display: flex; flex-direction: column;
220
+ background: var(--bg); overflow: hidden; min-height: 0;
213
221
  }
214
-
215
- .search-box {
216
- position: relative;
222
+ .sidebar-scroll {
223
+ overflow-y: auto; padding: 12px 4px 12px 12px;
224
+ display: flex; flex-direction: column; gap: 16px;
225
+ min-height: 0; flex: 1;
226
+ }
227
+ .side-section-header {
228
+ display: flex; align-items: center; justify-content: space-between;
229
+ padding: 0 0 8px;
230
+ }
231
+ .side-section-title {
232
+ display: inline-flex; align-items: center; gap: 6px;
233
+ font-size: var(--fz-xs); letter-spacing: 0.08em;
234
+ text-transform: uppercase; color: var(--dim);
235
+ }
236
+ .side-dot {
237
+ display: inline-block; width: 6px; height: 6px; border-radius: 999px;
238
+ }
239
+ .side-add-btn {
240
+ font-size: var(--fz-xs); color: var(--dim); padding: 0 5px;
241
+ border-radius: var(--r-sm); border: 1px solid transparent;
242
+ transition: all 0.12s;
243
+ }
244
+ .side-add-btn:hover { border-color: var(--line); color: var(--fg); }
245
+ .side-collapse-arrow {
246
+ display: inline-block; transition: transform 0.15s; margin-left: 4px;
247
+ }
248
+ .side-collapse-arrow.collapsed { transform: rotate(-90deg); }
249
+
250
+ /* Sidebar search */
251
+ .side-search {
252
+ width: 100%; background: var(--panel);
253
+ border: 1px solid var(--line); border-radius: var(--r-sm);
254
+ padding: 4px 8px; font-size: var(--fz-sm);
255
+ color: var(--fg); outline: 0; margin-bottom: 6px;
256
+ }
257
+ .side-search::placeholder { color: var(--dim); }
258
+ .side-search:focus { border-color: var(--accent-dim); }
259
+
260
+ /* Tree folder header */
261
+ .tree-folder {
262
+ display: flex; align-items: center; gap: 6px;
263
+ padding: 3px 0; font-size: var(--fz-sm); cursor: pointer;
264
+ user-select: none;
217
265
  }
218
-
219
- .search-box input {
220
- background: var(--bg-surface);
221
- border: 1px solid var(--border);
222
- color: var(--text-primary);
223
- font-family: var(--font-mono);
224
- padding: 6px 12px 6px 28px;
225
- border-radius: var(--radius);
226
- font-size: 11px;
227
- width: 200px;
228
- outline: none;
229
- transition: all 0.25s;
266
+ .tree-folder .tf-arrow { font-size: 10px; color: var(--dim); transition: transform 0.15s; }
267
+ .tree-folder .tf-arrow.closed { transform: rotate(-90deg); }
268
+ .tree-folder .tf-icon { color: var(--muted); font-size: 11px; }
269
+ .tree-folder .tf-label { flex: 1; color: var(--fg); }
270
+ .tree-folder .tf-count { font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
271
+
272
+ /* Sidebar rows */
273
+ .sidebar-row {
274
+ display: flex; align-items: center; gap: 8px;
275
+ width: 100%; text-align: left; padding: 3px 8px 3px 6px;
276
+ border-radius: var(--r-sm); border-left: 2px solid transparent;
277
+ background: transparent; transition: background 0.1s;
278
+ }
279
+ .sidebar-row:hover { background: var(--panel); }
280
+ .sidebar-row.active {
281
+ border-left-color: var(--accent);
282
+ background: color-mix(in oklch, var(--accent) 10%, transparent);
283
+ }
284
+ .sidebar-row .sr-dot { font-size: 9px; }
285
+ .sidebar-row .sr-label { flex: 1; font-size: var(--fz-sm); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
286
+ .sidebar-row.active .sr-label { color: var(--accent); }
287
+
288
+ /* Tag chip */
289
+ .tag-chip {
290
+ display: inline-flex; align-items: center; gap: 5px;
291
+ padding: 1px 7px; border: 1px solid var(--line);
292
+ background: var(--panel); color: var(--muted);
293
+ border-radius: var(--r-sm); font-size: var(--fz-sm);
294
+ white-space: nowrap; transition: color 0.12s, border-color 0.12s;
295
+ }
296
+ .tag-chip:hover { border-color: var(--line-2); color: var(--fg); }
297
+ .tag-chip.active { border-color: var(--accent-dim); background: color-mix(in srgb, var(--accent) 12%, var(--panel)); color: var(--accent); }
298
+ .tag-chip .tc-hash { color: var(--dim); }
299
+ .tag-chip .tc-count { margin-left: 2px; font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
300
+
301
+ /* StatusBar */
302
+ #statusbar {
303
+ display: flex; align-items: center; justify-content: space-between;
304
+ padding: 0 14px; font-size: var(--fz-xs); color: var(--dim);
305
+ border-top: 1px solid var(--line); background: var(--bg);
306
+ }
307
+ .sb-left, .sb-right, .sb-center { display: flex; gap: 14px; align-items: center; }
308
+ .sb-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--accent); display: inline-block; margin-right: 5px; }
309
+
310
+ /* Content area */
311
+ #content-area {
312
+ padding: var(--pad-y) var(--pad-x);
313
+ min-height: 0; min-width: 0;
314
+ border-left: 1px solid var(--line);
315
+ overflow: hidden; display: flex; flex-direction: column;
316
+ }
317
+
318
+ /* Note view */
319
+ .note-view {
320
+ display: grid; grid-template-columns: minmax(0,1fr) 300px;
321
+ gap: var(--gap); height: 100%; min-height: 0;
322
+ }
323
+ .note-view.no-rail { grid-template-columns: minmax(0,1fr); }
324
+ .note-article { overflow-y: auto; min-width: 0; padding-right: 6px; }
325
+ .note-rail { display: flex; flex-direction: column; gap: var(--gap); overflow-y: auto; padding-bottom: 8px; }
326
+
327
+ /* Command prompt strip */
328
+ .cmd-strip {
329
+ display: flex; align-items: center; gap: 8px;
330
+ font-size: var(--fz-xs); color: var(--dim);
331
+ padding: 2px 0 10px; font-variant-numeric: tabular-nums;
332
+ }
333
+ .cmd-strip .cs-right { margin-left: auto; }
334
+
335
+ /* Note title */
336
+ .note-title {
337
+ margin: 12px 0 4px; font-size: calc(var(--fz-xl) + 8px);
338
+ letter-spacing: -0.01em; font-weight: 500; line-height: 1.15;
339
+ }
340
+ .note-title .title-caret {
341
+ display: inline-block; width: 0.5em; height: 0.85em;
342
+ background: var(--accent); margin-left: 6px;
343
+ vertical-align: -0.08em; opacity: 0.6;
344
+ animation: kypBlink 1.05s steps(1) infinite;
345
+ }
346
+ .note-meta {
347
+ font-size: var(--fz-xs); color: var(--dim);
348
+ margin-bottom: 22px; display: flex; gap: 14px;
349
+ }
350
+ .note-tags-strip {
351
+ display: flex; align-items: center; justify-content: space-between;
352
+ gap: 10px; padding: 0 0 14px; flex-wrap: wrap;
353
+ }
354
+ .note-tags-left { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
355
+ .note-tags-right { display: flex; gap: 6px; }
356
+ .note-dates { font-size: var(--fz-xs); color: var(--dim); margin-left: 8px; }
357
+
358
+ /* Markdown body */
359
+ .md-body h1 { font-size: calc(var(--fz-xl) + 4px); font-weight: 500; margin: 32px 0 14px; color: var(--fg); }
360
+ .md-body h2 {
361
+ margin: 32px 0 14px; font-size: calc(var(--fz-lg) + 4px);
362
+ font-weight: 500; color: var(--fg); letter-spacing: -0.005em;
363
+ }
364
+ .md-body h3 { margin: 26px 0 10px; font-size: var(--fz-lg); font-weight: 500; color: var(--accent); }
365
+ .md-body p { margin: 0 0 14px; line-height: 1.65; color: var(--muted); font-size: var(--fz); }
366
+ .md-body ul, .md-body ol { padding-left: 22px; margin: 0 0 14px; }
367
+ .md-body li { line-height: 1.65; color: var(--muted); font-size: var(--fz); margin: 4px 0; }
368
+ .md-body li::marker { color: var(--dim); }
369
+ .md-body a { color: var(--link); text-decoration: none; }
370
+ .md-body a:hover { text-decoration: underline; text-underline-offset: 3px; }
371
+ .md-body strong { color: var(--fg); font-weight: 500; }
372
+ .md-body code {
373
+ font-size: 0.92em; padding: 1px 6px; border-radius: var(--r-sm);
374
+ background: color-mix(in oklch, var(--accent) 14%, var(--bg));
375
+ color: var(--accent);
376
+ border: 1px solid color-mix(in oklch, var(--accent) 28%, transparent);
230
377
  }
231
-
232
- .search-box input::placeholder { color: var(--text-muted); }
233
-
234
- .search-box input:focus {
235
- border-color: rgba(217,119,87,0.3);
236
- box-shadow: 0 0 16px rgba(217,119,87,0.08);
237
- width: 260px;
378
+ .md-body pre {
379
+ background: var(--bg); border: 1px solid var(--line);
380
+ border-radius: var(--r); padding: 14px 16px;
381
+ overflow-x: auto; margin: 14px 0;
382
+ }
383
+ .md-body pre code { background: none; padding: 0; border: none; color: var(--muted); font-size: var(--fz); line-height: 1.6; }
384
+ .md-body table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: var(--fz-sm); }
385
+ .md-body th { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); color: var(--fg); font-weight: 500; }
386
+ .md-body td { padding: 8px 12px; border-bottom: 1px solid var(--line); color: var(--muted); }
387
+ .md-body tr:last-child td { border-bottom: none; }
388
+ .md-body blockquote { border-left: 2px solid var(--accent); padding: 8px 16px; margin: 14px 0; color: var(--muted); }
389
+ .md-body hr { border: none; height: 1px; background: var(--line); margin: 24px 0; }
390
+ .wikilink { color: var(--accent); cursor: pointer; border-bottom: 1px dashed var(--accent-dim); font-weight: 500; }
391
+ .wikilink:hover { border-bottom-style: solid; }
392
+
393
+ /* Rail cards */
394
+ .rail-card { margin-bottom: 0; }
395
+ .rail-card-header {
396
+ display: flex; align-items: center; justify-content: space-between;
397
+ padding: 2px 2px 8px;
398
+ }
399
+ .rail-card-title {
400
+ display: flex; align-items: center; gap: 6px;
401
+ font-size: var(--fz-xs); letter-spacing: 0.08em;
402
+ text-transform: uppercase; color: var(--dim);
403
+ }
404
+ .rail-card-body {
405
+ border: 1px solid var(--line); background: var(--panel);
406
+ border-radius: var(--r); padding: 10px 12px;
407
+ }
408
+ .rail-icon-btn {
409
+ width: 20px; height: 18px; line-height: 16px;
410
+ border-radius: var(--r-sm); border: 1px solid var(--line);
411
+ color: var(--muted); font-size: 11px;
412
+ display: inline-flex; align-items: center; justify-content: center;
413
+ }
414
+ .rail-icon-btn:hover { color: var(--fg); border-color: var(--line-2); }
415
+
416
+ /* Outline */
417
+ .outline-item {
418
+ display: block; font-size: var(--fz-sm); padding: 3px 4px;
419
+ color: var(--muted); font-weight: 400; text-decoration: none;
420
+ border-radius: var(--r-sm); cursor: pointer;
421
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
422
+ }
423
+ .outline-item:hover { background: var(--panel-2); }
424
+ .outline-item.lv1 { color: var(--fg); font-weight: 500; }
425
+
426
+ /* Backlink items */
427
+ .bl-item {
428
+ display: flex; align-items: center; gap: 8px;
429
+ padding: 5px 2px; font-size: var(--fz-sm); color: var(--muted); cursor: pointer;
430
+ }
431
+ .bl-item:hover { color: var(--fg); }
432
+
433
+ /* Session view */
434
+ .session-view { overflow-y: auto; height: 100%; min-height: 0; padding-right: 6px; }
435
+ .session-status {
436
+ display: inline-flex; align-items: center; gap: 6px;
437
+ padding: 2px 8px; border-radius: var(--r-sm); font-size: var(--fz-sm);
438
+ }
439
+ .session-status.live { border: 1px solid var(--accent-dim); background: color-mix(in oklch, var(--accent) 14%, var(--panel)); color: var(--accent); }
440
+ .session-status.done { border: 1px solid var(--line); background: var(--panel); color: var(--muted); }
441
+
442
+ /* Session sections */
443
+ .session-section {
444
+ margin-bottom: 20px; border: 1px solid var(--line);
445
+ border-radius: var(--r); background: var(--panel); overflow: hidden;
446
+ }
447
+ .ss-header {
448
+ padding: 10px 14px; font-size: var(--fz-lg); font-weight: 500;
449
+ display: flex; align-items: center; gap: 8px;
450
+ border-bottom: 1px solid var(--line); background: var(--bg-2);
451
+ }
452
+ .ss-body { padding: 12px 16px; }
453
+ .ss-body.md-body { font-size: var(--fz-sm); line-height: 1.65; }
454
+ .ss-body.md-body ul { padding-left: 18px; }
455
+ .ss-body.md-body li { margin-bottom: 4px; }
456
+ .prompts-list.scrollable { max-height: 280px; overflow-y: auto; }
457
+ .prompt-entry { border-bottom: 1px solid var(--line); padding-bottom: 10px; margin-bottom: 10px; }
458
+ .prompt-entry:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
459
+ .prompt-entry h3 { font-size: var(--fz-sm); color: var(--accent); font-weight: 500; margin-bottom: 4px; }
460
+ .prompt-entry blockquote {
461
+ border-left: 2px solid var(--accent-dim); padding-left: 12px;
462
+ color: var(--muted); font-size: var(--fz-sm); margin: 4px 0;
463
+ }
464
+
465
+ /* Graph view */
466
+ .graph-view {
467
+ display: grid; grid-template-columns: minmax(0,1fr) 300px;
468
+ gap: var(--gap); height: 100%; min-height: 0;
469
+ }
470
+ .graph-canvas {
471
+ border: 1px solid var(--line); background: var(--panel);
472
+ border-radius: var(--r); position: relative; overflow: hidden;
473
+ display: flex; flex-direction: column;
474
+ }
475
+ .graph-header {
476
+ display: flex; align-items: center; justify-content: space-between;
477
+ padding: 10px 14px; border-bottom: 1px solid var(--line);
478
+ background: var(--bg-2); gap: 12px;
479
+ }
480
+ .graph-header-left { display: flex; align-items: center; gap: 10px; font-size: var(--fz-sm); white-space: nowrap; }
481
+ .graph-header-right { display: flex; gap: 6px; flex-shrink: 0; }
482
+ .tool-chip {
483
+ padding: 3px 9px; border-radius: var(--r-sm);
484
+ border: 1px solid var(--line); color: var(--muted);
485
+ background: transparent; font-size: var(--fz-xs);
486
+ }
487
+ .tool-chip.active { border-color: var(--accent-dim); color: var(--accent); background: color-mix(in oklch, var(--accent) 10%, var(--panel)); }
488
+ .graph-svg-wrap { position: relative; flex: 1; min-height: 0; }
489
+ .graph-svg-wrap svg { position: absolute; inset: 0; width: 100%; height: 100%; }
490
+ .graph-legend {
491
+ position: absolute; left: 14px; bottom: 14px;
492
+ background: color-mix(in oklch, var(--bg) 85%, transparent);
493
+ border: 1px solid var(--line); border-radius: var(--r);
494
+ padding: 8px 10px; font-size: var(--fz-xs);
495
+ display: flex; flex-direction: column; gap: 5px;
496
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
497
+ }
498
+ .graph-legend-title { color: var(--dim); letter-spacing: 0.08em; text-transform: uppercase; }
499
+ .graph-legend-item { display: flex; align-items: center; gap: 8px; color: var(--muted); }
500
+ .graph-rail { display: flex; flex-direction: column; gap: var(--gap); overflow-y: auto; }
501
+ .conn-item {
502
+ display: flex; align-items: center; gap: 8px;
503
+ width: 100%; padding: 4px 6px; border-radius: var(--r-sm);
504
+ font-size: var(--fz-sm); color: var(--muted); text-align: left;
505
+ transition: background 0.1s;
238
506
  }
239
-
240
- .search-box .search-icon {
241
- position: absolute;
242
- left: 10px;
243
- top: 50%;
244
- transform: translateY(-50%);
245
- color: var(--text-muted);
246
- font-size: 11px;
507
+ .conn-item:hover { background: var(--panel-2); }
508
+
509
+ /* Modals */
510
+ .modal-overlay {
511
+ position: fixed; inset: 0; background: rgba(0,0,0,0.5);
512
+ z-index: 200; display: none; align-items: flex-start;
513
+ justify-content: center; padding-top: 15vh;
514
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
515
+ }
516
+ .modal-overlay.active { display: flex; }
517
+ .modal-overlay.center { align-items: center; padding-top: 0; }
518
+ .modal {
519
+ width: 480px; background: var(--panel);
520
+ border: 1px solid var(--line); border-radius: var(--r-lg);
521
+ box-shadow: 0 24px 64px rgba(0,0,0,0.6);
522
+ overflow: hidden;
247
523
  }
248
-
249
- .search-box .search-hint {
250
- position: absolute;
251
- right: 8px;
252
- top: 50%;
253
- transform: translateY(-50%);
254
- font-family: var(--font-mono);
255
- font-size: 9px;
256
- color: var(--text-muted);
257
- background: var(--bg-hover);
258
- padding: 1px 5px;
259
- border-radius: 3px;
260
- border: 1px solid var(--border);
524
+ .modal-sm { width: 420px; }
525
+ .modal-header {
526
+ padding: 16px 20px 12px; border-bottom: 1px solid var(--line);
527
+ font-size: var(--fz-sm); font-weight: 600; color: var(--muted);
261
528
  letter-spacing: 0.5px;
262
529
  }
263
-
264
- .search-results {
265
- position: absolute;
266
- top: calc(100% + 8px);
267
- right: 0;
268
- width: 400px;
269
- max-height: 380px;
270
- overflow-y: auto;
271
- background: var(--bg-card);
272
- border: 1px solid var(--border);
273
- border-radius: var(--radius-lg);
274
- z-index: 100;
275
- display: none;
276
- box-shadow: 0 12px 40px rgba(0,0,0,0.5);
530
+ .modal-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }
531
+ .modal-footer { padding: 12px 20px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; gap: 8px; }
532
+ .modal-field label {
533
+ display: block; font-size: var(--fz-xs); font-weight: 600;
534
+ text-transform: uppercase; letter-spacing: 1px; color: var(--dim); margin-bottom: 4px;
277
535
  }
278
-
279
- .search-results.active { display: block; }
280
-
281
- .search-result {
282
- padding: 10px 14px;
283
- cursor: pointer;
284
- border-bottom: 1px solid var(--border-subtle);
285
- transition: background 0.15s;
536
+ .modal-field input, .modal-field textarea {
537
+ width: 100%; background: var(--bg); border: 1px solid var(--line);
538
+ color: var(--fg); padding: 8px 12px; border-radius: var(--r); outline: none;
286
539
  }
287
-
288
- .search-result:last-child { border-bottom: none; }
289
- .search-result:hover { background: var(--bg-hover); }
290
-
291
- .search-result .sr-title {
292
- font-size: 12px;
293
- font-weight: 500;
294
- color: var(--neon-cyan);
295
- }
296
-
297
- .search-result .sr-path {
298
- font-family: var(--font-mono);
299
- font-size: 10px;
300
- color: var(--text-muted);
301
- margin-top: 2px;
302
- }
303
-
304
- .search-result .sr-snippet {
305
- font-size: 11px;
306
- color: var(--text-secondary);
307
- margin-top: 4px;
308
- line-height: 1.5;
309
- }
310
-
311
- /* ============ QUICK SWITCHER ============ */
312
- .quick-switcher-overlay {
313
- position: fixed;
314
- inset: 0;
315
- background: rgba(6,6,12,0.6);
316
- z-index: 200;
317
- display: none;
318
- align-items: flex-start;
319
- justify-content: center;
320
- padding-top: 15vh;
321
- backdrop-filter: blur(12px);
322
- -webkit-backdrop-filter: blur(12px);
323
- }
324
-
325
- .quick-switcher-overlay.active { display: flex; }
326
-
327
- .quick-switcher {
328
- width: 480px;
329
- background: rgba(17,17,22,0.85);
330
- border: 1px solid var(--border);
331
- border-radius: var(--radius-lg);
332
- box-shadow: 0 24px 64px rgba(0,0,0,0.8), 0 0 0 1px rgba(217,119,87,0.1);
333
- overflow: hidden;
334
- backdrop-filter: blur(20px);
335
- -webkit-backdrop-filter: blur(20px);
540
+ .modal-field input:focus, .modal-field textarea:focus { border-color: var(--accent-dim); }
541
+ .modal-field textarea { min-height: 60px; resize: vertical; }
542
+ .modal-btn {
543
+ padding: 6px 14px; border-radius: var(--r-sm); font-size: var(--fz-sm);
544
+ border: 1px solid var(--line); color: var(--muted); background: var(--panel);
336
545
  }
546
+ .modal-btn:hover { border-color: var(--line-2); color: var(--fg); }
547
+ .modal-btn.primary { border-color: var(--accent-dim); color: var(--accent); background: color-mix(in oklch, var(--accent) 10%, var(--panel)); }
548
+ .modal-btn.primary:hover { background: color-mix(in oklch, var(--accent) 18%, var(--panel)); }
337
549
 
338
- .quick-switcher input {
339
- width: 100%;
340
- background: transparent;
341
- border: none;
342
- border-bottom: 1px solid var(--border);
343
- color: var(--text-primary);
344
- font-family: var(--font-mono);
345
- font-size: 14px;
346
- padding: 16px 20px;
347
- outline: none;
550
+ /* Edit modal */
551
+ .edit-textarea {
552
+ width: 100%; height: 50vh; background: var(--bg);
553
+ border: 1px solid var(--line); color: var(--fg);
554
+ padding: 16px; border-radius: var(--r); outline: none;
555
+ font-size: var(--fz); line-height: 1.6; resize: none;
348
556
  }
557
+ .edit-textarea:focus { border-color: var(--accent-dim); }
349
558
 
350
- .quick-switcher input::placeholder { color: var(--text-muted); }
351
-
352
- .quick-switcher-results {
353
- max-height: 320px;
354
- overflow-y: auto;
559
+ /* Quick switcher */
560
+ .qs-input {
561
+ width: 100%; background: transparent; border: none;
562
+ border-bottom: 1px solid var(--line); color: var(--fg);
563
+ font-size: 14px; padding: 16px 20px; outline: none;
355
564
  }
356
-
565
+ .qs-input::placeholder { color: var(--dim); }
566
+ .qs-results { max-height: 320px; overflow-y: auto; }
357
567
  .qs-item {
358
- padding: 10px 20px;
359
- cursor: pointer;
360
- display: flex;
361
- align-items: center;
362
- gap: 10px;
363
- transition: background 0.1s;
364
- }
365
-
366
- .qs-item:hover, .qs-item.selected { background: var(--bg-hover); }
367
-
368
- .qs-item .qs-icon {
369
- color: var(--neon-purple);
370
- font-size: 12px;
371
- flex-shrink: 0;
372
- opacity: 0.7;
373
- }
374
-
375
- .qs-item .qs-name {
376
- font-size: 13px;
377
- color: var(--text-primary);
378
- font-weight: 500;
379
- }
568
+ padding: 10px 20px; cursor: pointer;
569
+ display: flex; align-items: center; gap: 10px; transition: background 0.1s;
570
+ }
571
+ .qs-item:hover, .qs-item.selected { background: var(--panel-2); }
572
+ .qs-item .qs-icon { color: var(--accent); font-size: 10px; opacity: 0.7; }
573
+ .qs-item .qs-name { font-size: var(--fz); color: var(--fg); font-weight: 500; }
574
+ .qs-item .qs-path { font-size: var(--fz-xs); color: var(--dim); margin-left: auto; }
575
+ .qs-item.selected .qs-name { color: var(--accent); }
576
+ .qs-empty { padding: 20px; text-align: center; color: var(--dim); font-size: var(--fz-sm); }
577
+
578
+ /* Session search results in sidebar */
579
+ .ss-results { max-height: 160px; overflow-y: auto; margin-bottom: 6px; }
580
+ .ss-result { padding: 5px 8px; border-radius: var(--r-sm); cursor: pointer; margin: 1px 0; }
581
+ .ss-result:hover { background: var(--panel); }
582
+ .ss-result .ssr-title { font-size: var(--fz-sm); color: var(--accent); font-weight: 500; }
583
+ .ss-result .ssr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
584
+ </style>
585
+ </head>
586
+ <body>
587
+ <div id="app">
588
+ <!-- TopBar -->
589
+ <header id="topbar">
590
+ <div class="topbar-left">
591
+ <button class="toggle-btn" id="sidebar-toggle" title="Toggle sidebar">
592
+ <svg width="14" height="14" viewBox="0 0 14 14"><rect x="0.5" y="1.5" width="13" height="11" rx="1.5" fill="none" stroke="currentColor"/><line x1="5" y1="1.5" x2="5" y2="12.5" stroke="currentColor"/></svg>
593
+ </button>
594
+ <span class="logo"><span class="logo-mark"></span>KYP·MEM</span>
595
+ <span class="dim" style="font-size:var(--fz-sm);letter-spacing:0.01em">know your project</span>
596
+ <span class="dim">·</span>
597
+ <span class="breadcrumb" id="breadcrumb"></span>
598
+ </div>
599
+ <div class="topbar-right">
600
+ <div class="view-switch" id="view-switch">
601
+ <button class="active" data-view="note">note</button>
602
+ <button data-view="session">session</button>
603
+ <button data-view="graph">graph</button>
604
+ </div>
605
+ <button class="ghost-btn" id="new-project-btn">+ project</button>
606
+ <div class="search-bar" id="search-bar">
607
+ <span class="s-prompt">$</span>
608
+ <input type="text" id="search-input" placeholder="search vault">
609
+ <span class="s-caret"></span>
610
+ <span class="s-hint">⌘K</span>
611
+ <div class="search-dropdown" id="search-dropdown"></div>
612
+ </div>
613
+ </div>
614
+ </header>
615
+
616
+ <!-- Main -->
617
+ <main id="main">
618
+ <aside id="sidebar">
619
+ <div class="sidebar-scroll" id="sidebar-scroll"></div>
620
+ </aside>
621
+ <div id="resize-handle"></div>
622
+ <section id="content-area"></section>
623
+ </main>
624
+
625
+ <!-- StatusBar -->
626
+ <footer id="statusbar">
627
+ <div class="sb-left">
628
+ <span><span class="mut tab-nums" id="stat-notes">0</span> notes</span>
629
+ <span><span class="mut tab-nums" id="stat-folders">0</span> folders</span>
630
+ <span>·</span>
631
+ <span>store <span class="mut">~/.kyp-mem/vault</span></span>
632
+ <span><i class="sb-dot"></i>sync ok</span>
633
+ </div>
634
+ <div class="sb-center" id="token-economics" style="display:none;">
635
+ <span title="Tokens spent exploring (file reads + commands) across all sessions">explore <span class="mut tab-nums" id="te-explore">—</span></span>
636
+ <span>·</span>
637
+ <span title="Tokens injected at session start from memory">inject <span class="mut tab-nums" id="te-inject">—</span></span>
638
+ <span>·</span>
639
+ <span title="How much re-exploration memory saves you">saving <span class="mut tab-nums" id="te-savings">—</span></span>
640
+ </div>
641
+ <div class="sb-right">
642
+ <span>idx <span class="mut">up to date</span></span>
643
+ <span>·</span>
644
+ <span class="tab-nums" id="clock"></span>
645
+ <span>·</span>
646
+ <span class="tab-nums mut">v0.4.3</span>
647
+ </div>
648
+ </footer>
649
+ </div>
380
650
 
381
- .qs-item .qs-path {
382
- font-family: var(--font-mono);
383
- font-size: 10px;
384
- color: var(--text-muted);
385
- margin-left: auto;
386
- }
651
+ <!-- Quick Switcher -->
652
+ <div class="modal-overlay" id="qs-overlay">
653
+ <div class="modal">
654
+ <input type="text" class="qs-input" id="qs-input" placeholder="Jump to note...">
655
+ <div class="qs-results" id="qs-results"></div>
656
+ </div>
657
+ </div>
387
658
 
388
- .qs-item.selected .qs-name { color: var(--neon-cyan); }
659
+ <!-- Edit Modal -->
660
+ <div class="modal-overlay center" id="edit-overlay">
661
+ <div class="modal" style="width:640px">
662
+ <div class="modal-header" id="edit-path"></div>
663
+ <div style="padding:16px 20px">
664
+ <textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown..."></textarea>
665
+ </div>
666
+ <div class="modal-footer">
667
+ <button class="modal-btn" id="edit-cancel">ESC</button>
668
+ <button class="modal-btn primary" id="edit-save">SAVE</button>
669
+ </div>
670
+ </div>
671
+ </div>
389
672
 
390
- .qs-empty {
391
- padding: 20px;
392
- text-align: center;
393
- color: var(--text-muted);
394
- font-size: 12px;
395
- }
673
+ <!-- Session Create Modal -->
674
+ <div class="modal-overlay center" id="session-create-overlay">
675
+ <div class="modal modal-sm">
676
+ <div class="modal-header">NEW SESSION</div>
677
+ <div class="modal-body">
678
+ <div class="modal-field"><label>Project</label><input type="text" id="sc-project" list="sc-project-list" placeholder="Project name..."><datalist id="sc-project-list"></datalist></div>
679
+ <div class="modal-field"><label>Summary</label><textarea id="sc-summary" placeholder="What are you working on?"></textarea></div>
680
+ </div>
681
+ <div class="modal-footer">
682
+ <button class="modal-btn" id="sc-cancel">CANCEL</button>
683
+ <button class="modal-btn primary" id="sc-create">CREATE</button>
684
+ </div>
685
+ </div>
686
+ </div>
396
687
 
397
- /* ============ SIDEBAR ============ */
398
- .sidebar {
399
- background: var(--bg-secondary);
400
- display: flex;
401
- flex-direction: column;
402
- overflow: hidden;
403
- }
688
+ <!-- Project Create Modal -->
689
+ <div class="modal-overlay center" id="project-create-overlay">
690
+ <div class="modal modal-sm">
691
+ <div class="modal-header">NEW PROJECT</div>
692
+ <div class="modal-body">
693
+ <div class="modal-field"><label>Project Name</label><input type="text" id="pc-name" placeholder="My Project..."></div>
694
+ <div class="modal-field"><label>Overview (optional)</label><textarea id="pc-overview" placeholder="Brief description..."></textarea></div>
695
+ </div>
696
+ <div class="modal-footer">
697
+ <button class="modal-btn" id="pc-cancel">CANCEL</button>
698
+ <button class="modal-btn primary" id="pc-create">CREATE</button>
699
+ </div>
700
+ </div>
701
+ </div>
404
702
 
405
- .sidebar-scroll {
406
- flex: 1;
407
- overflow-y: auto;
408
- padding: 6px 0;
409
- }
410
-
411
- .sidebar-section { padding: 0 8px; }
412
-
413
- .section-label {
414
- font-family: var(--font-mono);
415
- font-size: 9px;
416
- font-weight: 600;
417
- text-transform: uppercase;
418
- letter-spacing: 1.5px;
419
- color: var(--text-muted);
420
- padding: 12px 10px 6px;
421
- display: flex;
422
- align-items: center;
423
- justify-content: space-between;
424
- }
703
+ <script>
704
+ // ─── State ───────────────────────────────────────────────────────────────────
705
+ let currentPath = null;
706
+ let currentNote = null;
707
+ let treeData = null;
708
+ let allNotes = {};
709
+ let activeTagFilters = new Set();
710
+ let view = 'note';
711
+ let activeSession = null;
712
+ let sidebarOpen = localStorage.getItem('kyp-sidebar-open') !== 'false';
713
+ let qsSelectedIndex = 0;
714
+ let editingPath = null;
715
+ let editingNote = null;
716
+ let lastKnownStats = null;
717
+ let graphHeight = parseInt(localStorage.getItem('kyp-graph-h')) || 240;
718
+ let _graphData = null;
719
+ let searchTimeout, sessionSearchTimeout;
425
720
 
426
- .tree-item {
427
- display: flex;
428
- align-items: center;
429
- padding: 4px 10px;
430
- border-radius: var(--radius-sm);
431
- cursor: pointer;
432
- font-size: 12px;
433
- gap: 6px;
434
- user-select: none;
435
- transition: background 0.1s;
436
- position: relative;
437
- margin: 1px 0;
438
- }
721
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
722
+ async function fetchJSON(url) { const r = await fetch(url); return r.json(); }
439
723
 
440
- .tree-item:hover { background: var(--bg-hover); }
724
+ function $(id) { return document.getElementById(id); }
441
725
 
442
- .tree-item.active {
443
- background: rgba(217,119,87,0.05);
726
+ function formatSessionTime(filename) {
727
+ const stem = filename.replace('.md', '');
728
+ const m = stem.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})(\d{2})(\d{2})$/);
729
+ if (!m) return stem;
730
+ const [, , mo, d, h, mi] = m;
731
+ const hr = parseInt(h), ampm = hr >= 12 ? 'PM' : 'AM';
732
+ const hr12 = hr % 12 || 12;
733
+ const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
734
+ return `${months[parseInt(mo)-1]} ${parseInt(d)}, ${hr12}:${mi} ${ampm}`;
444
735
  }
445
736
 
446
- .tree-item.active::before {
447
- content: '';
448
- position: absolute;
449
- left: 0;
450
- top: 4px;
451
- bottom: 4px;
452
- width: 2px;
453
- background: var(--neon-cyan);
454
- border-radius: 1px;
737
+ function findNotePath(name) {
738
+ const lower = name.toLowerCase();
739
+ for (const [path, note] of Object.entries(allNotes)) {
740
+ const stem = path.split('/').pop().replace('.md', '').toLowerCase();
741
+ if (stem === lower || note.title.toLowerCase() === lower) return path;
742
+ }
743
+ return null;
455
744
  }
456
745
 
457
- .tree-item .arrow {
458
- width: 12px;
459
- font-size: 8px;
460
- color: var(--text-muted);
461
- text-align: center;
462
- flex-shrink: 0;
463
- transition: transform 0.15s;
746
+ function collectAllTags() {
747
+ const tags = {};
748
+ for (const note of Object.values(allNotes)) {
749
+ (note.tags || []).forEach(t => { tags[t] = (tags[t] || 0) + 1; });
750
+ }
751
+ return tags;
464
752
  }
465
753
 
466
- .tree-item .arrow.open { transform: rotate(90deg); }
467
-
468
- .tree-item .icon {
469
- flex-shrink: 0;
470
- font-size: 11px;
471
- opacity: 0.6;
754
+ function isSessionPath(path) {
755
+ return path.includes('/Sessions/') || path.startsWith('Sessions/');
472
756
  }
473
757
 
474
- .tree-item .folder-icon { color: var(--neon-yellow); }
475
- .tree-item .note-icon { color: var(--neon-purple); }
476
-
477
- .tree-item .name {
478
- overflow: hidden;
479
- text-overflow: ellipsis;
480
- white-space: nowrap;
481
- font-size: 12px;
482
- color: var(--text-primary);
758
+ // ─── Clock ───────────────────────────────────────────────────────────────────
759
+ function updateClock() {
760
+ const d = new Date();
761
+ const h = d.getHours() % 12 || 12;
762
+ const m = String(d.getMinutes()).padStart(2, '0');
763
+ const ampm = d.getHours() >= 12 ? 'PM' : 'AM';
764
+ $('clock').textContent = `${h}:${m} ${ampm}`;
483
765
  }
766
+ updateClock();
767
+ setInterval(updateClock, 30000);
484
768
 
485
- .tree-item.active .name { color: var(--neon-cyan); }
486
-
487
- .tree-children { padding-left: 12px; overflow: hidden; }
488
- .tree-children.collapsed { display: none; }
489
-
490
- /* ============ TAG FILTER ============ */
491
- .tag-filter-section { padding: 0 8px 8px; }
492
-
493
- .tag-filter-header {
494
- font-family: var(--font-mono);
495
- font-size: 9px;
496
- font-weight: 600;
497
- text-transform: uppercase;
498
- letter-spacing: 1.5px;
499
- color: var(--text-muted);
500
- padding: 12px 10px 6px;
501
- display: flex;
502
- align-items: center;
503
- justify-content: space-between;
504
- cursor: pointer;
505
- user-select: none;
769
+ // ─── Breadcrumb ──────────────────────────────────────────────────────────────
770
+ function updateBreadcrumb(path) {
771
+ const bc = $('breadcrumb');
772
+ if (!path) { bc.innerHTML = ''; return; }
773
+ const parts = path.replace('.md', '').split('/').filter(Boolean);
774
+ bc.innerHTML = parts.map((p, i) =>
775
+ i < parts.length - 1
776
+ ? `<span>${p}</span><span class="bc-sep">/</span>`
777
+ : `<span class="bc-last">${p}</span>`
778
+ ).join('');
506
779
  }
507
780
 
508
- .tag-filter-header .arrow {
509
- font-size: 8px;
510
- transition: transform 0.15s;
781
+ // ─── View Switching ──────────────────────────────────────────────────────────
782
+ function setView(v) {
783
+ view = v;
784
+ $('view-switch').querySelectorAll('button').forEach(b => {
785
+ b.classList.toggle('active', b.dataset.view === v);
786
+ });
511
787
  }
512
788
 
513
- .tag-filter-header .arrow.open { transform: rotate(90deg); }
514
-
515
- .tag-filter-body { padding: 0 4px; }
516
- .tag-filter-body.collapsed { display: none; }
789
+ $('view-switch').addEventListener('click', e => {
790
+ if (e.target.dataset.view) {
791
+ setView(e.target.dataset.view);
792
+ if (view === 'graph') { _graphData = null; renderGraphView(); }
793
+ else if (view === 'session' && activeSession) loadNote(activeSession);
794
+ else if (view === 'note' && currentPath) loadNote(currentPath);
795
+ }
796
+ });
517
797
 
518
- .tag-filter-active {
519
- display: flex;
520
- flex-wrap: wrap;
521
- gap: 4px;
522
- margin-bottom: 6px;
798
+ // ─── Sidebar Toggle ──────────────────────────────────────────────────────────
799
+ function applySidebar() {
800
+ $('main').classList.toggle('sidebar-hidden', !sidebarOpen);
523
801
  }
802
+ $('sidebar-toggle').addEventListener('click', () => {
803
+ sidebarOpen = !sidebarOpen;
804
+ localStorage.setItem('kyp-sidebar-open', sidebarOpen);
805
+ applySidebar();
806
+ });
807
+ applySidebar();
524
808
 
525
- .tag-filter-active:empty { display: none; }
526
-
527
- .active-tag {
528
- font-family: var(--font-mono);
529
- font-size: 9px;
530
- padding: 2px 7px;
531
- border-radius: var(--radius-sm);
532
- background: rgba(217,119,87,0.08);
533
- color: var(--neon-cyan);
534
- border: 1px solid rgba(217,119,87,0.2);
535
- cursor: pointer;
536
- display: flex;
537
- align-items: center;
538
- gap: 3px;
539
- transition: all 0.15s;
540
- }
809
+ // Rail is always open
810
+ let railOpen = true;
541
811
 
542
- .active-tag:hover { background: rgba(217,119,87,0.15); }
543
- .active-tag .remove { font-size: 11px; opacity: 0.5; }
544
- .active-tag .remove:hover { opacity: 1; }
812
+ // ─── Resize Handle ───────────────────────────────────────────────────────────
813
+ (function initResize() {
814
+ const handle = $('resize-handle');
815
+ const main = $('main');
816
+ let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 236;
817
+ main.style.setProperty('--sidebar-w', sidebarW + 'px');
545
818
 
546
- .tag-cloud {
547
- display: flex;
548
- flex-wrap: wrap;
549
- gap: 3px;
550
- max-height: 140px;
551
- overflow-y: auto;
552
- padding: 2px 0;
553
- }
819
+ handle.addEventListener('mousedown', e => {
820
+ e.preventDefault();
821
+ const startX = e.clientX, startW = sidebarW;
822
+ handle.classList.add('dragging');
823
+ document.body.classList.add('resizing');
554
824
 
555
- .tag-chip {
556
- font-family: var(--font-mono);
557
- font-size: 9px;
558
- font-weight: 500;
559
- padding: 2px 7px;
560
- border-radius: var(--radius-sm);
561
- background: rgba(168,139,250,0.06);
562
- color: var(--neon-purple);
563
- border: 1px solid rgba(168,139,250,0.1);
564
- cursor: pointer;
565
- transition: all 0.15s;
566
- user-select: none;
567
- }
825
+ function onMove(ev) {
826
+ sidebarW = Math.max(180, Math.min(400, startW + (ev.clientX - startX)));
827
+ main.style.setProperty('--sidebar-w', sidebarW + 'px');
828
+ }
829
+ function onUp() {
830
+ handle.classList.remove('dragging');
831
+ document.body.classList.remove('resizing');
832
+ localStorage.setItem('kyp-sidebar-w', sidebarW);
833
+ document.removeEventListener('mousemove', onMove);
834
+ document.removeEventListener('mouseup', onUp);
835
+ }
836
+ document.addEventListener('mousemove', onMove);
837
+ document.addEventListener('mouseup', onUp);
838
+ });
839
+ })();
568
840
 
569
- .tag-chip:hover {
570
- background: rgba(168,139,250,0.12);
571
- border-color: rgba(168,139,250,0.25);
572
- }
841
+ // ─── Sidebar Rendering ───────────────────────────────────────────────────────
842
+ function renderSidebar(sessionsData) {
843
+ const container = $('sidebar-scroll');
844
+ container.innerHTML = '';
573
845
 
574
- .tag-chip.selected {
575
- background: rgba(217,119,87,0.08);
576
- color: var(--neon-cyan);
577
- border-color: rgba(217,119,87,0.2);
578
- }
846
+ // Sessions section
847
+ const sessSection = document.createElement('section');
848
+ sessSection.innerHTML = `
849
+ <div class="side-section-header">
850
+ <span class="side-section-title"><span class="side-dot" style="background:var(--accent)"></span>sessions</span>
851
+ <button class="side-add-btn" id="sidebar-new-session">+ new</button>
852
+ </div>
853
+ `;
854
+ const sessSearch = document.createElement('input');
855
+ sessSearch.className = 'side-search';
856
+ sessSearch.placeholder = '⌕ semantic search session…';
857
+ sessSection.appendChild(sessSearch);
858
+
859
+ const ssResults = document.createElement('div');
860
+ ssResults.className = 'ss-results';
861
+ sessSection.appendChild(ssResults);
862
+
863
+ // Session search
864
+ sessSearch.addEventListener('input', () => {
865
+ clearTimeout(sessionSearchTimeout);
866
+ const q = sessSearch.value.trim();
867
+ if (!q) { ssResults.innerHTML = ''; return; }
868
+ sessionSearchTimeout = setTimeout(async () => {
869
+ const results = await fetchJSON(`/api/sessions/search?q=${encodeURIComponent(q)}`);
870
+ ssResults.innerHTML = '';
871
+ results.forEach(r => {
872
+ const div = document.createElement('div');
873
+ div.className = 'ss-result';
874
+ div.innerHTML = `<div class="ssr-title">${r.title}</div><div class="ssr-snippet">${(r.snippet || '').substring(0, 100)}</div>`;
875
+ div.addEventListener('click', () => { openSession(r.path); sessSearch.value = ''; ssResults.innerHTML = ''; });
876
+ ssResults.appendChild(div);
877
+ });
878
+ }, 250);
879
+ });
579
880
 
580
- .tag-chip .tag-count {
581
- font-size: 8px;
582
- opacity: 0.4;
583
- margin-left: 2px;
584
- }
881
+ // Session groups
882
+ const sortedProjects = Object.keys(sessionsData).sort();
883
+ sortedProjects.forEach(project => {
884
+ const sessions = sessionsData[project];
885
+ const group = document.createElement('div');
886
+ const folder = document.createElement('div');
887
+ folder.className = 'tree-folder';
888
+ folder.innerHTML = `<span class="tf-arrow">▾</span><span class="tf-icon">≡</span><span class="tf-label">${project}</span><span class="tf-count">${sessions.length}</span>`;
585
889
 
586
- .filter-info {
587
- font-family: var(--font-mono);
588
- font-size: 9px;
589
- color: var(--text-muted);
590
- padding: 2px 10px 4px;
591
- display: flex;
592
- align-items: center;
593
- justify-content: space-between;
594
- }
890
+ const list = document.createElement('div');
891
+ list.style.cssText = 'display:flex;flex-direction:column;gap:1px;padding-left:16px;margin-top:2px;';
595
892
 
596
- .filter-info .clear-btn {
597
- color: var(--neon-cyan);
598
- cursor: pointer;
599
- font-size: 9px;
600
- }
893
+ sessions.sort((a, b) => b.path.localeCompare(a.path));
894
+ sessions.forEach(s => {
895
+ const row = document.createElement('button');
896
+ row.className = 'sidebar-row';
897
+ row.dataset.path = s.path;
898
+ const displayTime = formatSessionTime(s.path.split('/').pop());
899
+ row.innerHTML = `<span class="sr-dot" style="color:var(--dim)">●</span><span class="sr-label">${displayTime}</span>`;
900
+ row.addEventListener('click', () => openSession(s.path));
901
+ list.appendChild(row);
902
+ });
601
903
 
602
- .filter-info .clear-btn:hover { text-decoration: underline; }
904
+ folder.addEventListener('click', () => {
905
+ const arrow = folder.querySelector('.tf-arrow');
906
+ const isOpen = arrow.textContent === '▾';
907
+ arrow.textContent = isOpen ? '▸' : '▾';
908
+ arrow.classList.toggle('closed', isOpen);
909
+ list.style.display = isOpen ? 'none' : 'flex';
910
+ });
603
911
 
604
- .stats-bar {
605
- padding: 8px 14px;
606
- border-top: 1px solid var(--border-subtle);
607
- font-family: var(--font-mono);
608
- font-size: 9px;
609
- color: var(--text-muted);
610
- display: flex;
611
- gap: 12px;
612
- flex-wrap: wrap;
613
- }
912
+ group.appendChild(folder);
913
+ group.appendChild(list);
914
+ sessSection.appendChild(group);
915
+ });
916
+ container.appendChild(sessSection);
917
+
918
+ // Projects section
919
+ const projSection = document.createElement('section');
920
+ projSection.innerHTML = `
921
+ <div class="side-section-header">
922
+ <span class="side-section-title"><span class="side-dot" style="background:var(--accent)"></span>projects</span>
923
+ <button class="side-add-btn" id="sidebar-new-project">+ new</button>
924
+ </div>
925
+ `;
926
+ renderProjectTree(projSection);
927
+ container.appendChild(projSection);
928
+
929
+ // Tags section
930
+ const tagSection = document.createElement('section');
931
+ let tagsOpen = true;
932
+ tagSection.innerHTML = `
933
+ <div class="side-section-header">
934
+ <button class="side-section-title" id="tags-toggle"><span class="side-dot" style="background:var(--muted);opacity:0.4"></span>tags<span class="side-collapse-arrow">▾</span></button>
935
+ </div>
936
+ `;
937
+ const tagBody = document.createElement('div');
938
+ tagBody.style.cssText = 'display:flex;flex-wrap:wrap;gap:5px;';
939
+ renderTagCloud(tagBody);
940
+ tagSection.appendChild(tagBody);
941
+ container.appendChild(tagSection);
942
+
943
+ // Wire tag toggle
944
+ tagSection.querySelector('#tags-toggle').addEventListener('click', () => {
945
+ tagsOpen = !tagsOpen;
946
+ tagBody.style.display = tagsOpen ? 'flex' : 'none';
947
+ tagSection.querySelector('.side-collapse-arrow').classList.toggle('collapsed', !tagsOpen);
948
+ });
614
949
 
615
- .stat-val { color: var(--neon-green); font-weight: 500; }
950
+ // Wire add buttons
951
+ container.querySelector('#sidebar-new-session')?.addEventListener('click', () => openSessionCreate(''));
952
+ container.querySelector('#sidebar-new-project')?.addEventListener('click', () => openProjectCreate());
953
+ }
954
+
955
+ function renderProjectTree(container) {
956
+ if (!treeData) return;
957
+ function walk(node, parent) {
958
+ if (node.type === 'folder' && node.name !== 'vault') {
959
+ if (node.name === 'Sessions') return;
960
+ const group = document.createElement('div');
961
+ const folder = document.createElement('div');
962
+ folder.className = 'tree-folder';
963
+ folder.innerHTML = `<span class="tf-arrow">▾</span><span class="tf-icon">≡</span><span class="tf-label">${node.name}</span>`;
964
+ const children = document.createElement('div');
965
+ children.style.cssText = 'display:flex;flex-direction:column;gap:1px;padding-left:16px;margin-top:2px;';
966
+
967
+ folder.addEventListener('click', () => {
968
+ const arrow = folder.querySelector('.tf-arrow');
969
+ const isOpen = arrow.textContent === '▾';
970
+ arrow.textContent = isOpen ? '▸' : '▾';
971
+ children.style.display = isOpen ? 'none' : 'flex';
972
+ });
616
973
 
617
- /* ============ CONTENT ============ */
618
- .content {
619
- overflow-y: auto;
620
- padding: 40px 56px;
621
- background: var(--bg-primary);
974
+ group.appendChild(folder);
975
+ group.appendChild(children);
976
+ parent.appendChild(group);
977
+ (node.children || []).forEach(c => walk(c, children));
978
+ } else if (node.type === 'note') {
979
+ const row = document.createElement('button');
980
+ row.className = 'sidebar-row';
981
+ row.dataset.path = node.path;
982
+ const name = node.name.replace('.md', '');
983
+ row.innerHTML = `<span style="color:var(--muted);font-size:10px">◇</span><span class="sr-label">${name}</span>`;
984
+ row.addEventListener('click', () => { setView('note'); loadNote(node.path); });
985
+ parent.appendChild(row);
986
+ } else if (node.children) {
987
+ node.children.forEach(c => walk(c, parent));
988
+ }
989
+ }
990
+ walk(treeData, container);
622
991
  }
623
992
 
624
- .note-properties {
625
- display: flex;
626
- flex-wrap: wrap;
627
- gap: 5px;
628
- margin-bottom: 28px;
629
- padding-bottom: 20px;
630
- border-bottom: 1px solid var(--border-subtle);
993
+ function renderTagCloud(container) {
994
+ container.innerHTML = '';
995
+ const tags = collectAllTags();
996
+ const sorted = Object.entries(tags).sort((a, b) => b[1] - a[1]);
997
+ sorted.forEach(([tag, count]) => {
998
+ const chip = document.createElement('button');
999
+ chip.className = 'tag-chip' + (activeTagFilters.has(tag) ? ' active' : '');
1000
+ chip.innerHTML = `<span class="tc-hash">#</span>${tag}<span class="tc-count">${count}</span>`;
1001
+ chip.addEventListener('click', () => {
1002
+ if (activeTagFilters.has(tag)) activeTagFilters.delete(tag);
1003
+ else activeTagFilters.add(tag);
1004
+ applyTagFilter();
1005
+ });
1006
+ container.appendChild(chip);
1007
+ });
631
1008
  }
632
1009
 
633
- .tag {
634
- font-family: var(--font-mono);
635
- font-size: 10px;
636
- font-weight: 500;
637
- padding: 2px 9px;
638
- border-radius: var(--radius-sm);
639
- background: rgba(168,139,250,0.08);
640
- color: var(--neon-purple);
641
- border: 1px solid rgba(168,139,250,0.12);
642
- transition: all 0.15s;
1010
+ function applyTagFilter() {
1011
+ const rows = document.querySelectorAll('.sidebar-row[data-path]');
1012
+ if (activeTagFilters.size === 0) {
1013
+ rows.forEach(el => { el.style.display = ''; });
1014
+ } else {
1015
+ rows.forEach(el => {
1016
+ const path = el.dataset.path;
1017
+ if (isSessionPath(path)) { el.style.display = ''; return; }
1018
+ const note = allNotes[path];
1019
+ if (!note) { el.style.display = 'none'; return; }
1020
+ const noteTags = (note.tags || []).map(t => t.toLowerCase());
1021
+ const matches = [...activeTagFilters].every(f => noteTags.includes(f.toLowerCase()));
1022
+ el.style.display = matches ? '' : 'none';
1023
+ });
1024
+ }
1025
+ // Re-render tag chips
1026
+ const tagBody = document.querySelector('.sidebar-scroll section:last-child > div[style*="flex-wrap"]');
1027
+ if (tagBody) renderTagCloud(tagBody);
643
1028
  }
644
1029
 
645
- .tag.clickable-tag:hover {
646
- background: rgba(217,119,87,0.08);
647
- color: var(--neon-cyan);
648
- border-color: rgba(217,119,87,0.2);
649
- cursor: pointer;
1030
+ function markActiveItems() {
1031
+ document.querySelectorAll('.sidebar-row[data-path]').forEach(el => {
1032
+ el.classList.toggle('active', el.dataset.path === currentPath);
1033
+ });
650
1034
  }
651
1035
 
652
- .prop-item {
653
- font-family: var(--font-mono);
654
- font-size: 10px;
655
- padding: 2px 9px;
656
- border-radius: var(--radius-sm);
657
- background: var(--bg-card);
658
- color: var(--text-secondary);
659
- border: 1px solid var(--border-subtle);
1036
+ // ─── Open Session ────────────────────────────────────────────────────────────
1037
+ function openSession(path) {
1038
+ activeSession = path;
1039
+ view = 'session';
1040
+ setView('session');
1041
+ loadNote(path);
660
1042
  }
661
1043
 
662
- .prop-item .prop-key { color: var(--text-muted); }
1044
+ // ─── Load Note ───────────────────────────────────────────────────────────────
1045
+ async function loadNote(path) {
1046
+ currentPath = path;
1047
+ const note = await fetchJSON(`/api/note/${path}`);
1048
+ if (note.error) return;
1049
+ currentNote = note;
1050
+ markActiveItems();
1051
+ updateBreadcrumb(path);
663
1052
 
664
- /* ============ MARKDOWN ============ */
665
- .md-body h1 {
666
- font-size: 28px;
667
- font-weight: 700;
668
- margin: 0 0 28px;
669
- color: var(--text-primary);
670
- letter-spacing: -0.5px;
671
- text-shadow: 0 0 15px rgba(255,255,255,0.05);
1053
+ if (view === 'session') {
1054
+ activeSession = path;
1055
+ renderSessionView(note);
1056
+ } else {
1057
+ renderNoteView(note);
1058
+ }
672
1059
  }
673
1060
 
674
- .md-body h2 {
675
- font-size: 20px;
676
- font-weight: 600;
677
- margin: 40px 0 16px;
678
- color: var(--text-primary);
679
- padding-bottom: 10px;
680
- border-bottom: 1px solid var(--border);
681
- }
1061
+ // ─── Note View ───────────────────────────────────────────────────────────────
1062
+ function renderNoteView(note) {
1063
+ const area = $('content-area');
1064
+ const wordCount = (note.content || '').split(/\s+/).filter(Boolean).length;
1065
+ const readTime = Math.ceil(wordCount / 200) + ' min';
682
1066
 
683
- .md-body h3 {
684
- font-size: 16px;
685
- font-weight: 600;
686
- margin: 32px 0 12px;
687
- color: var(--neon-purple);
688
- text-shadow: 0 0 8px rgba(168,139,250,0.2);
689
- }
1067
+ let md = note.content || '';
1068
+ md = md.replace(/\[\[([^\]]+)\]\]/g, (_, link) => {
1069
+ const display = link.includes('#') ? link.split('#').pop() : link;
1070
+ return `<span class="wikilink" data-link="${link}">${display}</span>`;
1071
+ });
690
1072
 
691
- .md-body p {
692
- line-height: 1.9;
693
- margin: 12px 0;
694
- color: var(--text-secondary);
695
- font-size: 14px;
696
- }
1073
+ const tagsHtml = (note.tags || []).map(t =>
1074
+ `<button class="tag-chip" data-tag="${t}"><span class="tc-hash">#</span>${t}</button>`
1075
+ ).join('');
697
1076
 
698
- .md-body ul, .md-body ol { padding-left: 24px; margin: 12px 0; }
1077
+ const railClass = railOpen ? '' : ' no-rail';
1078
+ area.innerHTML = `
1079
+ <div class="note-view${railClass}">
1080
+ <article class="note-article">
1081
+ <div class="cmd-strip">
1082
+ <span class="acc">$</span>
1083
+ <span class="mut">kyp_read</span>
1084
+ <span>${note.path.replace('.md','')}</span>
1085
+ <span class="cs-right dim">↳ ok · ${wordCount.toLocaleString()} words · ${readTime}</span>
1086
+ </div>
1087
+ <div class="note-tags-strip">
1088
+ <div class="note-tags-left">
1089
+ ${tagsHtml}
1090
+ <span class="note-dates">created <span class="mut tab-nums">${note.created || ''}</span> · edited <span class="mut">${note.updated || ''}</span></span>
1091
+ </div>
1092
+ <div class="note-tags-right">
1093
+ <button class="ghost-btn" id="note-edit-btn">⌥ edit</button>
1094
+ </div>
1095
+ </div>
1096
+ <h1 class="note-title">${note.title}<span class="title-caret"></span></h1>
1097
+ <div class="note-meta">
1098
+ <span><span class="mut tab-nums">${wordCount.toLocaleString()}</span> words</span>
1099
+ <span>·</span>
1100
+ <span><span class="mut">${readTime}</span> read</span>
1101
+ </div>
1102
+ <div class="md-body">${marked.parse(md)}</div>
1103
+ <div style="height:80px"></div>
1104
+ </article>
1105
+ ${railOpen ? `<aside class="note-rail" id="note-rail"></aside>` : ''}
1106
+ </div>
1107
+ `;
699
1108
 
700
- .md-body li {
701
- line-height: 1.9;
702
- color: var(--text-secondary);
703
- margin: 4px 0;
704
- font-size: 14px;
705
- }
706
-
707
- .md-body li::marker { color: var(--text-muted); }
708
-
709
- .md-body a { color: var(--neon-blue); text-decoration: none; transition: color 0.2s; }
710
- .md-body a:hover { color: var(--neon-cyan); text-decoration: underline; }
711
-
712
- .md-body strong { color: var(--text-primary); font-weight: 600; }
713
-
714
- .md-body code {
715
- background: var(--bg-hover);
716
- padding: 3px 6px;
717
- border-radius: 4px;
718
- font-family: var(--font-mono);
719
- font-size: 12.5px;
720
- color: var(--neon-orange);
721
- border: 1px solid var(--border);
722
- }
723
-
724
- .md-body pre {
725
- background: rgba(8,8,10,0.4);
726
- border: 1px solid var(--border);
727
- border-radius: var(--radius-lg);
728
- padding: 20px;
729
- overflow-x: auto;
730
- margin: 20px 0;
731
- box-shadow: inset 0 2px 10px rgba(0,0,0,0.2);
732
- }
733
-
734
- .md-body pre code {
735
- background: none;
736
- padding: 0;
737
- border: none;
738
- color: #c9c7c2;
739
- font-size: 12.5px;
740
- line-height: 1.8;
741
- }
742
-
743
- .md-body table {
744
- width: 100%;
745
- border-collapse: separate;
746
- border-spacing: 0;
747
- margin: 24px 0;
748
- font-size: 13px;
749
- border: 1px solid var(--border);
750
- border-radius: var(--radius);
751
- overflow: hidden;
752
- }
753
-
754
- .md-body th {
755
- text-align: left;
756
- padding: 12px 16px;
757
- border-bottom: 1px solid var(--border);
758
- background: rgba(255,255,255,0.02);
759
- color: var(--text-primary);
760
- font-family: var(--font-mono);
761
- font-weight: 600;
762
- font-size: 11px;
763
- text-transform: uppercase;
764
- letter-spacing: 0.5px;
765
- }
766
-
767
- .md-body td {
768
- padding: 12px 16px;
769
- border-bottom: 1px solid var(--border);
770
- color: var(--text-secondary);
771
- transition: background 0.15s;
772
- }
773
-
774
- .md-body tr:last-child td { border-bottom: none; }
775
- .md-body tr:hover td { background: var(--bg-hover); color: var(--text-primary); }
776
-
777
- .md-body blockquote {
778
- border-left: 3px solid var(--neon-purple);
779
- padding: 12px 20px;
780
- margin: 20px 0;
781
- color: var(--text-secondary);
782
- background: linear-gradient(90deg, rgba(168,139,250,0.05), transparent);
783
- border-radius: 0 var(--radius) var(--radius) 0;
784
- font-style: italic;
785
- }
786
-
787
- .md-body hr {
788
- border: none;
789
- height: 1px;
790
- background: linear-gradient(90deg, transparent, var(--border), transparent);
791
- margin: 36px 0;
792
- }
793
-
794
- .wikilink {
795
- color: var(--neon-purple);
796
- cursor: pointer;
797
- text-decoration: none;
798
- border-bottom: 1px dashed rgba(168,139,250,0.3);
799
- transition: all 0.15s;
800
- font-weight: 500;
801
- }
802
-
803
- .wikilink:hover {
804
- color: var(--neon-cyan);
805
- border-bottom-color: rgba(217,119,87,0.4);
806
- }
807
-
808
- /* ============ EMPTY STATE ============ */
809
- .empty-state {
810
- display: flex;
811
- flex-direction: column;
812
- align-items: center;
813
- justify-content: center;
814
- height: 100%;
815
- gap: 12px;
816
- }
817
-
818
- .empty-state .es-logo {
819
- font-family: var(--font-mono);
820
- font-size: 28px;
821
- font-weight: 700;
822
- color: var(--neon-cyan);
823
- letter-spacing: 4px;
824
- opacity: 0.6;
825
- }
826
-
827
- .empty-state .es-tagline {
828
- font-family: var(--font-mono);
829
- font-size: 11px;
830
- color: var(--text-muted);
831
- letter-spacing: 3px;
832
- text-transform: uppercase;
833
- }
834
-
835
- .empty-state .es-hint {
836
- font-size: 12px;
837
- color: var(--text-muted);
838
- margin-top: 24px;
839
- }
840
-
841
- .empty-state .es-hint kbd {
842
- font-family: var(--font-mono);
843
- font-size: 10px;
844
- background: var(--bg-card);
845
- border: 1px solid var(--border);
846
- padding: 2px 6px;
847
- border-radius: 3px;
848
- color: var(--text-secondary);
849
- }
850
-
851
- /* ============ RIGHT PANEL ============ */
852
- .right-panel {
853
- background: var(--bg-secondary);
854
- overflow-y: auto;
855
- padding: 12px;
856
- }
857
-
858
- .rp-section { margin-bottom: 20px; }
859
-
860
- .rp-section-title {
861
- font-family: var(--font-mono);
862
- font-size: 10px;
863
- font-weight: 600;
864
- text-transform: uppercase;
865
- letter-spacing: 1.5px;
866
- color: var(--text-secondary);
867
- margin-bottom: 8px;
868
- display: flex;
869
- align-items: center;
870
- gap: 6px;
871
- cursor: pointer;
872
- padding: 4px 6px;
873
- border-radius: var(--radius-sm);
874
- transition: background 0.15s;
875
- }
876
-
877
- .rp-section-title:hover {
878
- background: var(--bg-hover);
879
- color: var(--text-primary);
880
- }
881
-
882
- .rp-section-title .arrow {
883
- margin-left: auto;
884
- font-size: 8px;
885
- color: var(--text-muted);
886
- transition: transform 0.2s;
887
- transform: rotate(90deg);
888
- }
889
-
890
- .rp-section.collapsed .rp-section-title .arrow {
891
- transform: rotate(0deg);
892
- }
893
-
894
- .rp-section.collapsed .rp-section-body {
895
- display: none;
896
- }
897
-
898
- .rp-section-title .rp-count {
899
- background: var(--bg-hover);
900
- color: var(--text-muted);
901
- font-size: 9px;
902
- padding: 0px 5px;
903
- border-radius: 8px;
904
- }
905
-
906
- .rp-item {
907
- display: flex;
908
- align-items: center;
909
- padding: 5px 8px;
910
- border-radius: var(--radius-sm);
911
- cursor: pointer;
912
- font-size: 11px;
913
- gap: 8px;
914
- margin-bottom: 1px;
915
- transition: background 0.1s;
916
- }
917
-
918
- .rp-item:hover { background: var(--bg-hover); }
919
-
920
- .rp-item .rp-score {
921
- font-family: var(--font-mono);
922
- font-size: 10px;
923
- color: var(--neon-green);
924
- min-width: 30px;
925
- opacity: 0.8;
926
- }
927
-
928
- .rp-item .rp-title {
929
- overflow: hidden;
930
- text-overflow: ellipsis;
931
- white-space: nowrap;
932
- color: var(--neon-purple);
933
- font-weight: 500;
934
- }
935
-
936
- .rp-item .rp-backlink-title { color: var(--neon-cyan); }
937
-
938
- /* ============ OUTLINE ============ */
939
- .outline-item {
940
- display: block;
941
- padding: 3px 8px;
942
- border-radius: var(--radius-sm);
943
- cursor: pointer;
944
- font-size: 11px;
945
- color: var(--text-secondary);
946
- transition: all 0.1s;
947
- overflow: hidden;
948
- text-overflow: ellipsis;
949
- white-space: nowrap;
950
- margin-bottom: 1px;
951
- }
952
-
953
- .outline-item:hover {
954
- background: var(--bg-hover);
955
- color: var(--text-primary);
956
- }
957
-
958
- .outline-item.h2 { padding-left: 8px; }
959
- .outline-item.h3 { padding-left: 20px; font-size: 10px; color: var(--text-muted); }
960
-
961
- /* ============ GRAPH ============ */
962
- #graph-section { margin-bottom: 12px; transition: all 0.3s; }
963
- #graph-section.hidden { display: none; }
964
-
965
- #graph-section.maximized {
966
- position: fixed;
967
- inset: 40px;
968
- z-index: 250;
969
- background: rgba(17,17,22,0.95);
970
- border: 1px solid var(--border);
971
- border-radius: var(--radius-lg);
972
- padding: 20px;
973
- display: flex;
974
- flex-direction: column;
975
- box-shadow: 0 24px 64px rgba(0,0,0,0.8), 0 0 0 1px rgba(217,119,87,0.3);
976
- backdrop-filter: blur(20px);
977
- -webkit-backdrop-filter: blur(20px);
978
- }
979
-
980
- #graph-container {
981
- width: 100%;
982
- height: 200px;
983
- border: 1px solid var(--border);
984
- border-radius: var(--radius);
985
- overflow: hidden;
986
- background: transparent;
987
- }
988
-
989
- #graph-section.maximized #graph-container {
990
- flex: 1;
991
- height: auto !important;
992
- border: none;
993
- }
994
-
995
- #graph-container svg { width: 100%; height: 100%; }
996
-
997
- .graph-node { cursor: pointer; }
998
-
999
- .graph-node circle {
1000
- fill: var(--neon-purple);
1001
- filter: drop-shadow(0 0 3px rgba(168,139,250,0.5));
1002
- transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
1003
- }
1004
-
1005
- .graph-node:hover circle {
1006
- fill: var(--neon-cyan);
1007
- r: 8;
1008
- filter: drop-shadow(0 0 8px rgba(217,119,87,0.7));
1009
- }
1010
-
1011
- .graph-node.active circle {
1012
- fill: var(--neon-cyan);
1013
- filter: drop-shadow(0 0 10px rgba(217,119,87,0.9));
1014
- }
1015
-
1016
- .graph-node text {
1017
- fill: var(--text-muted);
1018
- font-family: var(--font-mono);
1019
- font-size: 11px;
1020
- pointer-events: none;
1021
- transition: all 0.2s;
1022
- text-shadow: 0 0 2px var(--bg-primary), 0 0 4px var(--bg-primary);
1023
- }
1024
-
1025
- .graph-node:hover text, .graph-node.active text {
1026
- fill: var(--text-primary);
1027
- font-size: 13px;
1028
- }
1029
-
1030
- .graph-link {
1031
- stroke: var(--neon-purple);
1032
- stroke-width: 1.5;
1033
- transition: opacity 0.3s;
1034
- }
1035
-
1036
- .graph-size-controls {
1037
- margin-left: auto;
1038
- display: flex;
1039
- gap: 2px;
1040
- }
1041
-
1042
- .graph-size-btn {
1043
- background: var(--bg-hover);
1044
- border: 1px solid var(--border);
1045
- color: var(--text-muted);
1046
- font-family: var(--font-mono);
1047
- font-size: 11px;
1048
- width: 18px;
1049
- height: 18px;
1050
- border-radius: 3px;
1051
- cursor: pointer;
1052
- display: flex;
1053
- align-items: center;
1054
- justify-content: center;
1055
- transition: all 0.15s;
1056
- padding: 0;
1057
- line-height: 1;
1058
- }
1059
-
1060
- .graph-size-btn:hover {
1061
- border-color: var(--neon-cyan);
1062
- color: var(--neon-cyan);
1063
- }
1064
-
1065
- .graph-resize-handle {
1066
- height: 8px;
1067
- cursor: ns-resize;
1068
- position: relative;
1069
- }
1070
-
1071
- .graph-resize-handle::after {
1072
- content: '';
1073
- position: absolute;
1074
- bottom: 2px;
1075
- left: 50%;
1076
- transform: translateX(-50%);
1077
- width: 24px;
1078
- height: 2px;
1079
- background: var(--border);
1080
- border-radius: 1px;
1081
- transition: background 0.2s;
1082
- }
1083
-
1084
- .graph-resize-handle:hover::after {
1085
- background: var(--neon-cyan);
1086
- }
1087
-
1088
- /* ============ SESSIONS PILLAR ============ */
1089
- .pillar-divider {
1090
- height: 1px;
1091
- margin: 4px 12px;
1092
- background: linear-gradient(90deg, transparent, var(--border), transparent);
1093
- }
1094
-
1095
- .pillar-label {
1096
- font-family: var(--font-mono);
1097
- font-size: 8px;
1098
- font-weight: 700;
1099
- text-transform: uppercase;
1100
- letter-spacing: 2px;
1101
- color: var(--text-muted);
1102
- padding: 10px 18px 4px;
1103
- display: flex;
1104
- align-items: center;
1105
- justify-content: space-between;
1106
- user-select: none;
1107
- }
1108
-
1109
- .pillar-label .pillar-accent {
1110
- width: 6px;
1111
- height: 6px;
1112
- border-radius: 50%;
1113
- display: inline-block;
1114
- margin-right: 6px;
1115
- }
1116
-
1117
- .pillar-label .pillar-accent.sessions { background: var(--neon-green); box-shadow: 0 0 6px var(--neon-green); }
1118
- .pillar-label .pillar-accent.projects { background: var(--neon-cyan); box-shadow: 0 0 6px var(--neon-cyan); }
1119
-
1120
- .pillar-label .pillar-action {
1121
- font-size: 9px;
1122
- font-weight: 500;
1123
- color: var(--text-muted);
1124
- cursor: pointer;
1125
- padding: 1px 6px;
1126
- border-radius: 3px;
1127
- border: 1px solid transparent;
1128
- transition: all 0.15s;
1129
- letter-spacing: 0.5px;
1130
- }
1131
-
1132
- .pillar-label .pillar-action:hover {
1133
- color: var(--neon-green);
1134
- border-color: rgba(91,185,140,0.3);
1135
- }
1136
-
1137
- .session-search-box {
1138
- padding: 6px 12px 4px;
1139
- }
1140
-
1141
- .session-search-box input {
1142
- width: 100%;
1143
- background: rgba(8,8,10,0.4);
1144
- border: 1px solid var(--border);
1145
- color: var(--text-primary);
1146
- font-family: var(--font-mono);
1147
- font-size: 10px;
1148
- padding: 5px 10px 5px 24px;
1149
- border-radius: var(--radius);
1150
- outline: none;
1151
- transition: all 0.2s;
1152
- }
1153
-
1154
- .session-search-box input::placeholder { color: var(--text-muted); }
1155
-
1156
- .session-search-box input:focus {
1157
- border-color: rgba(91,185,140,0.4);
1158
- box-shadow: 0 0 8px rgba(91,185,140,0.08);
1159
- }
1160
-
1161
- .session-search-box {
1162
- position: relative;
1163
- }
1164
-
1165
- .session-search-box .ss-icon {
1166
- position: absolute;
1167
- left: 20px;
1168
- top: 50%;
1169
- transform: translateY(-50%);
1170
- color: var(--text-muted);
1171
- font-size: 10px;
1172
- pointer-events: none;
1173
- }
1174
-
1175
- .session-search-results {
1176
- padding: 0 8px;
1177
- max-height: 180px;
1178
- overflow-y: auto;
1179
- }
1180
-
1181
- .session-search-results:empty { display: none; }
1182
-
1183
- .ss-result {
1184
- padding: 5px 10px;
1185
- border-radius: var(--radius-sm);
1186
- cursor: pointer;
1187
- margin: 1px 0;
1188
- transition: background 0.1s;
1189
- }
1190
-
1191
- .ss-result:hover { background: var(--bg-hover); }
1192
-
1193
- .ss-result .ss-title {
1194
- font-size: 11px;
1195
- color: var(--neon-green);
1196
- font-weight: 500;
1197
- }
1198
-
1199
- .ss-result .ss-meta {
1200
- font-family: var(--font-mono);
1201
- font-size: 9px;
1202
- color: var(--text-muted);
1203
- margin-top: 1px;
1204
- }
1205
-
1206
- .ss-result .ss-snippet {
1207
- font-size: 10px;
1208
- color: var(--text-secondary);
1209
- margin-top: 2px;
1210
- overflow: hidden;
1211
- text-overflow: ellipsis;
1212
- white-space: nowrap;
1213
- }
1214
-
1215
- .session-project-group {
1216
- padding: 0 8px;
1217
- }
1218
-
1219
- .session-project-header {
1220
- display: flex;
1221
- align-items: center;
1222
- padding: 5px 10px;
1223
- border-radius: var(--radius-sm);
1224
- cursor: pointer;
1225
- gap: 6px;
1226
- user-select: none;
1227
- transition: background 0.1s;
1228
- margin: 1px 0;
1229
- }
1230
-
1231
- .session-project-header:hover { background: var(--bg-hover); }
1232
-
1233
- .session-project-header .sp-arrow {
1234
- width: 12px;
1235
- font-size: 8px;
1236
- color: var(--text-muted);
1237
- text-align: center;
1238
- flex-shrink: 0;
1239
- transition: transform 0.15s;
1240
- }
1241
-
1242
- .session-project-header .sp-arrow.open { transform: rotate(90deg); }
1243
-
1244
- .session-project-header .sp-icon {
1245
- font-size: 11px;
1246
- color: var(--neon-green);
1247
- flex-shrink: 0;
1248
- opacity: 0.7;
1249
- }
1250
-
1251
- .session-project-header .sp-name {
1252
- font-size: 12px;
1253
- color: var(--text-primary);
1254
- font-weight: 500;
1255
- flex: 1;
1256
- }
1257
-
1258
- .session-project-header .sp-count {
1259
- font-family: var(--font-mono);
1260
- font-size: 9px;
1261
- color: var(--text-muted);
1262
- background: rgba(91,185,140,0.06);
1263
- padding: 1px 5px;
1264
- border-radius: 3px;
1265
- }
1266
-
1267
- .session-project-header .sp-add {
1268
- background: transparent;
1269
- border: 1px solid var(--border);
1270
- color: var(--text-muted);
1271
- font-family: var(--font-mono);
1272
- font-size: 11px;
1273
- width: 16px;
1274
- height: 16px;
1275
- border-radius: 3px;
1276
- cursor: pointer;
1277
- display: none;
1278
- align-items: center;
1279
- justify-content: center;
1280
- transition: all 0.15s;
1281
- padding: 0;
1282
- line-height: 1;
1283
- }
1284
-
1285
- .session-project-header:hover .sp-add { display: flex; }
1286
- .session-project-header .sp-add:hover { border-color: var(--neon-green); color: var(--neon-green); }
1287
-
1288
- .session-list {
1289
- padding-left: 12px;
1290
- overflow: hidden;
1291
- }
1292
-
1293
- .session-list.collapsed { display: none; }
1294
-
1295
- .session-item {
1296
- display: flex;
1297
- align-items: center;
1298
- padding: 3px 10px;
1299
- border-radius: var(--radius-sm);
1300
- cursor: pointer;
1301
- font-size: 11px;
1302
- gap: 6px;
1303
- user-select: none;
1304
- transition: background 0.1s;
1305
- position: relative;
1306
- margin: 1px 0;
1307
- }
1308
-
1309
- .session-item:hover { background: var(--bg-hover); }
1310
-
1311
- .session-item.active {
1312
- background: rgba(91,185,140,0.05);
1313
- }
1314
-
1315
- .session-item.active::before {
1316
- content: '';
1317
- position: absolute;
1318
- left: 0;
1319
- top: 3px;
1320
- bottom: 3px;
1321
- width: 2px;
1322
- background: var(--neon-green);
1323
- border-radius: 1px;
1324
- }
1325
-
1326
- .session-item .si-icon {
1327
- font-size: 9px;
1328
- color: var(--neon-green);
1329
- opacity: 0.5;
1330
- flex-shrink: 0;
1331
- }
1332
-
1333
- .session-item .si-time {
1334
- font-family: var(--font-mono);
1335
- font-size: 10px;
1336
- color: var(--text-secondary);
1337
- }
1338
-
1339
- .session-item.active .si-time { color: var(--neon-green); }
1340
-
1341
- .session-badge {
1342
- display: inline-flex;
1343
- align-items: center;
1344
- gap: 8px;
1345
- font-family: var(--font-mono);
1346
- font-size: 10px;
1347
- color: var(--neon-green);
1348
- background: rgba(91,185,140,0.08);
1349
- border: 1px solid rgba(91,185,140,0.15);
1350
- padding: 4px 12px;
1351
- border-radius: var(--radius);
1352
- margin-bottom: 16px;
1353
- letter-spacing: 0.5px;
1354
- }
1355
-
1356
- .session-empty {
1357
- padding: 12px 18px;
1358
- font-family: var(--font-mono);
1359
- font-size: 10px;
1360
- color: var(--text-muted);
1361
- text-align: center;
1362
- }
1363
-
1364
- .session-create-overlay {
1365
- position: fixed;
1366
- inset: 0;
1367
- background: rgba(6,6,12,0.6);
1368
- z-index: 200;
1369
- display: none;
1370
- align-items: center;
1371
- justify-content: center;
1372
- backdrop-filter: blur(12px);
1373
- -webkit-backdrop-filter: blur(12px);
1374
- }
1375
-
1376
- .session-create-overlay.active { display: flex; }
1377
-
1378
- .session-create-modal {
1379
- width: 420px;
1380
- background: rgba(17,17,22,0.85);
1381
- border: 1px solid var(--border);
1382
- border-radius: var(--radius-lg);
1383
- box-shadow: 0 24px 64px rgba(0,0,0,0.8), 0 0 0 1px rgba(91,185,140,0.1);
1384
- overflow: hidden;
1385
- backdrop-filter: blur(20px);
1386
- -webkit-backdrop-filter: blur(20px);
1387
- }
1388
-
1389
- .session-create-header {
1390
- padding: 16px 20px 12px;
1391
- border-bottom: 1px solid var(--border);
1392
- font-family: var(--font-mono);
1393
- font-size: 11px;
1394
- font-weight: 600;
1395
- color: var(--text-secondary);
1396
- letter-spacing: 0.5px;
1397
- }
1398
-
1399
- .session-create-body {
1400
- padding: 16px 20px;
1401
- display: flex;
1402
- flex-direction: column;
1403
- gap: 12px;
1404
- }
1405
-
1406
- .session-field label {
1407
- display: block;
1408
- font-family: var(--font-mono);
1409
- font-size: 9px;
1410
- font-weight: 600;
1411
- text-transform: uppercase;
1412
- letter-spacing: 1px;
1413
- color: var(--text-muted);
1414
- margin-bottom: 4px;
1415
- }
1416
-
1417
- .session-field select,
1418
- .session-field input,
1419
- .session-field textarea {
1420
- width: 100%;
1421
- background: rgba(8,8,10,0.5);
1422
- border: 1px solid var(--border);
1423
- color: var(--text-primary);
1424
- font-family: var(--font-mono);
1425
- font-size: 12px;
1426
- padding: 8px 12px;
1427
- border-radius: var(--radius);
1428
- outline: none;
1429
- transition: all 0.2s;
1430
- }
1431
-
1432
- .session-field select:focus,
1433
- .session-field input:focus,
1434
- .session-field textarea:focus {
1435
- border-color: rgba(91,185,140,0.4);
1436
- box-shadow: 0 0 8px rgba(91,185,140,0.1);
1437
- background: var(--bg-surface);
1438
- }
1439
-
1440
- .session-field textarea {
1441
- min-height: 60px;
1442
- resize: vertical;
1443
- }
1444
-
1445
- .session-create-footer {
1446
- padding: 12px 20px;
1447
- border-top: 1px solid var(--border);
1448
- display: flex;
1449
- justify-content: flex-end;
1450
- gap: 8px;
1451
- }
1452
-
1453
- .session-create-footer .edit-btn.create {
1454
- background: rgba(91,185,140,0.1);
1455
- color: var(--neon-green);
1456
- border-color: rgba(91,185,140,0.3);
1457
- }
1458
-
1459
- .session-create-footer .edit-btn.create:hover {
1460
- background: rgba(91,185,140,0.2);
1461
- box-shadow: 0 0 10px rgba(91,185,140,0.15);
1462
- transform: translateY(-1px);
1463
- }
1464
-
1465
- /* ============ SCROLLBAR ============ */
1466
- ::-webkit-scrollbar { width: 5px; height: 5px; }
1467
- ::-webkit-scrollbar-track { background: transparent; }
1468
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
1469
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
1470
-
1471
- /* ============ EDIT MODAL ============ */
1472
- .edit-overlay {
1473
- position: fixed;
1474
- inset: 0;
1475
- background: rgba(6,6,12,0.6);
1476
- z-index: 200;
1477
- display: none;
1478
- align-items: center;
1479
- justify-content: center;
1480
- backdrop-filter: blur(12px);
1481
- -webkit-backdrop-filter: blur(12px);
1482
- }
1483
-
1484
- .edit-overlay.active { display: flex; }
1485
-
1486
- .edit-modal {
1487
- width: 640px;
1488
- max-height: 80vh;
1489
- background: rgba(17,17,22,0.85);
1490
- border: 1px solid var(--border);
1491
- border-radius: var(--radius-lg);
1492
- box-shadow: 0 24px 64px rgba(0,0,0,0.8), 0 0 0 1px rgba(217,119,87,0.1);
1493
- display: flex;
1494
- flex-direction: column;
1495
- overflow: hidden;
1496
- backdrop-filter: blur(20px);
1497
- -webkit-backdrop-filter: blur(20px);
1498
- }
1499
-
1500
- .edit-header {
1501
- display: flex;
1502
- align-items: center;
1503
- justify-content: space-between;
1504
- padding: 12px 16px;
1505
- border-bottom: 1px solid var(--border);
1506
- font-family: var(--font-mono);
1507
- font-size: 11px;
1508
- color: var(--text-secondary);
1509
- }
1510
-
1511
- .edit-header .edit-path { opacity: 0.6; }
1512
-
1513
- .edit-actions { display: flex; gap: 8px; }
1514
-
1515
- .edit-btn {
1516
- font-family: var(--font-mono);
1517
- font-size: 10px;
1518
- padding: 5px 14px;
1519
- border-radius: var(--radius);
1520
- border: 1px solid var(--border);
1521
- cursor: pointer;
1522
- transition: all 0.2s ease-out;
1523
- letter-spacing: 0.5px;
1524
- }
1525
-
1526
- .edit-btn.cancel {
1527
- background: transparent;
1528
- color: var(--text-muted);
1529
- }
1530
-
1531
- .edit-btn.cancel:hover { color: var(--text-secondary); border-color: var(--text-muted); transform: translateY(-1px); }
1532
-
1533
- .edit-btn.save {
1534
- background: rgba(217,119,87,0.1);
1535
- color: var(--neon-cyan);
1536
- border-color: rgba(217,119,87,0.3);
1537
- }
1538
-
1539
- .edit-btn.save:hover { background: rgba(217,119,87,0.2); box-shadow: 0 0 10px rgba(217,119,87,0.15); transform: translateY(-1px); }
1540
-
1541
- .edit-textarea {
1542
- flex: 1;
1543
- min-height: 300px;
1544
- background: rgba(8,8,10,0.3);
1545
- border: none;
1546
- color: var(--text-primary);
1547
- font-family: var(--font-mono);
1548
- font-size: 13px;
1549
- line-height: 1.8;
1550
- padding: 16px 20px;
1551
- resize: none;
1552
- outline: none;
1553
- }
1554
-
1555
- .edit-textarea::placeholder { color: var(--text-muted); }
1556
-
1557
- .note-edit-btn {
1558
- background: transparent;
1559
- border: 1px solid var(--border);
1560
- color: var(--text-muted);
1561
- font-family: var(--font-mono);
1562
- font-size: 10px;
1563
- padding: 3px 10px;
1564
- border-radius: var(--radius-sm);
1565
- cursor: pointer;
1566
- transition: all 0.15s;
1567
- float: right;
1568
- margin-bottom: 12px;
1569
- }
1570
-
1571
- .note-edit-btn:hover {
1572
- border-color: var(--text-muted);
1573
- color: var(--text-secondary);
1574
- }
1575
-
1576
- /* ============ BACKLINK & UNLINKED ITEMS ============ */
1577
- .rp-link-item {
1578
- display: flex;
1579
- align-items: center;
1580
- padding: 4px 8px;
1581
- border-radius: var(--radius-sm);
1582
- cursor: pointer;
1583
- margin-bottom: 1px;
1584
- transition: background 0.1s;
1585
- gap: 6px;
1586
- }
1587
-
1588
- .rp-link-item:hover { background: var(--bg-hover); }
1589
-
1590
- .rp-link-item .rp-link-icon {
1591
- font-size: 9px;
1592
- flex-shrink: 0;
1593
- opacity: 0.5;
1594
- }
1595
-
1596
- .rp-link-item .rp-link-icon.backlink { color: var(--neon-cyan); }
1597
- .rp-link-item .rp-link-icon.unlinked { color: var(--text-muted); }
1598
-
1599
- .rp-link-item .rp-link-title {
1600
- font-size: 12px;
1601
- color: var(--neon-cyan);
1602
- font-weight: 500;
1603
- overflow: hidden;
1604
- text-overflow: ellipsis;
1605
- white-space: nowrap;
1606
- transition: all 0.15s;
1607
- }
1608
-
1609
- .rp-link-item:hover .rp-link-title {
1610
- text-decoration: underline;
1611
- text-underline-offset: 2px;
1612
- }
1613
-
1614
- .rp-link-item.unlinked .rp-link-title {
1615
- color: var(--text-secondary);
1616
- }
1617
-
1618
- .rp-link-item.unlinked:hover .rp-link-title {
1619
- color: var(--neon-cyan);
1620
- text-decoration: underline;
1621
- }
1622
-
1623
- /* ============ TRANSITIONS ============ */
1624
- .fade-in { animation: fadeIn 0.15s ease; }
1625
-
1626
- @keyframes fadeIn {
1627
- from { opacity: 0; transform: translateY(3px); }
1628
- to { opacity: 1; transform: translateY(0); }
1629
- }
1630
- </style>
1631
- </head>
1632
- <body>
1633
-
1634
- <div class="layout" id="layout">
1635
- <!-- Header -->
1636
- <div class="header">
1637
- <div class="logo">KYP-MEM <span class="logo-sub">know your project</span></div>
1638
- <div class="breadcrumb" id="breadcrumb"></div>
1639
- <div class="header-actions">
1640
- <button class="header-btn active" id="graph-toggle" title="Toggle graph">
1641
- <span class="dot"></span>
1642
- <span>GRAPH</span>
1643
- </button>
1644
- <button class="header-btn" id="new-project-btn" title="New project">
1645
- <span>+ PROJECT</span>
1646
- </button>
1647
- <div class="search-box">
1648
- <span class="search-icon">&#9906;</span>
1649
- <input type="text" id="search-input" placeholder="Search...">
1650
- <span class="search-hint">&#8984;K</span>
1651
- <div class="search-results" id="search-results"></div>
1652
- </div>
1653
- </div>
1654
- </div>
1655
-
1656
- <!-- Left sidebar -->
1657
- <div class="sidebar">
1658
- <div class="sidebar-scroll">
1659
- <!-- SESSIONS PILLAR (Claude-Mem) -->
1660
- <div class="pillar-label">
1661
- <span><span class="pillar-accent sessions"></span>SESSIONS</span>
1662
- <span class="pillar-action" id="new-session-btn">+ NEW</span>
1663
- </div>
1664
- <div class="session-search-box">
1665
- <span class="ss-icon">&#9906;</span>
1666
- <input type="text" id="session-search-input" placeholder="Semantic search sessions...">
1667
- </div>
1668
- <div class="session-search-results" id="session-search-results"></div>
1669
- <div id="session-tree"></div>
1670
-
1671
- <div class="pillar-divider"></div>
1672
-
1673
- <!-- PROJECTS PILLAR (Obsidian) -->
1674
- <div class="pillar-label">
1675
- <span><span class="pillar-accent projects"></span>PROJECTS</span>
1676
- <span class="pillar-action" id="new-project-sidebar-btn">+ NEW</span>
1677
- </div>
1678
- <div class="sidebar-section">
1679
- <div id="filter-info" class="filter-info" style="display:none;">
1680
- <span id="filter-count"></span>
1681
- <span class="clear-btn" id="clear-filters">clear</span>
1682
- </div>
1683
- <div id="file-tree"></div>
1684
- </div>
1685
- <div class="tag-filter-section">
1686
- <div class="tag-filter-header" id="tag-filter-toggle">
1687
- <span>Tags</span>
1688
- <span class="arrow open" id="tag-arrow">&#9654;</span>
1689
- </div>
1690
- <div class="tag-filter-body" id="tag-filter-body">
1691
- <div class="tag-filter-active" id="active-tags"></div>
1692
- <div class="tag-cloud" id="tag-cloud"></div>
1693
- </div>
1694
- </div>
1695
- </div>
1696
- <div class="stats-bar" id="stats-bar"></div>
1697
- </div>
1698
-
1699
- <!-- Resize handle: sidebar | content -->
1700
- <div class="resize-handle" id="resize-left"></div>
1701
-
1702
- <!-- Content -->
1703
- <div class="content" id="content">
1704
- <div class="empty-state">
1705
- <div class="es-logo">KYP-MEM</div>
1706
- <div class="es-tagline">Know Your Project Memory</div>
1707
- <div class="es-hint">Select a note or press <kbd>&#8984;O</kbd> to quick-switch &middot; <kbd>&#8984;K</kbd> to search</div>
1708
- </div>
1709
- </div>
1710
-
1711
- <!-- Resize handle: content | right panel -->
1712
- <div class="resize-handle" id="resize-right"></div>
1713
-
1714
- <!-- Right panel -->
1715
- <div class="right-panel" id="right-panel">
1716
- <div id="graph-section">
1717
- <div class="rp-section-title">Local Graph <span class="graph-size-controls"><button class="graph-size-btn" id="graph-shrink" title="Shrink graph">&minus;</button><button class="graph-size-btn" id="graph-grow" title="Grow graph">+</button><button class="graph-size-btn" id="graph-max" title="Maximize graph">&#9974;</button></span></div>
1718
- <div id="graph-container"></div>
1719
- <div id="graph-resize-handle" class="graph-resize-handle"></div>
1720
- </div>
1721
- <div id="rp-outline" class="rp-section" style="display:none;">
1722
- <div class="rp-section-title" onclick="this.parentElement.classList.toggle('collapsed')">Outline <span class="rp-count" id="outline-count"></span><span class="arrow">&#9654;</span></div>
1723
- <div id="rp-outline-list" class="rp-section-body"></div>
1724
- </div>
1725
- <div id="rp-backlinks" class="rp-section" style="display:none;">
1726
- <div class="rp-section-title" onclick="this.parentElement.classList.toggle('collapsed')">Backlinks <span class="rp-count" id="bl-count"></span><span class="arrow">&#9654;</span></div>
1727
- <div id="rp-backlinks-list" class="rp-section-body"></div>
1728
- </div>
1729
- <div id="rp-related" class="rp-section" style="display:none;">
1730
- <div class="rp-section-title" onclick="this.parentElement.classList.toggle('collapsed')">Related <span class="rp-count" id="rel-count"></span><span class="arrow">&#9654;</span></div>
1731
- <div id="rp-related-list" class="rp-section-body"></div>
1732
- </div>
1733
- <div id="rp-outlinks" class="rp-section" style="display:none;">
1734
- <div class="rp-section-title" onclick="this.parentElement.classList.toggle('collapsed')">Outgoing Links <span class="rp-count" id="out-count"></span><span class="arrow">&#9654;</span></div>
1735
- <div id="rp-outlinks-list" class="rp-section-body"></div>
1736
- </div>
1737
- <div id="rp-unlinked" class="rp-section" style="display:none;">
1738
- <div class="rp-section-title" onclick="this.parentElement.classList.toggle('collapsed')">Unlinked Mentions <span class="rp-count" id="unlinked-count"></span><span class="arrow">&#9654;</span></div>
1739
- <div id="rp-unlinked-list" class="rp-section-body"></div>
1740
- </div>
1741
- </div>
1742
- </div>
1743
-
1744
- <!-- Edit Modal -->
1745
- <div class="edit-overlay" id="edit-overlay">
1746
- <div class="edit-modal">
1747
- <div class="edit-header">
1748
- <span class="edit-path" id="edit-path"></span>
1749
- <div class="edit-actions">
1750
- <button class="edit-btn cancel" id="edit-cancel">ESC</button>
1751
- <button class="edit-btn save" id="edit-save">SAVE</button>
1752
- </div>
1753
- </div>
1754
- <textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown..."></textarea>
1755
- </div>
1756
- </div>
1757
-
1758
- <!-- Quick Switcher Overlay -->
1759
- <div class="quick-switcher-overlay" id="qs-overlay">
1760
- <div class="quick-switcher">
1761
- <input type="text" id="qs-input" placeholder="Jump to note...">
1762
- <div class="quick-switcher-results" id="qs-results"></div>
1763
- </div>
1764
- </div>
1765
-
1766
- <!-- Session Create Modal -->
1767
- <div class="session-create-overlay" id="session-create-overlay">
1768
- <div class="session-create-modal">
1769
- <div class="session-create-header">NEW SESSION</div>
1770
- <div class="session-create-body">
1771
- <div class="session-field">
1772
- <label>Project</label>
1773
- <input type="text" id="session-project" list="project-list" placeholder="Project name...">
1774
- <datalist id="project-list"></datalist>
1775
- </div>
1776
- <div class="session-field">
1777
- <label>Summary</label>
1778
- <textarea id="session-summary" placeholder="What are you working on?"></textarea>
1779
- </div>
1780
- </div>
1781
- <div class="session-create-footer">
1782
- <button class="edit-btn cancel" id="session-cancel">CANCEL</button>
1783
- <button class="edit-btn create" id="session-create-btn">CREATE</button>
1784
- </div>
1785
- </div>
1786
- </div>
1787
-
1788
- <!-- Project Create Modal -->
1789
- <div class="session-create-overlay" id="project-create-overlay">
1790
- <div class="session-create-modal">
1791
- <div class="session-create-header">NEW PROJECT</div>
1792
- <div class="session-create-body">
1793
- <div class="session-field">
1794
- <label>Project Name</label>
1795
- <input type="text" id="project-name-input" placeholder="My Project...">
1796
- </div>
1797
- <div class="session-field">
1798
- <label>Overview (optional)</label>
1799
- <textarea id="project-overview-input" placeholder="Brief project description, goals, tech stack..."></textarea>
1800
- </div>
1801
- </div>
1802
- <div class="session-create-footer">
1803
- <button class="edit-btn cancel" id="project-cancel">CANCEL</button>
1804
- <button class="edit-btn create" id="project-create-btn">CREATE</button>
1805
- </div>
1806
- </div>
1807
- </div>
1808
-
1809
- <script>
1810
- let currentPath = null;
1811
- let treeData = null;
1812
- let allNotes = {};
1813
- let graphVisible = true;
1814
- let currentNote = null;
1815
- let activeTagFilters = new Set();
1816
- let qsSelectedIndex = 0;
1817
-
1818
- async function fetchJSON(url) {
1819
- const r = await fetch(url);
1820
- return r.json();
1821
- }
1822
-
1823
- // --- Graph Toggle ---
1824
- document.getElementById('graph-toggle').addEventListener('click', () => {
1825
- graphVisible = !graphVisible;
1826
- const btn = document.getElementById('graph-toggle');
1827
- const section = document.getElementById('graph-section');
1828
- const layout = document.getElementById('layout');
1829
- btn.classList.toggle('active', graphVisible);
1830
-
1831
- if (!graphVisible) {
1832
- section.classList.add('hidden');
1833
- layout.classList.add('no-right-panel');
1834
- } else {
1835
- section.classList.remove('hidden');
1836
- layout.classList.remove('no-right-panel');
1837
- if (currentPath) loadNote(currentPath);
1838
- }
1839
- });
1840
-
1841
- // --- Keyboard shortcuts ---
1842
- document.addEventListener('keydown', (e) => {
1843
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1844
- e.preventDefault();
1845
- document.getElementById('search-input').focus();
1846
- }
1847
- if ((e.metaKey || e.ctrlKey) && e.key === 'o') {
1848
- e.preventDefault();
1849
- openQuickSwitcher();
1850
- }
1851
- if (e.key === 'Escape') {
1852
- closeQuickSwitcher();
1853
- closeSessionCreate();
1854
- closeProjectCreate();
1855
- }
1856
- });
1857
-
1858
- // --- Quick Switcher ---
1859
- function openQuickSwitcher() {
1860
- const overlay = document.getElementById('qs-overlay');
1861
- const input = document.getElementById('qs-input');
1862
- overlay.classList.add('active');
1863
- input.value = '';
1864
- input.focus();
1865
- qsSelectedIndex = 0;
1866
- renderQsResults('');
1867
- }
1868
-
1869
- function closeQuickSwitcher() {
1870
- document.getElementById('qs-overlay').classList.remove('active');
1871
- }
1872
-
1873
- document.getElementById('qs-overlay').addEventListener('click', (e) => {
1874
- if (e.target === e.currentTarget) closeQuickSwitcher();
1875
- });
1876
-
1877
- document.getElementById('qs-input').addEventListener('input', (e) => {
1878
- qsSelectedIndex = 0;
1879
- renderQsResults(e.target.value.trim().toLowerCase());
1880
- });
1881
-
1882
- document.getElementById('qs-input').addEventListener('keydown', (e) => {
1883
- const items = document.querySelectorAll('.qs-item');
1884
- if (e.key === 'ArrowDown') {
1885
- e.preventDefault();
1886
- qsSelectedIndex = Math.min(qsSelectedIndex + 1, items.length - 1);
1887
- updateQsSelection();
1888
- } else if (e.key === 'ArrowUp') {
1889
- e.preventDefault();
1890
- qsSelectedIndex = Math.max(qsSelectedIndex - 1, 0);
1891
- updateQsSelection();
1892
- } else if (e.key === 'Enter') {
1893
- e.preventDefault();
1894
- const selected = items[qsSelectedIndex];
1895
- if (selected) {
1896
- loadNote(selected.dataset.path);
1897
- closeQuickSwitcher();
1898
- }
1899
- }
1900
- });
1901
-
1902
- function renderQsResults(query) {
1903
- const container = document.getElementById('qs-results');
1904
- const entries = Object.entries(allNotes);
1905
-
1906
- let filtered = entries;
1907
- if (query) {
1908
- filtered = entries.filter(([path, note]) => {
1909
- const name = note.title.toLowerCase();
1910
- const p = path.toLowerCase();
1911
- return name.includes(query) || p.includes(query);
1912
- });
1913
- }
1914
-
1915
- filtered.sort((a, b) => a[1].title.localeCompare(b[1].title));
1916
- filtered = filtered.slice(0, 15);
1917
-
1918
- if (filtered.length === 0) {
1919
- container.innerHTML = '<div class="qs-empty">No notes found</div>';
1920
- return;
1921
- }
1922
-
1923
- container.innerHTML = filtered.map(([path, note], i) => {
1924
- const folder = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '';
1925
- return `<div class="qs-item${i === qsSelectedIndex ? ' selected' : ''}" data-path="${path}">
1926
- <span class="qs-icon">&#9671;</span>
1927
- <span class="qs-name">${note.title}</span>
1928
- ${folder ? `<span class="qs-path">${folder}</span>` : ''}
1929
- </div>`;
1930
- }).join('');
1931
-
1932
- container.querySelectorAll('.qs-item').forEach((el, i) => {
1933
- el.addEventListener('click', () => {
1934
- loadNote(el.dataset.path);
1935
- closeQuickSwitcher();
1936
- });
1937
- el.addEventListener('mouseenter', () => {
1938
- qsSelectedIndex = i;
1939
- updateQsSelection();
1940
- });
1941
- });
1942
- }
1943
-
1944
- function updateQsSelection() {
1945
- document.querySelectorAll('.qs-item').forEach((el, i) => {
1946
- el.classList.toggle('selected', i === qsSelectedIndex);
1947
- });
1948
- }
1949
-
1950
- // --- File Tree (Projects pillar only — no Sessions folders) ---
1951
- function renderTree(node, container) {
1952
- if (node.type === 'folder' && node.name !== 'vault') {
1953
- if (node.name === 'Sessions') return;
1954
-
1955
- const item = document.createElement('div');
1956
- item.className = 'tree-item';
1957
- item.innerHTML = `<span class="arrow open">&#9654;</span><span class="icon folder-icon">&#9776;</span><span class="name">${node.name}</span>`;
1958
-
1959
- const children = document.createElement('div');
1960
- children.className = 'tree-children';
1961
-
1962
- item.addEventListener('click', (e) => {
1963
- e.stopPropagation();
1964
- item.querySelector('.arrow').classList.toggle('open');
1965
- children.classList.toggle('collapsed');
1966
- });
1967
-
1968
- container.appendChild(item);
1969
- container.appendChild(children);
1970
- (node.children || []).forEach(c => renderTree(c, children));
1971
-
1972
- } else if (node.type === 'note') {
1973
- const item = document.createElement('div');
1974
- item.className = 'tree-item';
1975
- item.dataset.path = node.path;
1976
- const displayName = node.name.replace('.md', '');
1977
- item.innerHTML = `<span class="arrow" style="visibility:hidden">&#9654;</span><span class="icon note-icon">&#9671;</span><span class="name">${displayName}</span>`;
1978
- item.addEventListener('click', () => loadNote(node.path));
1979
- container.appendChild(item);
1980
-
1981
- } else if (node.children) {
1982
- node.children.forEach(c => renderTree(c, container));
1983
- }
1984
- }
1985
-
1986
- // --- Session Tree (Sessions pillar — grouped by project with dropdowns) ---
1987
- function formatSessionTimestamp(filename) {
1988
- const stem = filename.replace('.md', '');
1989
- const match = stem.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})(\d{2})(\d{2})$/);
1990
- if (!match) return stem;
1991
- const [, y, mo, d, h, mi] = match;
1992
- const hr = parseInt(h);
1993
- const ampm = hr >= 12 ? 'PM' : 'AM';
1994
- const hr12 = hr % 12 || 12;
1995
- const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1996
- return `${months[parseInt(mo)-1]} ${parseInt(d)}, ${hr12}:${mi} ${ampm}`;
1997
- }
1998
-
1999
- function renderSessionTree(sessionsData) {
2000
- const container = document.getElementById('session-tree');
2001
- container.innerHTML = '';
2002
-
2003
- if (Object.keys(sessionsData).length === 0) {
2004
- container.innerHTML = '<div class="session-empty">No sessions yet</div>';
2005
- return;
2006
- }
2007
-
2008
- const sortedProjects = Object.keys(sessionsData).sort();
2009
- sortedProjects.forEach(project => {
2010
- const sessions = sessionsData[project];
2011
- const group = document.createElement('div');
2012
- group.className = 'session-project-group';
2013
-
2014
- const header = document.createElement('div');
2015
- header.className = 'session-project-header';
2016
- header.innerHTML = `
2017
- <span class="sp-arrow open">&#9654;</span>
2018
- <span class="sp-icon">&#9716;</span>
2019
- <span class="sp-name">${project}</span>
2020
- <span class="sp-count">${sessions.length}</span>
2021
- <button class="sp-add" title="New session for ${project}">+</button>
2022
- `;
2023
-
2024
- const list = document.createElement('div');
2025
- list.className = 'session-list';
2026
-
2027
- sessions.sort((a, b) => b.path.localeCompare(a.path));
2028
- sessions.forEach(session => {
2029
- const item = document.createElement('div');
2030
- item.className = 'session-item';
2031
- item.dataset.path = session.path;
2032
- const displayTime = formatSessionTimestamp(session.path.split('/').pop());
2033
- item.innerHTML = `<span class="si-icon">&#9679;</span><span class="si-time">${displayTime}</span>`;
2034
- item.title = session.title || session.path;
2035
- item.addEventListener('click', () => loadNote(session.path));
2036
- list.appendChild(item);
2037
- });
2038
-
2039
- header.addEventListener('click', (e) => {
2040
- if (e.target.classList.contains('sp-add')) {
2041
- e.stopPropagation();
2042
- openSessionCreate(project);
2043
- return;
2044
- }
2045
- e.stopPropagation();
2046
- header.querySelector('.sp-arrow').classList.toggle('open');
2047
- list.classList.toggle('collapsed');
2048
- });
2049
-
2050
- group.appendChild(header);
2051
- group.appendChild(list);
2052
- container.appendChild(group);
2053
- });
2054
- }
2055
-
2056
- // --- Session Semantic Search ---
2057
- let sessionSearchTimeout;
2058
- const sessionSearchInput = document.getElementById('session-search-input');
2059
- const sessionSearchResults = document.getElementById('session-search-results');
2060
-
2061
- sessionSearchInput.addEventListener('input', () => {
2062
- clearTimeout(sessionSearchTimeout);
2063
- const q = sessionSearchInput.value.trim();
2064
- if (!q) { sessionSearchResults.innerHTML = ''; return; }
2065
- sessionSearchTimeout = setTimeout(async () => {
2066
- const results = await fetchJSON(`/api/sessions/search?q=${encodeURIComponent(q)}`);
2067
- sessionSearchResults.innerHTML = '';
2068
- if (results.length === 0) {
2069
- sessionSearchResults.innerHTML = '<div class="ss-result"><span class="ss-meta">No matching sessions</span></div>';
2070
- return;
2071
- }
2072
- results.forEach(r => {
2073
- const div = document.createElement('div');
2074
- div.className = 'ss-result';
2075
- const dist = r.distance !== undefined ? ` &middot; dist: ${r.distance.toFixed(2)}` : '';
2076
- div.innerHTML = `<div class="ss-title">${r.title}</div><div class="ss-meta">${r.path}${dist}</div>${r.snippet ? `<div class="ss-snippet">${r.snippet.substring(0, 120)}</div>` : ''}`;
2077
- div.addEventListener('click', () => {
2078
- loadNote(r.path);
2079
- sessionSearchInput.value = '';
2080
- sessionSearchResults.innerHTML = '';
2081
- });
2082
- sessionSearchResults.appendChild(div);
2083
- });
2084
- }, 250);
2085
- });
2086
-
2087
- // --- Note rendering ---
2088
- async function loadNote(path) {
2089
- currentPath = path;
2090
- const note = await fetchJSON(`/api/note/${path}`);
2091
- if (note.error) return;
2092
- currentNote = note;
2093
-
2094
- document.querySelectorAll('.tree-item[data-path]').forEach(el => {
2095
- el.classList.toggle('active', el.dataset.path === path);
2096
- });
2097
- document.querySelectorAll('.session-item[data-path]').forEach(el => {
2098
- el.classList.toggle('active', el.dataset.path === path);
2099
- });
2100
-
2101
- const parts = path.replace('.md', '').split('/');
2102
- document.getElementById('breadcrumb').innerHTML = parts.map((p, i) =>
2103
- i < parts.length - 1
2104
- ? `<span>${p}</span><span class="sep">/</span>`
2105
- : `<span class="current">${p}</span>`
2106
- ).join('');
2107
-
2108
- const contentEl = document.getElementById('content');
2109
- let html = '<div class="fade-in">';
2110
-
2111
- html += `<button class="note-edit-btn" onclick="openEditor('${path}')">EDIT</button>`;
2112
-
2113
- const isSession = path.includes('/Sessions/') || path.startsWith('Sessions/');
2114
- if (isSession) {
2115
- const sessionProject = path.includes('/Sessions/') ? path.split('/Sessions/')[0] : 'General';
2116
- html += `<div class="session-badge">&#9716; SESSION &middot; ${sessionProject}</div>`;
2117
- }
2118
-
2119
- html += '<div class="note-properties">';
2120
- (note.tags || []).forEach(t => { html += `<span class="tag clickable-tag" data-tag="${t}">#${t}</span>`; });
2121
- Object.entries(note.properties || {}).forEach(([k, v]) => {
2122
- html += `<span class="prop-item"><span class="prop-key">${k}:</span> ${v}</span>`;
2123
- });
2124
- if (note.created) html += `<span class="prop-item"><span class="prop-key">created:</span> ${note.created}</span>`;
2125
- if (note.updated && note.updated !== note.created) html += `<span class="prop-item"><span class="prop-key">updated:</span> ${note.updated}</span>`;
2126
- html += '</div>';
2127
-
2128
- let md = note.content || '';
2129
- md = md.replace(/\[\[([^\]]+)\]\]/g, (_, link) => {
2130
- const display = link.includes('#') ? link.split('#').pop() : link;
2131
- return `<span class="wikilink" data-link="${link}">${display}</span>`;
2132
- });
2133
-
2134
- html += `<div class="md-body">${marked.parse(md)}</div>`;
2135
- html += '</div>';
2136
- contentEl.innerHTML = html;
2137
- contentEl.scrollTop = 0;
2138
-
2139
- contentEl.querySelectorAll('.wikilink').forEach(el => {
1109
+ // Wire wikilinks
1110
+ area.querySelectorAll('.wikilink').forEach(el => {
2140
1111
  el.addEventListener('click', () => {
2141
1112
  const target = findNotePath(el.dataset.link.split('#')[0]);
2142
1113
  if (target) loadNote(target);
2143
1114
  });
2144
1115
  });
2145
1116
 
2146
- contentEl.querySelectorAll('.clickable-tag').forEach(el => {
2147
- el.addEventListener('click', () => {
2148
- activeTagFilters.add(el.dataset.tag);
2149
- applyTagFilter();
2150
- });
2151
- });
2152
-
2153
- renderRightPanel(note);
2154
- renderOutline(note);
2155
- if (graphVisible) renderGraph(note);
2156
- }
2157
-
2158
- function findNotePath(name) {
2159
- const lower = name.toLowerCase();
2160
- for (const [path, note] of Object.entries(allNotes)) {
2161
- const stem = path.split('/').pop().replace('.md', '').toLowerCase();
2162
- if (stem === lower || note.title.toLowerCase() === lower) return path;
2163
- }
2164
- return null;
2165
- }
2166
-
2167
- // --- Outline ---
2168
- function renderOutline(note) {
2169
- const section = document.getElementById('rp-outline');
2170
- const list = document.getElementById('rp-outline-list');
2171
- list.innerHTML = '';
2172
-
2173
- const content = note.content || '';
2174
- const headings = [];
2175
- content.split('\n').forEach(line => {
2176
- const m = line.match(/^(#{1,3})\s+(.+)/);
2177
- if (m) headings.push({ level: m[1].length, text: m[2].trim() });
2178
- });
2179
-
2180
- if (headings.length < 2) {
2181
- section.style.display = 'none';
2182
- return;
2183
- }
2184
-
2185
- section.style.display = '';
2186
- document.getElementById('outline-count').textContent = headings.length;
2187
-
2188
- headings.forEach(h => {
2189
- const el = document.createElement('div');
2190
- el.className = `outline-item h${h.level}`;
2191
- el.textContent = h.text;
1117
+ // Wire tag clicks
1118
+ area.querySelectorAll('.tag-chip[data-tag]').forEach(el => {
2192
1119
  el.addEventListener('click', () => {
2193
- const contentEl = document.getElementById('content');
2194
- const target = Array.from(contentEl.querySelectorAll('h1,h2,h3')).find(
2195
- el => el.textContent.trim() === h.text
2196
- );
2197
- if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
2198
- });
2199
- list.appendChild(el);
2200
- });
2201
- }
2202
-
2203
- // --- Right Panel ---
2204
- function renderRightPanel(note) {
2205
- const blSection = document.getElementById('rp-backlinks');
2206
- const blList = document.getElementById('rp-backlinks-list');
2207
- blList.innerHTML = '';
2208
- const backlinks = note.backlinks || [];
2209
- if (backlinks.length) {
2210
- blSection.style.display = '';
2211
- document.getElementById('bl-count').textContent = backlinks.length;
2212
- backlinks.forEach(bl => {
2213
- const item = document.createElement('div');
2214
- item.className = 'rp-link-item';
2215
- item.innerHTML = `<span class="rp-link-icon backlink">&#8592;</span><span class="rp-link-title">${bl.title || bl.path || bl}</span>`;
2216
- item.addEventListener('click', () => loadNote(bl.path || bl));
2217
- blList.appendChild(item);
2218
- });
2219
- } else {
2220
- blSection.style.display = 'none';
2221
- }
2222
-
2223
- const relSection = document.getElementById('rp-related');
2224
- const relList = document.getElementById('rp-related-list');
2225
- relList.innerHTML = '';
2226
- if (note.related && note.related.length) {
2227
- relSection.style.display = '';
2228
- document.getElementById('rel-count').textContent = note.related.length;
2229
- note.related.forEach(r => {
2230
- const item = document.createElement('div');
2231
- item.className = 'rp-item';
2232
- item.innerHTML = `<span class="rp-score">${r.score.toFixed(2)}</span><span class="rp-title">${r.title}</span>`;
2233
- item.addEventListener('click', () => loadNote(r.path));
2234
- relList.appendChild(item);
2235
- });
2236
- } else {
2237
- relSection.style.display = 'none';
2238
- }
2239
-
2240
- const outSection = document.getElementById('rp-outlinks');
2241
- const outList = document.getElementById('rp-outlinks-list');
2242
- outList.innerHTML = '';
2243
- if (note.links && note.links.length) {
2244
- outSection.style.display = '';
2245
- document.getElementById('out-count').textContent = note.links.length;
2246
- note.links.forEach(link => {
2247
- const path = findNotePath(link);
2248
- const item = document.createElement('div');
2249
- item.className = 'rp-item';
2250
- item.innerHTML = `<span class="rp-title">${link}</span>`;
2251
- if (path) item.addEventListener('click', () => loadNote(path));
2252
- else item.style.opacity = '0.3';
2253
- outList.appendChild(item);
2254
- });
2255
- } else {
2256
- outSection.style.display = 'none';
2257
- }
2258
-
2259
- const ulSection = document.getElementById('rp-unlinked');
2260
- const ulList = document.getElementById('rp-unlinked-list');
2261
- ulList.innerHTML = '';
2262
- const unlinked = note.unlinked || [];
2263
- if (unlinked.length) {
2264
- ulSection.style.display = '';
2265
- document.getElementById('unlinked-count').textContent = unlinked.length;
2266
- unlinked.forEach(u => {
2267
- const item = document.createElement('div');
2268
- item.className = 'rp-link-item unlinked';
2269
- item.innerHTML = `<span class="rp-link-icon unlinked">&#8776;</span><span class="rp-link-title">${u.title}</span>`;
2270
- item.addEventListener('click', () => loadNote(u.path));
2271
- ulList.appendChild(item);
2272
- });
2273
- } else {
2274
- ulSection.style.display = 'none';
2275
- }
2276
- }
2277
-
2278
- // --- Editor ---
2279
- let editingPath = null;
2280
- let editingNote = null;
2281
-
2282
- async function openEditor(path) {
2283
- const note = await fetchJSON(`/api/note/${path}`);
2284
- if (note.error) return;
2285
- editingPath = path;
2286
- editingNote = note;
2287
- document.getElementById('edit-path').textContent = path;
2288
- document.getElementById('edit-textarea').value = note.content || '';
2289
- document.getElementById('edit-overlay').classList.add('active');
2290
- document.getElementById('edit-textarea').focus();
2291
- }
2292
-
2293
- document.getElementById('edit-cancel').addEventListener('click', closeEditor);
2294
- document.getElementById('edit-overlay').addEventListener('click', (e) => {
2295
- if (e.target === e.currentTarget) closeEditor();
2296
- });
2297
-
2298
- document.getElementById('edit-save').addEventListener('click', async () => {
2299
- if (!editingPath) return;
2300
- const content = document.getElementById('edit-textarea').value;
2301
- await fetch(`/api/note/${editingPath}`, {
2302
- method: 'POST',
2303
- headers: { 'Content-Type': 'application/json' },
2304
- body: JSON.stringify({
2305
- content,
2306
- tags: editingNote.tags || [],
2307
- properties: editingNote.properties || {},
2308
- }),
2309
- });
2310
- closeEditor();
2311
- await loadNote(editingPath);
2312
- });
2313
-
2314
- function closeEditor() {
2315
- document.getElementById('edit-overlay').classList.remove('active');
2316
- editingPath = null;
2317
- editingNote = null;
2318
- }
2319
-
2320
- document.addEventListener('keydown', (e) => {
2321
- if (e.key === 'Escape' && document.getElementById('edit-overlay').classList.contains('active')) {
2322
- closeEditor();
2323
- }
2324
- if ((e.metaKey || e.ctrlKey) && e.key === 's' && editingPath) {
2325
- e.preventDefault();
2326
- document.getElementById('edit-save').click();
2327
- }
2328
- });
2329
-
2330
- // --- Session Management ---
2331
- function openSessionCreate(defaultProject) {
2332
- const overlay = document.getElementById('session-create-overlay');
2333
- const projectInput = document.getElementById('session-project');
2334
- const datalist = document.getElementById('project-list');
2335
- const summaryInput = document.getElementById('session-summary');
2336
-
2337
- const projects = new Set();
2338
- for (const path of Object.keys(allNotes)) {
2339
- const parts = path.split('/');
2340
- if (parts.length > 1) projects.add(parts[0]);
2341
- }
2342
-
2343
- datalist.innerHTML = '';
2344
- for (const p of [...projects].sort()) {
2345
- const opt = document.createElement('option');
2346
- opt.value = p;
2347
- datalist.appendChild(opt);
2348
- }
2349
-
2350
- projectInput.value = defaultProject || '';
2351
- summaryInput.value = '';
2352
- overlay.classList.add('active');
2353
-
2354
- if (defaultProject) summaryInput.focus();
2355
- else projectInput.focus();
2356
- }
2357
-
2358
- function closeSessionCreate() {
2359
- document.getElementById('session-create-overlay').classList.remove('active');
2360
- }
2361
-
2362
- document.getElementById('session-cancel').addEventListener('click', closeSessionCreate);
2363
- document.getElementById('session-create-overlay').addEventListener('click', (e) => {
2364
- if (e.target === e.currentTarget) closeSessionCreate();
2365
- });
2366
-
2367
- document.getElementById('session-create-btn').addEventListener('click', async () => {
2368
- const project = document.getElementById('session-project').value.trim();
2369
- const summary = document.getElementById('session-summary').value.trim();
2370
-
2371
- if (!project) {
2372
- document.getElementById('session-project').focus();
2373
- return;
2374
- }
2375
-
2376
- const resp = await fetch('/api/sessions/create', {
2377
- method: 'POST',
2378
- headers: { 'Content-Type': 'application/json' },
2379
- body: JSON.stringify({ project, summary }),
2380
- });
2381
-
2382
- const result = await resp.json();
2383
- if (result.ok) {
2384
- closeSessionCreate();
2385
- await refreshTree();
2386
- loadNote(result.path);
2387
- }
2388
- });
2389
-
2390
- // --- Sidebar pillar buttons ---
2391
- document.getElementById('new-session-btn').addEventListener('click', () => openSessionCreate(''));
2392
-
2393
- document.getElementById('new-project-sidebar-btn').addEventListener('click', () => {
2394
- document.getElementById('project-create-overlay').classList.add('active');
2395
- document.getElementById('project-name-input').value = '';
2396
- document.getElementById('project-overview-input').value = '';
2397
- document.getElementById('project-name-input').focus();
2398
- });
2399
-
2400
- // --- Project Management ---
2401
- function closeProjectCreate() {
2402
- document.getElementById('project-create-overlay').classList.remove('active');
2403
- }
2404
-
2405
- document.getElementById('new-project-btn').addEventListener('click', () => {
2406
- document.getElementById('project-create-overlay').classList.add('active');
2407
- document.getElementById('project-name-input').value = '';
2408
- document.getElementById('project-overview-input').value = '';
2409
- document.getElementById('project-name-input').focus();
2410
- });
2411
-
2412
- document.getElementById('project-cancel').addEventListener('click', closeProjectCreate);
2413
- document.getElementById('project-create-overlay').addEventListener('click', (e) => {
2414
- if (e.target === e.currentTarget) closeProjectCreate();
2415
- });
2416
-
2417
- document.getElementById('project-create-btn').addEventListener('click', async () => {
2418
- const name = document.getElementById('project-name-input').value.trim();
2419
- const overview = document.getElementById('project-overview-input').value.trim();
2420
- if (!name) {
2421
- document.getElementById('project-name-input').focus();
2422
- return;
2423
- }
2424
- const resp = await fetch('/api/projects/create', {
2425
- method: 'POST',
2426
- headers: { 'Content-Type': 'application/json' },
2427
- body: JSON.stringify({ name, overview }),
2428
- });
2429
- const result = await resp.json();
2430
- if (result.ok) {
2431
- closeProjectCreate();
2432
- await refreshTree();
2433
- loadNote(result.path);
2434
- }
2435
- });
2436
-
2437
- async function refreshTree() {
2438
- const [rawTreeData, sessionsData, stats] = await Promise.all([
2439
- fetchJSON('/api/tree'),
2440
- fetchJSON('/api/sessions'),
2441
- fetchJSON('/api/stats'),
2442
- ]);
2443
-
2444
- allNotes = {};
2445
- function walk(node) {
2446
- if (node.type === 'note') {
2447
- allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
2448
- }
2449
- (node.children || []).forEach(walk);
2450
- }
2451
- walk(rawTreeData);
2452
-
2453
- const treeEl = document.getElementById('file-tree');
2454
- treeEl.innerHTML = '';
2455
- renderTree(rawTreeData, treeEl);
2456
-
2457
- renderSessionTree(sessionsData);
2458
- renderTagCloud(collectAllTags());
1120
+ activeTagFilters.add(el.dataset.tag);
1121
+ applyTagFilter();
1122
+ });
1123
+ });
2459
1124
 
2460
- document.getElementById('stats-bar').innerHTML = `
2461
- <span><span class="stat-val">${stats.notes}</span> notes</span>
2462
- <span><span class="stat-val">${stats.folders}</span> folders</span>
2463
- <span><span class="stat-val">${stats.tags}</span> tags</span>
2464
- <span><span class="stat-val">${stats.links}</span> links</span>
2465
- `;
1125
+ // Wire edit button
1126
+ area.querySelector('#note-edit-btn')?.addEventListener('click', () => openEditor(note.path));
2466
1127
 
2467
- markActiveItems();
1128
+ // Render rail
1129
+ if (railOpen) renderRail(note);
2468
1130
  }
2469
1131
 
2470
- function markActiveItems() {
2471
- if (!currentPath) return;
2472
- document.querySelectorAll('.tree-item[data-path]').forEach(el => {
2473
- el.classList.toggle('active', el.dataset.path === currentPath);
1132
+ // ─── Right Rail ──────────────────────────────────────────────────────────────
1133
+ function renderRail(note) {
1134
+ const rail = $('note-rail');
1135
+ if (!rail) return;
1136
+ rail.innerHTML = '';
1137
+
1138
+ // Local graph
1139
+ const graphCard = createRailCard('local graph', null, {
1140
+ right: `<div style="display:flex;gap:2px"><button class="rail-icon-btn" id="rail-graph-expand" title="Open full graph">⤢</button></div>`
2474
1141
  });
2475
- document.querySelectorAll('.session-item[data-path]').forEach(el => {
2476
- el.classList.toggle('active', el.dataset.path === currentPath);
1142
+ const graphBody = graphCard.querySelector('.rail-card-body');
1143
+ graphBody.style.cssText = 'position:relative;height:240px;padding:0;overflow:hidden;';
1144
+ renderLocalGraph(graphBody, note);
1145
+ rail.appendChild(graphCard);
1146
+
1147
+ graphCard.querySelector('#rail-graph-expand')?.addEventListener('click', () => {
1148
+ _graphData = null;
1149
+ setView('graph');
1150
+ renderGraphView();
1151
+ });
1152
+
1153
+ // Outline
1154
+ const headings = [];
1155
+ (note.content || '').split('\n').forEach(line => {
1156
+ const m = line.match(/^(#{1,3})\s+(.+)/);
1157
+ if (m) headings.push({ level: m[1].length, text: m[2].trim() });
2477
1158
  });
1159
+ if (headings.length > 1) {
1160
+ const outCard = createRailCard(`outline <span class="dim tab-nums" style="margin-left:6px">${headings.length}</span>`, null, { collapsible: true });
1161
+ const outBody = outCard.querySelector('.rail-card-body');
1162
+ outBody.style.cssText = 'display:flex;flex-direction:column;gap:2px;';
1163
+ headings.forEach(h => {
1164
+ const el = document.createElement('div');
1165
+ el.className = 'outline-item' + (h.level === 1 ? ' lv1' : '');
1166
+ el.style.paddingLeft = (4 + (h.level - 1) * 12) + 'px';
1167
+ el.textContent = h.text;
1168
+ el.addEventListener('click', () => {
1169
+ const target = Array.from(document.querySelectorAll('.md-body h1,.md-body h2,.md-body h3')).find(
1170
+ el => el.textContent.trim() === h.text
1171
+ );
1172
+ if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1173
+ });
1174
+ outBody.appendChild(el);
1175
+ });
1176
+ rail.appendChild(outCard);
1177
+ }
1178
+
1179
+ // Backlinks
1180
+ const backlinks = note.backlinks || [];
1181
+ if (backlinks.length > 0) {
1182
+ const blCard = createRailCard(`backlinks <span class="dim tab-nums" style="margin-left:6px">${backlinks.length}</span>`, null, { collapsible: true });
1183
+ const blBody = blCard.querySelector('.rail-card-body');
1184
+ backlinks.forEach(bl => {
1185
+ const item = document.createElement('div');
1186
+ item.className = 'bl-item';
1187
+ item.innerHTML = `<span class="dim">←</span><span>${bl.title || bl.path}</span>`;
1188
+ item.addEventListener('click', () => loadNote(bl.path));
1189
+ blBody.appendChild(item);
1190
+ });
1191
+ rail.appendChild(blCard);
1192
+ }
2478
1193
  }
2479
1194
 
2480
- // --- Graph ---
2481
- function renderGraph(note) {
2482
- const container = document.getElementById('graph-container');
2483
- container.innerHTML = '';
1195
+ function createRailCard(title, content, opts = {}) {
1196
+ const card = document.createElement('div');
1197
+ card.className = 'rail-card';
1198
+ let collapseHtml = '';
1199
+ if (opts.collapsible) collapseHtml = '<span class="side-collapse-arrow" style="margin-right:4px">▾</span>';
1200
+ card.innerHTML = `
1201
+ <div class="rail-card-header">
1202
+ <button class="rail-card-title">${collapseHtml}<span>${title}</span></button>
1203
+ ${opts.right || ''}
1204
+ </div>
1205
+ <div class="rail-card-body">${content || ''}</div>
1206
+ `;
1207
+ if (opts.collapsible) {
1208
+ const titleBtn = card.querySelector('.rail-card-title');
1209
+ const body = card.querySelector('.rail-card-body');
1210
+ const arrow = card.querySelector('.side-collapse-arrow');
1211
+ titleBtn.addEventListener('click', () => {
1212
+ const open = body.style.display !== 'none';
1213
+ body.style.display = open ? 'none' : '';
1214
+ arrow.classList.toggle('collapsed', open);
1215
+ });
1216
+ }
1217
+ return card;
1218
+ }
2484
1219
 
1220
+ // ─── Local Graph ─────────────────────────────────────────────────────────────
1221
+ function renderLocalGraph(container, note) {
2485
1222
  const nodes = new Map();
2486
1223
  const links = [];
2487
1224
 
2488
- nodes.set(note.path, { id: note.path, title: note.title, active: true, tags: note.tags || [] });
2489
-
2490
- const projectPrefix = note.path.includes('/') ? note.path.split('/')[0] + '/' : '';
2491
- const inSameProject = (p) => !projectPrefix ? !p.includes('/') : p.startsWith(projectPrefix);
1225
+ nodes.set(note.path, { id: note.path, title: note.title, kind: 'note', center: true });
2492
1226
 
2493
1227
  (note.links || []).forEach(link => {
2494
1228
  const path = findNotePath(link);
2495
- if (path && inSameProject(path)) {
2496
- if (!nodes.has(path)) {
2497
- const n = allNotes[path];
2498
- nodes.set(path, { id: path, title: n ? n.title : link, active: false, tags: n ? n.tags : [] });
2499
- }
1229
+ if (path && !nodes.has(path)) {
1230
+ nodes.set(path, { id: path, title: allNotes[path]?.title || link, kind: 'note', center: false });
1231
+ links.push({ source: note.path, target: path });
1232
+ } else if (path) {
2500
1233
  links.push({ source: note.path, target: path });
2501
1234
  }
2502
1235
  });
2503
1236
 
2504
1237
  (note.backlinks || []).forEach(bl => {
2505
- const blPath = typeof bl === 'string' ? bl : (bl && bl.path);
2506
- if (!blPath) return;
2507
- if (inSameProject(blPath)) {
2508
- if (!nodes.has(blPath)) {
2509
- const n = allNotes[blPath];
2510
- const title = (bl && typeof bl === 'object' && bl.title) ? bl.title : (n ? n.title : blPath);
2511
- nodes.set(blPath, { id: blPath, title, active: false, tags: n ? n.tags : [] });
2512
- }
2513
- links.push({ source: blPath, target: note.path });
1238
+ const p = bl.path;
1239
+ if (p && !nodes.has(p)) {
1240
+ nodes.set(p, { id: p, title: bl.title || p, kind: isSessionPath(p) ? 'session' : 'note', center: false });
2514
1241
  }
1242
+ if (p) links.push({ source: p, target: note.path });
2515
1243
  });
2516
1244
 
2517
- (note.related || []).forEach(r => {
2518
- if (r.path && inSameProject(r.path)) {
2519
- if (!nodes.has(r.path)) {
2520
- const n = allNotes[r.path];
2521
- nodes.set(r.path, { id: r.path, title: r.title, active: false, tags: n ? n.tags : [] });
2522
- }
2523
- links.push({ source: note.path, target: r.path, dashed: true });
1245
+ (note.related || []).slice(0, 5).forEach(r => {
1246
+ if (r.path && !nodes.has(r.path)) {
1247
+ nodes.set(r.path, { id: r.path, title: r.title, kind: isSessionPath(r.path) ? 'session' : 'note', center: false });
2524
1248
  }
1249
+ if (r.path) links.push({ source: note.path, target: r.path, session: isSessionPath(r.path) });
2525
1250
  });
2526
1251
 
2527
1252
  const nodeArray = Array.from(nodes.values());
2528
- nodeArray.forEach(n => n.degree = 0);
2529
- links.forEach(l => {
2530
- const s = nodes.get(l.source);
2531
- const t = nodes.get(l.target);
2532
- if (s) s.degree++;
2533
- if (t) t.degree++;
2534
- });
2535
-
2536
1253
  if (nodeArray.length < 2) {
2537
- container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:10px;font-family:var(--font-mono)">No connections</div>';
1254
+ container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--dim);font-size:var(--fz-xs)">No connections</div>';
2538
1255
  return;
2539
1256
  }
2540
1257
 
2541
- const isMaximized = document.getElementById('graph-section').classList.contains('maximized');
2542
- const width = container.clientWidth || 248;
2543
- const height = container.clientHeight || 200;
2544
- const margin = isMaximized ? 60 : 24;
2545
-
1258
+ const width = 100, height = 100;
2546
1259
  const svg = d3.select(container).append('svg')
2547
- .attr('viewBox', `0 0 ${width} ${height}`);
2548
-
2549
- const g = svg.append('g');
2550
-
2551
- svg.call(d3.zoom()
2552
- .scaleExtent([0.2, 4])
2553
- .on('zoom', (event) => g.attr('transform', event.transform))
2554
- );
1260
+ .attr('viewBox', '0 0 100 100')
1261
+ .attr('preserveAspectRatio', 'xMidYMid meet')
1262
+ .style('width', '100%').style('height', '100%');
2555
1263
 
2556
1264
  const simulation = d3.forceSimulation(nodeArray)
2557
- .force('link', d3.forceLink(links).id(d => d.id).distance(isMaximized ? 150 : 80))
2558
- .force('charge', d3.forceManyBody().strength(d => isMaximized ? -300 - (d.degree * 40) : -100 - (d.degree * 20)))
2559
- .force('center', d3.forceCenter(width / 2, height / 2))
2560
- .force('collision', d3.forceCollide().radius(d => {
2561
- const textWidth = d.title.length * (isMaximized ? 6 : 4.5);
2562
- return (isMaximized ? textWidth + 10 : 12) + (d.degree * 2);
2563
- }));
2564
-
2565
- const linkEl = g.selectAll('.graph-link')
2566
- .data(links).enter().append('line')
2567
- .attr('class', 'graph-link')
2568
- .attr('stroke-dasharray', d => d.dashed ? '3,3' : null)
2569
- .style('opacity', d => d.dashed ? 0.3 : 0.6);
2570
-
2571
- const node = g.selectAll('.graph-node')
2572
- .data(nodeArray).enter().append('g')
2573
- .attr('class', d => 'graph-node' + (d.active ? ' active' : ''))
2574
- .on('click', (e, d) => {
2575
- e.stopPropagation();
2576
- loadNote(d.id);
2577
- if (isMaximized) {
2578
- document.getElementById('graph-section').classList.remove('maximized');
2579
- isGraphMaximized = false;
2580
- renderGraph(currentNote);
2581
- }
2582
- })
2583
- .call(d3.drag()
2584
- .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
2585
- .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
2586
- .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
2587
- );
2588
-
2589
- node.append('circle').attr('r', d => d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2)));
2590
- node.append('text').text(d => d.title).attr('dx', d => (d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2))) + 6).attr('dy', 3);
2591
-
2592
- if (isMaximized) {
2593
- node.append('text')
2594
- .text(d => {
2595
- let hash = 0;
2596
- for (let i = 0; i < d.id.length; i++) hash = Math.imul(31, hash) + d.id.charCodeAt(i) | 0;
2597
- const v1 = ((Math.abs(hash) % 1000) / 1000).toFixed(3);
2598
- const v2 = ((Math.abs(hash * 13) % 1000) / 1000).toFixed(3);
2599
- const v3 = ((Math.abs(hash * 17) % 1000) / 1000).toFixed(3);
2600
- return `[${v1}, ${v2}, ${v3}, ...]`;
2601
- })
2602
- .attr('dx', d => (d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2))) + 6)
2603
- .attr('dy', 16)
2604
- .style('fill', 'var(--neon-purple)')
2605
- .style('font-size', '8px')
2606
- .style('opacity', '0.8')
2607
- .style('pointer-events', 'none')
2608
- .style('font-family', 'var(--font-mono)');
2609
-
2610
- node.append('text')
2611
- .text(d => d.id ? d.id.split('/').slice(0, -1).join('/') : '')
2612
- .attr('dx', d => (d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2))) + 6)
2613
- .attr('dy', 26)
2614
- .style('fill', 'var(--text-muted)')
2615
- .style('font-size', '8px')
2616
- .style('pointer-events', 'none');
2617
- }
1265
+ .force('link', d3.forceLink(links).id(d => d.id).distance(25))
1266
+ .force('charge', d3.forceManyBody().strength(-80))
1267
+ .force('center', d3.forceCenter(50, 50))
1268
+ .force('collision', d3.forceCollide(8));
2618
1269
 
2619
1270
  simulation.on('tick', () => {
2620
- linkEl
2621
- .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
2622
- .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
2623
- node.attr('transform', d => {
2624
- d.x = Math.max(margin, Math.min(width - margin, d.x));
2625
- d.y = Math.max(margin, Math.min(height - margin, d.y));
2626
- return `translate(${d.x},${d.y})`;
1271
+ nodeArray.forEach(d => {
1272
+ d.x = Math.max(10, Math.min(90, d.x));
1273
+ d.y = Math.max(10, Math.min(90, d.y));
2627
1274
  });
2628
1275
  });
2629
- }
2630
-
2631
- // --- Graph Resize ---
2632
- let graphHeight = parseInt(localStorage.getItem('kyp-graph-h')) || 200;
2633
- let isGraphMaximized = false;
2634
1276
 
2635
- function initGraphResize() {
2636
- const container = document.getElementById('graph-container');
2637
- const section = document.getElementById('graph-section');
2638
- container.style.height = graphHeight + 'px';
1277
+ // Run simulation
1278
+ for (let i = 0; i < 120; i++) simulation.tick();
1279
+ simulation.stop();
2639
1280
 
2640
- document.getElementById('graph-max').addEventListener('click', () => {
2641
- isGraphMaximized = !isGraphMaximized;
2642
- section.classList.toggle('maximized', isGraphMaximized);
2643
- if (graphVisible && currentNote) renderGraph(currentNote);
1281
+ // Draw edges
1282
+ links.forEach(l => {
1283
+ const s = typeof l.source === 'object' ? l.source : nodeArray.find(n => n.id === l.source);
1284
+ const t = typeof l.target === 'object' ? l.target : nodeArray.find(n => n.id === l.target);
1285
+ if (!s || !t) return;
1286
+ const isSession = l.session || s.kind === 'session' || t.kind === 'session';
1287
+ svg.append('line')
1288
+ .attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
1289
+ .attr('stroke', 'var(--line-2)')
1290
+ .attr('stroke-width', 0.22)
1291
+ .attr('stroke-opacity', isSession ? 0.5 : 0.85)
1292
+ .attr('stroke-dasharray', isSession ? '0.9 0.9' : '')
1293
+ .attr('stroke-linecap', 'round');
2644
1294
  });
2645
1295
 
2646
- document.getElementById('graph-shrink').addEventListener('click', () => {
2647
- graphHeight = Math.max(100, graphHeight - 50);
2648
- container.style.height = graphHeight + 'px';
2649
- localStorage.setItem('kyp-graph-h', graphHeight);
2650
- if (graphVisible && currentNote) renderGraph(currentNote);
2651
- });
1296
+ // Draw nodes
1297
+ nodeArray.forEach(n => {
1298
+ const r = n.center ? 5 : (n.kind === 'session' ? 2.2 : 3.2);
1299
+ const g = svg.append('g').style('cursor', 'pointer');
1300
+
1301
+ if (n.center) {
1302
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r * 1.9)
1303
+ .attr('fill', 'var(--accent)').attr('fill-opacity', 0.18);
1304
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1305
+ .attr('fill', 'var(--accent)');
1306
+ g.append('text').attr('x', n.x).attr('y', n.y + r + 6)
1307
+ .attr('text-anchor', 'middle').attr('font-size', 2.5).attr('font-weight', 500)
1308
+ .attr('fill', 'var(--accent)')
1309
+ .style('font-family', 'JetBrains Mono, monospace')
1310
+ .text(n.title.length > 24 ? n.title.substring(0, 22) + '…' : n.title);
1311
+ } else if (n.kind === 'session') {
1312
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1313
+ .attr('fill', 'var(--bg)').attr('stroke', 'var(--muted)').attr('stroke-width', 0.35);
1314
+ } else {
1315
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1316
+ .attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 0.5);
1317
+ }
2652
1318
 
2653
- document.getElementById('graph-grow').addEventListener('click', () => {
2654
- graphHeight = Math.min(600, graphHeight + 50);
2655
- container.style.height = graphHeight + 'px';
2656
- localStorage.setItem('kyp-graph-h', graphHeight);
2657
- if (graphVisible && currentNote) renderGraph(currentNote);
1319
+ g.on('click', () => { if (!n.center) loadNote(n.id); });
2658
1320
  });
1321
+ }
2659
1322
 
2660
- const handle = document.getElementById('graph-resize-handle');
2661
- handle.addEventListener('mousedown', (e) => {
2662
- e.preventDefault();
2663
- const startY = e.clientY;
2664
- const startH = graphHeight;
2665
- document.body.style.cursor = 'ns-resize';
2666
- document.body.style.userSelect = 'none';
2667
-
2668
- function onMove(ev) {
2669
- graphHeight = Math.max(100, Math.min(600, startH + (ev.clientY - startY)));
2670
- container.style.height = graphHeight + 'px';
2671
- }
1323
+ // ─── Session View ────────────────────────────────────────────────────────────
1324
+ function extractSection(content, sectionName) {
1325
+ const idx = content.indexOf(`## ${sectionName}`);
1326
+ if (idx === -1) return '';
1327
+ const nextSec = content.indexOf('\n## ', idx + 5);
1328
+ const block = nextSec !== -1 ? content.substring(idx, nextSec) : content.substring(idx);
1329
+ return block.replace(`## ${sectionName}`, '').trim();
1330
+ }
2672
1331
 
2673
- function onUp() {
2674
- document.body.style.cursor = '';
2675
- document.body.style.userSelect = '';
2676
- localStorage.setItem('kyp-graph-h', graphHeight);
2677
- if (graphVisible && currentNote) renderGraph(currentNote);
2678
- document.removeEventListener('mousemove', onMove);
2679
- document.removeEventListener('mouseup', onUp);
2680
- }
1332
+ function renderSessionView(note) {
1333
+ const area = $('content-area');
1334
+ const content = note.content || '';
2681
1335
 
2682
- document.addEventListener('mousemove', onMove);
2683
- document.addEventListener('mouseup', onUp);
2684
- });
2685
- }
1336
+ // Extract summary
1337
+ let summary = extractSection(content, 'Summary');
2686
1338
 
2687
- // --- Search ---
2688
- let searchTimeout;
2689
- const searchInput = document.getElementById('search-input');
2690
- const searchResults = document.getElementById('search-results');
1339
+ // Extract session ID
1340
+ const pathParts = note.path.split('/');
1341
+ const sessionFile = pathParts[pathParts.length - 1].replace('.md', '');
1342
+ const projectName = pathParts[0] || '';
2691
1343
 
2692
- searchInput.addEventListener('input', () => {
2693
- clearTimeout(searchTimeout);
2694
- const q = searchInput.value.trim();
2695
- if (!q) { searchResults.classList.remove('active'); return; }
2696
- searchTimeout = setTimeout(async () => {
2697
- const results = await fetchJSON(`/api/search?q=${encodeURIComponent(q)}`);
2698
- searchResults.innerHTML = '';
2699
- if (results.length === 0) {
2700
- searchResults.innerHTML = '<div class="search-result"><div class="sr-title" style="color:var(--text-muted)">No results</div></div>';
2701
- } else {
2702
- results.forEach(r => {
2703
- const div = document.createElement('div');
2704
- div.className = 'search-result';
2705
- 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>` : ''}`;
2706
- div.addEventListener('click', () => {
2707
- loadNote(r.path);
2708
- searchResults.classList.remove('active');
2709
- searchInput.value = '';
2710
- });
2711
- searchResults.appendChild(div);
2712
- });
2713
- }
2714
- searchResults.classList.add('active');
2715
- }, 200);
2716
- });
1344
+ const wordCount = content.split(/\s+/).filter(Boolean).length;
2717
1345
 
2718
- searchInput.addEventListener('focus', () => {
2719
- document.querySelector('.search-hint').style.display = 'none';
2720
- });
1346
+ // Extract each section
1347
+ const promptsRaw = extractSection(content, 'PROMPTS');
1348
+ const investigated = extractSection(content, 'INVESTIGATED');
1349
+ const learned = extractSection(content, 'LEARNED');
1350
+ const completed = extractSection(content, 'COMPLETED');
1351
+ const nextSteps = extractSection(content, 'NEXT STEPS');
2721
1352
 
2722
- searchInput.addEventListener('blur', () => {
2723
- if (!searchInput.value) document.querySelector('.search-hint').style.display = '';
2724
- });
1353
+ const sectionNames = ['Summary', 'PROMPTS', 'INVESTIGATED', 'LEARNED', 'COMPLETED', 'NEXT STEPS'];
1354
+ const presentSections = sectionNames.filter(s => content.includes(`## ${s}`));
2725
1355
 
2726
- document.addEventListener('click', (e) => {
2727
- if (!e.target.closest('.search-box')) searchResults.classList.remove('active');
2728
- });
1356
+ // Count prompt entries
1357
+ const promptEntries = promptsRaw.split(/^###\s+/m).filter(p => p.trim());
1358
+ const hasMultiplePrompts = promptEntries.length > 1;
2729
1359
 
2730
- // --- Tag Filter ---
2731
- document.getElementById('tag-filter-toggle').addEventListener('click', () => {
2732
- const body = document.getElementById('tag-filter-body');
2733
- const arrow = document.getElementById('tag-arrow');
2734
- body.classList.toggle('collapsed');
2735
- arrow.classList.toggle('open');
2736
- });
1360
+ const tagsHtml = (note.tags || ['session', 'auto-captured']).map(t =>
1361
+ `<button class="tag-chip"><span class="tc-hash">#</span>${t}</button>`
1362
+ ).join('');
2737
1363
 
2738
- document.getElementById('clear-filters').addEventListener('click', () => {
2739
- activeTagFilters.clear();
2740
- applyTagFilter();
2741
- });
1364
+ area.innerHTML = `
1365
+ <article class="session-view">
1366
+ <div style="display:flex;justify-content:space-between;align-items:center;padding:2px 0 14px">
1367
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1368
+ <span class="session-status done"><i style="width:6px;height:6px;border-radius:999px;background:var(--muted);display:inline-block"></i> done</span>
1369
+ ${tagsHtml}
1370
+ <span class="dim" style="font-size:var(--fz-xs);margin-left:8px">${note.created || sessionFile}</span>
1371
+ </div>
1372
+ <button class="ghost-btn" onclick="openEditor('${note.path}')">⌥ edit</button>
1373
+ </div>
1374
+ <h1 style="margin:12px 0 4px;font-size:calc(var(--fz-xl) + 6px);font-weight:500;letter-spacing:-0.01em">
1375
+ Session ${sessionFile}
1376
+ </h1>
1377
+ <div class="dim" style="font-size:var(--fz-xs);margin-bottom:22px;display:flex;gap:14px">
1378
+ <span>words <span class="mut tab-nums">${wordCount.toLocaleString()}</span></span>
1379
+ <span>·</span>
1380
+ <span>project <span class="acc">${projectName}</span></span>
1381
+ <span>·</span>
1382
+ <span>sections <span class="mut tab-nums">${presentSections.length}</span></span>
1383
+ </div>
1384
+ ${summary ? `
1385
+ <div class="session-section">
1386
+ <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Summary</div>
1387
+ <div class="ss-body md-body">${marked.parse(summary)}</div>
1388
+ </div>` : ''}
1389
+ ${promptsRaw ? `
1390
+ <div class="session-section">
1391
+ <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Prompts <span class="dim tab-nums" style="font-size:var(--fz-xs);margin-left:6px">${promptEntries.length}</span></div>
1392
+ <div class="ss-body prompts-list${hasMultiplePrompts ? ' scrollable' : ''}" id="session-prompts"></div>
1393
+ </div>` : ''}
1394
+ ${investigated ? `
1395
+ <div class="session-section">
1396
+ <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Investigated</div>
1397
+ <div class="ss-body md-body">${marked.parse(investigated)}</div>
1398
+ </div>` : ''}
1399
+ ${learned ? `
1400
+ <div class="session-section">
1401
+ <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Learned</div>
1402
+ <div class="ss-body md-body">${marked.parse(learned)}</div>
1403
+ </div>` : ''}
1404
+ ${completed ? `
1405
+ <div class="session-section">
1406
+ <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Completed</div>
1407
+ <div class="ss-body md-body">${marked.parse(completed)}</div>
1408
+ </div>` : ''}
1409
+ ${nextSteps ? `
1410
+ <div class="session-section">
1411
+ <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Next Steps</div>
1412
+ <div class="ss-body md-body">${marked.parse(nextSteps)}</div>
1413
+ </div>` : ''}
1414
+ <div style="height:60px"></div>
1415
+ </article>
1416
+ `;
2742
1417
 
2743
- function renderTagCloud(tags) {
2744
- const cloud = document.getElementById('tag-cloud');
2745
- cloud.innerHTML = '';
2746
- const sorted = Object.entries(tags).sort((a, b) => b[1] - a[1]);
2747
- sorted.forEach(([tag, count]) => {
2748
- const chip = document.createElement('span');
2749
- chip.className = 'tag-chip' + (activeTagFilters.has(tag) ? ' selected' : '');
2750
- chip.innerHTML = `#${tag}<span class="tag-count">${count}</span>`;
2751
- chip.addEventListener('click', () => {
2752
- if (activeTagFilters.has(tag)) {
2753
- activeTagFilters.delete(tag);
2754
- } else {
2755
- activeTagFilters.add(tag);
2756
- }
2757
- applyTagFilter();
1418
+ // Render prompts with scrollable container
1419
+ const promptsContainer = $('session-prompts');
1420
+ if (promptsContainer && promptEntries.length > 0) {
1421
+ promptEntries.forEach(entry => {
1422
+ const div = document.createElement('div');
1423
+ div.className = 'prompt-entry';
1424
+ div.innerHTML = marked.parse('### ' + entry.trim());
1425
+ promptsContainer.appendChild(div);
2758
1426
  });
2759
- cloud.appendChild(chip);
2760
- });
1427
+ }
2761
1428
  }
2762
1429
 
2763
- function applyTagFilter() {
2764
- const activeEl = document.getElementById('active-tags');
2765
- const filterInfo = document.getElementById('filter-info');
2766
- const filterCount = document.getElementById('filter-count');
2767
-
2768
- activeEl.innerHTML = '';
2769
- activeTagFilters.forEach(tag => {
2770
- const el = document.createElement('span');
2771
- el.className = 'active-tag';
2772
- el.innerHTML = `#${tag}<span class="remove">&times;</span>`;
2773
- el.addEventListener('click', () => {
2774
- activeTagFilters.delete(tag);
2775
- applyTagFilter();
1430
+ // ─── Graph View ──────────────────────────────────────────────────────────────
1431
+ function renderGraphView() {
1432
+ const area = $('content-area');
1433
+ area.innerHTML = `
1434
+ <div class="graph-view">
1435
+ <section class="graph-canvas">
1436
+ <div class="graph-header">
1437
+ <div class="graph-header-left">
1438
+ <span class="acc">▦</span>
1439
+ <span style="font-weight:500">vault graph</span>
1440
+ <span class="dim tab-nums" id="graph-stats"></span>
1441
+ </div>
1442
+ <div class="graph-header-right">
1443
+ <button class="tool-chip active">force</button>
1444
+ <button class="tool-chip">radial</button>
1445
+ <button class="tool-chip">time</button>
1446
+ <button class="ghost-btn" id="graph-close">✕</button>
1447
+ </div>
1448
+ </div>
1449
+ <div class="graph-svg-wrap" id="graph-svg-wrap"></div>
1450
+ <div class="graph-legend">
1451
+ <div class="graph-legend-title">legend</div>
1452
+ <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--accent)"></i>notes</div>
1453
+ <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--panel-2);border:1px solid var(--muted)"></i>sessions</div>
1454
+ <div class="graph-legend-item"><i style="width:12px;height:1px;background:var(--line-2)"></i>link</div>
1455
+ <div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed var(--line-2)"></i>session ref</div>
1456
+ </div>
1457
+ </section>
1458
+ <aside class="graph-rail" id="graph-rail">
1459
+ <div class="rail-card">
1460
+ <div class="rail-card-header"><span class="rail-card-title"><span>focused</span></span></div>
1461
+ <div class="rail-card-body" id="graph-focused">
1462
+ <div class="dim" style="font-size:var(--fz-xs)">Hover or click a node</div>
1463
+ </div>
1464
+ </div>
1465
+ <div class="rail-card">
1466
+ <div class="rail-card-header"><span class="rail-card-title"><span>connections</span></span></div>
1467
+ <div class="rail-card-body" id="graph-connections"></div>
1468
+ </div>
1469
+ </aside>
1470
+ </div>
1471
+ `;
1472
+
1473
+ $('graph-close').addEventListener('click', () => setView('note'));
1474
+
1475
+ // Layout toggle buttons
1476
+ const chips = document.querySelectorAll('.graph-header-right .tool-chip');
1477
+ chips.forEach(chip => {
1478
+ chip.addEventListener('click', () => {
1479
+ chips.forEach(c => c.classList.remove('active'));
1480
+ chip.classList.add('active');
1481
+ buildFullGraph(chip.textContent.trim());
2776
1482
  });
2777
- activeEl.appendChild(el);
2778
1483
  });
2779
1484
 
2780
- document.querySelectorAll('.tag-chip').forEach(chip => {
2781
- const tag = chip.textContent.replace('#', '').replace(/\d+$/, '');
2782
- chip.classList.toggle('selected', activeTagFilters.has(tag));
2783
- });
1485
+ buildFullGraph('force');
1486
+ }
2784
1487
 
2785
- const treeItems = document.querySelectorAll('.tree-item[data-path]');
2786
- let visibleCount = 0;
1488
+ async function buildFullGraph(layout) {
1489
+ const wrap = $('graph-svg-wrap');
1490
+ if (!wrap) return;
1491
+ wrap.innerHTML = '';
2787
1492
 
2788
- if (activeTagFilters.size === 0) {
2789
- treeItems.forEach(el => { el.style.display = ''; });
2790
- document.querySelectorAll('.tree-children').forEach(el => el.classList.remove('collapsed'));
2791
- filterInfo.style.display = 'none';
2792
- renderTagCloud(collectAllTags());
2793
- return;
1493
+ if (!_graphData) {
1494
+ _graphData = await fetchJSON('/api/graph');
2794
1495
  }
2795
1496
 
2796
- const matchingPaths = new Set();
1497
+ const nodes = _graphData.nodes.map(n => ({ ...n }));
1498
+ const links = _graphData.edges.map(e => ({ ...e }));
1499
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
2797
1500
 
2798
- for (const [path, note] of Object.entries(allNotes)) {
2799
- const noteTags = (note.tags || []).map(t => t.toLowerCase());
2800
- const matches = [...activeTagFilters].every(f => noteTags.includes(f.toLowerCase()));
2801
- if (matches) matchingPaths.add(path);
1501
+ $('graph-stats').textContent = `${nodes.length}n · ${links.length}e`;
1502
+ if (nodes.length === 0) return;
1503
+
1504
+ const rect = wrap.getBoundingClientRect();
1505
+ const W = rect.width || 800;
1506
+ const H = rect.height || 500;
1507
+
1508
+ const svg = d3.select(wrap).append('svg')
1509
+ .attr('viewBox', `0 0 ${W} ${H}`)
1510
+ .attr('preserveAspectRatio', 'xMidYMid meet');
1511
+
1512
+ const defs = svg.append('defs');
1513
+ const pattern = defs.append('pattern').attr('id', 'dotgrid').attr('width', 20).attr('height', 20).attr('patternUnits', 'userSpaceOnUse');
1514
+ pattern.append('circle').attr('cx', 10).attr('cy', 10).attr('r', 0.8).attr('fill', 'var(--line)');
1515
+ svg.append('rect').attr('width', W).attr('height', H).attr('fill', 'url(#dotgrid)');
1516
+
1517
+ const g = svg.append('g');
1518
+ const zoom = d3.zoom().scaleExtent([0.3, 5]).on('zoom', e => g.attr('transform', e.transform));
1519
+ svg.call(zoom);
1520
+
1521
+ // Compute positions based on layout
1522
+ const noteNodes = nodes.filter(n => n.kind === 'note');
1523
+ const sessionNodes = nodes.filter(n => n.kind === 'session');
1524
+ const cx = W / 2, cy = H / 2;
1525
+
1526
+ if (layout === 'radial') {
1527
+ const noteR = Math.min(W, H) * 0.25;
1528
+ const sessionR = Math.min(W, H) * 0.42;
1529
+ noteNodes.forEach((n, i) => {
1530
+ const a = (2 * Math.PI * i) / Math.max(noteNodes.length, 1) - Math.PI / 2;
1531
+ n.x = cx + noteR * Math.cos(a);
1532
+ n.y = cy + noteR * Math.sin(a);
1533
+ });
1534
+ sessionNodes.forEach((n, i) => {
1535
+ const a = (2 * Math.PI * i) / Math.max(sessionNodes.length, 1) - Math.PI / 2;
1536
+ n.x = cx + sessionR * Math.cos(a);
1537
+ n.y = cy + sessionR * Math.sin(a);
1538
+ });
1539
+ } else if (layout === 'time') {
1540
+ const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
1541
+ const pad = 80;
1542
+ const usableW = W - pad * 2;
1543
+ sorted.forEach((n, i) => {
1544
+ const src = nodes.find(x => x.id === n.id);
1545
+ src.x = pad + (usableW * i) / Math.max(sorted.length - 1, 1);
1546
+ src.y = n.kind === 'note' ? cy - 60 : cy + 60;
1547
+ });
1548
+ } else {
1549
+ const sim = d3.forceSimulation(nodes)
1550
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80).strength(0.6))
1551
+ .force('charge', d3.forceManyBody().strength(-200))
1552
+ .force('center', d3.forceCenter(cx, cy))
1553
+ .force('collision', d3.forceCollide(30))
1554
+ .force('x', d3.forceX(cx).strength(0.05))
1555
+ .force('y', d3.forceY(cy).strength(0.05));
1556
+ for (let i = 0; i < 300; i++) sim.tick();
1557
+ sim.stop();
2802
1558
  }
2803
1559
 
2804
- treeItems.forEach(el => {
2805
- const path = el.dataset.path;
2806
- if (matchingPaths.has(path)) {
2807
- el.style.display = '';
2808
- visibleCount++;
1560
+ nodes.forEach(d => {
1561
+ d.x = Math.max(40, Math.min(W - 40, d.x || cx));
1562
+ d.y = Math.max(40, Math.min(H - 40, d.y || cy));
1563
+ });
1564
+
1565
+ // Draw edges
1566
+ const edgeGroup = g.append('g');
1567
+ links.forEach(l => {
1568
+ const s = typeof l.source === 'object' ? l.source : nodeMap.get(l.source);
1569
+ const t = typeof l.target === 'object' ? l.target : nodeMap.get(l.target);
1570
+ if (!s || !t) return;
1571
+ const isSession = s.kind === 'session' || t.kind === 'session';
1572
+ edgeGroup.append('line')
1573
+ .attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
1574
+ .attr('stroke', 'var(--line-2)').attr('stroke-width', 1.2)
1575
+ .attr('stroke-opacity', isSession ? 0.35 : 0.6)
1576
+ .attr('stroke-dasharray', isSession ? '4 3' : '')
1577
+ .attr('stroke-linecap', 'round');
1578
+ });
1579
+
1580
+ // Draw nodes
1581
+ const nodeGroup = g.append('g');
1582
+ nodes.forEach(n => {
1583
+ const r = n.kind === 'session' ? 6 : 10;
1584
+ const ng = nodeGroup.append('g').style('cursor', 'pointer');
1585
+
1586
+ if (n.kind === 'session') {
1587
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1588
+ .attr('fill', 'var(--bg-2)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
2809
1589
  } else {
2810
- el.style.display = 'none';
1590
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r + 4)
1591
+ .attr('fill', 'var(--accent)').attr('fill-opacity', 0.12);
1592
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1593
+ .attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 1.5);
2811
1594
  }
2812
- });
2813
1595
 
2814
- document.querySelectorAll('.tree-children').forEach(el => el.classList.remove('collapsed'));
1596
+ ng.append('text').attr('x', n.x).attr('y', n.y + r + 14)
1597
+ .attr('text-anchor', 'middle').attr('font-size', 11)
1598
+ .attr('fill', n.kind === 'session' ? 'var(--dim)' : 'var(--muted)').attr('opacity', 0.9)
1599
+ .style('font-family', 'JetBrains Mono, monospace')
1600
+ .text(n.title.length > 20 ? n.title.substring(0, 18) + '…' : n.title);
2815
1601
 
2816
- filterInfo.style.display = '';
2817
- filterCount.textContent = `${visibleCount} note${visibleCount !== 1 ? 's' : ''}`;
2818
- renderTagCloud(collectAllTags());
2819
- }
1602
+ ng.on('click', () => {
1603
+ updateGraphRail(n, links, nodes);
1604
+ if (isSessionPath(n.id)) { openSession(n.id); }
1605
+ else { setView('note'); loadNote(n.id); }
1606
+ });
2820
1607
 
2821
- function collectAllTags() {
2822
- const tags = {};
2823
- for (const note of Object.values(allNotes)) {
2824
- (note.tags || []).forEach(t => { tags[t] = (tags[t] || 0) + 1; });
2825
- }
2826
- return tags;
1608
+ ng.on('mouseenter', () => updateGraphRail(n, links, nodes));
1609
+ });
2827
1610
  }
2828
1611
 
2829
- // --- Resizable Panels ---
2830
- function initResize() {
2831
- const layout = document.getElementById('layout');
2832
- const leftHandle = document.getElementById('resize-left');
2833
- const rightHandle = document.getElementById('resize-right');
1612
+ function updateGraphRail(node, links, nodes) {
1613
+ const focused = $('graph-focused');
1614
+ const conns = $('graph-connections');
1615
+ if (!focused || !conns) return;
2834
1616
 
2835
- let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 256;
2836
- let rightW = parseInt(localStorage.getItem('kyp-right-w')) || 272;
1617
+ const neighbors = new Set();
1618
+ links.forEach(l => {
1619
+ const sId = typeof l.source === 'object' ? l.source.id : l.source;
1620
+ const tId = typeof l.target === 'object' ? l.target.id : l.target;
1621
+ if (sId === node.id) neighbors.add(tId);
1622
+ if (tId === node.id) neighbors.add(sId);
1623
+ });
2837
1624
 
2838
- layout.style.setProperty('--sidebar-w', sidebarW + 'px');
2839
- layout.style.setProperty('--right-w', rightW + 'px');
1625
+ const dotColor = node.kind === 'session' ? 'var(--muted)' : 'var(--accent)';
1626
+ focused.innerHTML = `
1627
+ <div style="display:flex;align-items:center;gap:8px">
1628
+ <i style="width:9px;height:9px;border-radius:999px;background:${dotColor}"></i>
1629
+ <span style="font-weight:500">${node.title}</span>
1630
+ </div>
1631
+ <div class="dim" style="font-size:var(--fz-xs);margin-top:6px">degree ${neighbors.size} · ${node.kind}</div>
1632
+ `;
2840
1633
 
2841
- function makeDraggable(handle, opts) {
2842
- handle.addEventListener('mousedown', (e) => {
2843
- e.preventDefault();
2844
- const startX = e.clientX;
2845
- const startW = opts.getWidth();
2846
- handle.classList.add('dragging');
2847
- document.body.classList.add('resizing');
1634
+ conns.innerHTML = '';
1635
+ [...neighbors].forEach(id => {
1636
+ const n = nodes.find(x => x.id === id);
1637
+ if (!n) return;
1638
+ const btn = document.createElement('button');
1639
+ btn.className = 'conn-item';
1640
+ const kindColor = n.kind === 'session' ? 'var(--dim)' : 'var(--accent)';
1641
+ btn.innerHTML = `<i style="width:6px;height:6px;border-radius:999px;background:${kindColor};flex-shrink:0"></i><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${n.title}</span><span class="dim" style="font-size:var(--fz-xs);flex-shrink:0">${n.kind}</span>`;
1642
+ btn.addEventListener('click', () => {
1643
+ if (isSessionPath(n.id)) openSession(n.id);
1644
+ else { setView('note'); loadNote(n.id); }
1645
+ });
1646
+ conns.appendChild(btn);
1647
+ });
1648
+ }
2848
1649
 
2849
- function onMove(e) {
2850
- const delta = e.clientX - startX;
2851
- const adjusted = opts.invert ? startW - delta : startW + delta;
2852
- const clamped = Math.max(opts.min, Math.min(opts.max, adjusted));
2853
- opts.setWidth(clamped);
2854
- }
1650
+ // ─── Editor ──────────────────────────────────────────────────────────────────
1651
+ async function openEditor(path) {
1652
+ const note = await fetchJSON(`/api/note/${path}`);
1653
+ if (note.error) return;
1654
+ editingPath = path;
1655
+ editingNote = note;
1656
+ $('edit-path').textContent = path;
1657
+ $('edit-textarea').value = note.content || '';
1658
+ $('edit-overlay').classList.add('active');
1659
+ $('edit-textarea').focus();
1660
+ }
2855
1661
 
2856
- function onUp() {
2857
- handle.classList.remove('dragging');
2858
- document.body.classList.remove('resizing');
2859
- opts.save();
2860
- if (graphVisible && currentPath) loadNote(currentPath);
2861
- document.removeEventListener('mousemove', onMove);
2862
- document.removeEventListener('mouseup', onUp);
2863
- }
1662
+ function closeEditor() { $('edit-overlay').classList.remove('active'); editingPath = null; }
2864
1663
 
2865
- document.addEventListener('mousemove', onMove);
2866
- document.addEventListener('mouseup', onUp);
2867
- });
1664
+ $('edit-cancel').addEventListener('click', closeEditor);
1665
+ $('edit-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeEditor(); });
1666
+ $('edit-save').addEventListener('click', async () => {
1667
+ if (!editingPath) return;
1668
+ await fetch(`/api/note/${editingPath}`, {
1669
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1670
+ body: JSON.stringify({ content: $('edit-textarea').value, tags: editingNote.tags || [], properties: editingNote.properties || {} }),
1671
+ });
1672
+ closeEditor();
1673
+ await loadNote(editingPath);
1674
+ });
1675
+
1676
+ // ─── Session Create ──────────────────────────────────────────────────────────
1677
+ function openSessionCreate(defaultProject) {
1678
+ const datalist = $('sc-project-list');
1679
+ datalist.innerHTML = '';
1680
+ const projects = new Set();
1681
+ for (const path of Object.keys(allNotes)) {
1682
+ const parts = path.split('/');
1683
+ if (parts.length > 1) projects.add(parts[0]);
1684
+ }
1685
+ for (const p of [...projects].sort()) {
1686
+ const opt = document.createElement('option'); opt.value = p; datalist.appendChild(opt);
2868
1687
  }
1688
+ $('sc-project').value = defaultProject || '';
1689
+ $('sc-summary').value = '';
1690
+ $('session-create-overlay').classList.add('active');
1691
+ (defaultProject ? $('sc-summary') : $('sc-project')).focus();
1692
+ }
1693
+
1694
+ function closeSessionCreate() { $('session-create-overlay').classList.remove('active'); }
1695
+ $('sc-cancel').addEventListener('click', closeSessionCreate);
1696
+ $('session-create-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeSessionCreate(); });
1697
+ $('sc-create').addEventListener('click', async () => {
1698
+ const project = $('sc-project').value.trim();
1699
+ const summary = $('sc-summary').value.trim();
1700
+ if (!project) { $('sc-project').focus(); return; }
1701
+ const resp = await fetch('/api/sessions/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project, summary }) });
1702
+ const result = await resp.json();
1703
+ if (result.ok) { closeSessionCreate(); await refreshTree(); loadNote(result.path); }
1704
+ });
2869
1705
 
2870
- makeDraggable(leftHandle, {
2871
- getWidth: () => sidebarW,
2872
- setWidth: (w) => { sidebarW = w; layout.style.setProperty('--sidebar-w', w + 'px'); },
2873
- save: () => localStorage.setItem('kyp-sidebar-w', sidebarW),
2874
- invert: false,
2875
- min: 180,
2876
- max: 400,
2877
- });
1706
+ // ─── Project Create ──────────────────────────────────────────────────────────
1707
+ function openProjectCreate() {
1708
+ $('pc-name').value = ''; $('pc-overview').value = '';
1709
+ $('project-create-overlay').classList.add('active');
1710
+ $('pc-name').focus();
1711
+ }
1712
+ function closeProjectCreate() { $('project-create-overlay').classList.remove('active'); }
1713
+ $('pc-cancel').addEventListener('click', closeProjectCreate);
1714
+ $('project-create-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeProjectCreate(); });
1715
+ $('new-project-btn').addEventListener('click', openProjectCreate);
1716
+ $('pc-create').addEventListener('click', async () => {
1717
+ const name = $('pc-name').value.trim();
1718
+ const overview = $('pc-overview').value.trim();
1719
+ if (!name) { $('pc-name').focus(); return; }
1720
+ const resp = await fetch('/api/projects/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, overview }) });
1721
+ const result = await resp.json();
1722
+ if (result.ok) { closeProjectCreate(); await refreshTree(); loadNote(result.path); }
1723
+ });
2878
1724
 
2879
- makeDraggable(rightHandle, {
2880
- getWidth: () => rightW,
2881
- setWidth: (w) => { rightW = w; layout.style.setProperty('--right-w', w + 'px'); },
2882
- save: () => localStorage.setItem('kyp-right-w', rightW),
2883
- invert: true,
2884
- min: 200,
2885
- max: 450,
2886
- });
1725
+ // ─── Quick Switcher ──────────────────────────────────────────────────────────
1726
+ function openQuickSwitcher() {
1727
+ $('qs-overlay').classList.add('active');
1728
+ $('qs-input').value = '';
1729
+ $('qs-input').focus();
1730
+ qsSelectedIndex = 0;
1731
+ renderQsResults('');
2887
1732
  }
1733
+ function closeQuickSwitcher() { $('qs-overlay').classList.remove('active'); }
1734
+ $('qs-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeQuickSwitcher(); });
2888
1735
 
2889
- // --- Init ---
2890
- async function init() {
2891
- const [rawTreeData, sessionsData, stats] = await Promise.all([
2892
- fetchJSON('/api/tree'),
2893
- fetchJSON('/api/sessions'),
2894
- fetchJSON('/api/stats'),
2895
- ]);
1736
+ $('qs-input').addEventListener('input', e => { qsSelectedIndex = 0; renderQsResults(e.target.value.trim().toLowerCase()); });
1737
+ $('qs-input').addEventListener('keydown', e => {
1738
+ const items = document.querySelectorAll('.qs-item');
1739
+ if (e.key === 'ArrowDown') { e.preventDefault(); qsSelectedIndex = Math.min(qsSelectedIndex + 1, items.length - 1); updateQsSelection(); }
1740
+ else if (e.key === 'ArrowUp') { e.preventDefault(); qsSelectedIndex = Math.max(qsSelectedIndex - 1, 0); updateQsSelection(); }
1741
+ else if (e.key === 'Enter') { e.preventDefault(); const sel = items[qsSelectedIndex]; if (sel) { loadNote(sel.dataset.path); closeQuickSwitcher(); } }
1742
+ });
1743
+
1744
+ function renderQsResults(query) {
1745
+ const container = $('qs-results');
1746
+ let entries = Object.entries(allNotes);
1747
+ if (query) entries = entries.filter(([path, note]) => note.title.toLowerCase().includes(query) || path.toLowerCase().includes(query));
1748
+ entries.sort((a, b) => a[1].title.localeCompare(b[1].title));
1749
+ entries = entries.slice(0, 15);
1750
+ if (entries.length === 0) { container.innerHTML = '<div class="qs-empty">No notes found</div>'; return; }
1751
+ container.innerHTML = entries.map(([path, note], i) => {
1752
+ const folder = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '';
1753
+ return `<div class="qs-item${i === qsSelectedIndex ? ' selected' : ''}" data-path="${path}"><span class="qs-icon">◇</span><span class="qs-name">${note.title}</span>${folder ? `<span class="qs-path">${folder}</span>` : ''}</div>`;
1754
+ }).join('');
1755
+ container.querySelectorAll('.qs-item').forEach((el, i) => {
1756
+ el.addEventListener('click', () => { loadNote(el.dataset.path); closeQuickSwitcher(); });
1757
+ el.addEventListener('mouseenter', () => { qsSelectedIndex = i; updateQsSelection(); });
1758
+ });
1759
+ }
1760
+ function updateQsSelection() {
1761
+ document.querySelectorAll('.qs-item').forEach((el, i) => el.classList.toggle('selected', i === qsSelectedIndex));
1762
+ }
2896
1763
 
2897
- treeData = rawTreeData;
1764
+ // ─── Search ──────────────────────────────────────────────────────────────────
1765
+ const searchInput = $('search-input');
1766
+ const searchDropdown = $('search-dropdown');
2898
1767
 
2899
- function walk(node) {
2900
- if (node.type === 'note') {
2901
- allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
1768
+ searchInput.addEventListener('input', () => {
1769
+ clearTimeout(searchTimeout);
1770
+ const q = searchInput.value.trim();
1771
+ if (!q) { searchDropdown.classList.remove('active'); return; }
1772
+ searchTimeout = setTimeout(async () => {
1773
+ const results = await fetchJSON(`/api/search?q=${encodeURIComponent(q)}`);
1774
+ searchDropdown.innerHTML = '';
1775
+ if (results.length === 0) {
1776
+ searchDropdown.innerHTML = '<div class="search-result"><div class="sr-title" style="color:var(--dim)">No results</div></div>';
1777
+ } else {
1778
+ results.forEach(r => {
1779
+ const div = document.createElement('div');
1780
+ div.className = 'search-result';
1781
+ div.innerHTML = `<div class="sr-title">${r.title}</div><div class="sr-path">${r.path}</div>${r.snippet ? `<div class="sr-snippet">${r.snippet.substring(0, 120)}</div>` : ''}`;
1782
+ div.addEventListener('click', () => { loadNote(r.path); searchDropdown.classList.remove('active'); searchInput.value = ''; });
1783
+ searchDropdown.appendChild(div);
1784
+ });
2902
1785
  }
2903
- (node.children || []).forEach(walk);
2904
- }
2905
- walk(treeData);
1786
+ searchDropdown.classList.add('active');
1787
+ }, 200);
1788
+ });
2906
1789
 
2907
- const promises = Object.keys(allNotes).map(async path => {
2908
- try {
2909
- const note = await fetchJSON(`/api/note/${path}`);
2910
- if (!note.error) allNotes[path] = { title: note.title, tags: note.tags };
2911
- } catch {}
2912
- });
2913
- await Promise.all(promises);
1790
+ searchInput.addEventListener('focus', () => { $('search-bar').querySelector('.s-caret').style.display = 'none'; $('search-bar').querySelector('.s-hint').style.display = 'none'; });
1791
+ searchInput.addEventListener('blur', () => { if (!searchInput.value) { $('search-bar').querySelector('.s-caret').style.display = ''; $('search-bar').querySelector('.s-hint').style.display = ''; } });
1792
+ document.addEventListener('click', e => { if (!e.target.closest('.search-bar')) searchDropdown.classList.remove('active'); });
2914
1793
 
2915
- renderTree(treeData, document.getElementById('file-tree'));
2916
- renderSessionTree(sessionsData);
2917
- renderTagCloud(collectAllTags());
1794
+ // ─── Keyboard Shortcuts ──────────────────────────────────────────────────────
1795
+ document.addEventListener('keydown', e => {
1796
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); searchInput.focus(); }
1797
+ if ((e.metaKey || e.ctrlKey) && e.key === 'o') { e.preventDefault(); openQuickSwitcher(); }
1798
+ if (e.key === 'Escape') { closeQuickSwitcher(); closeEditor(); closeSessionCreate(); closeProjectCreate(); searchDropdown.classList.remove('active'); }
1799
+ if ((e.metaKey || e.ctrlKey) && e.key === 's' && editingPath) { e.preventDefault(); $('edit-save').click(); }
1800
+ });
2918
1801
 
2919
- document.getElementById('stats-bar').innerHTML = `
2920
- <span><span class="stat-val">${stats.notes}</span> notes</span>
2921
- <span><span class="stat-val">${stats.folders}</span> folders</span>
2922
- <span><span class="stat-val">${stats.tags}</span> tags</span>
2923
- <span><span class="stat-val">${stats.links}</span> links</span>
2924
- `;
1802
+ // ─── Refresh & Polling ───────────────────────────────────────────────────────
1803
+ function fmtTokens(n) {
1804
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1805
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
1806
+ return String(n);
2925
1807
  }
2926
1808
 
2927
- // --- Auto-refresh: poll for vault changes ---
2928
- let lastKnownStats = null;
1809
+ async function loadTokenEconomics() {
1810
+ try {
1811
+ const te = await fetchJSON('/api/token-economics');
1812
+ if (te && te.session_count > 0) {
1813
+ $('te-explore').textContent = fmtTokens(te.total_exploration_tokens) + 't';
1814
+ $('te-inject').textContent = te.latest_injection_tokens > 0 ? fmtTokens(te.latest_injection_tokens) + 't' : '—';
1815
+ if (te.compression_ratio > 0) {
1816
+ $('te-savings').textContent = te.compression_ratio + 'x';
1817
+ $('te-savings').parentElement.title = `${te.per_session_savings_pct}% per session — injection is ${te.compression_ratio}x smaller than avg exploration (${fmtTokens(te.avg_exploration_per_session)}t)`;
1818
+ } else {
1819
+ $('te-savings').textContent = '—';
1820
+ }
1821
+ $('token-economics').style.display = '';
1822
+ }
1823
+ } catch {}
1824
+ }
1825
+
1826
+ async function refreshTree() {
1827
+ const [rawTree, sessionsData, stats] = await Promise.all([
1828
+ fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
1829
+ ]);
1830
+ treeData = rawTree;
1831
+ allNotes = {};
1832
+ function walk(node) {
1833
+ if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
1834
+ (node.children || []).forEach(walk);
1835
+ }
1836
+ walk(treeData);
1837
+ renderSidebar(sessionsData);
1838
+ $('stat-notes').textContent = stats.notes;
1839
+ $('stat-folders').textContent = stats.folders;
1840
+ markActiveItems();
1841
+ loadTokenEconomics();
1842
+ }
2929
1843
 
2930
1844
  async function pollForChanges() {
2931
1845
  try {
@@ -2939,11 +1853,48 @@ async function pollForChanges() {
2939
1853
  } catch {}
2940
1854
  }
2941
1855
 
2942
- setInterval(pollForChanges, 3000);
1856
+ // ─── Init ────────────────────────────────────────────────────────────────────
1857
+ async function init() {
1858
+ const density = localStorage.getItem('kyp-density') || 'regular';
1859
+ document.documentElement.setAttribute('data-density', density);
1860
+
1861
+ const [rawTree, sessionsData, stats] = await Promise.all([
1862
+ fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
1863
+ ]);
1864
+ treeData = rawTree;
1865
+
1866
+ function walk(node) {
1867
+ if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
1868
+ (node.children || []).forEach(walk);
1869
+ }
1870
+ walk(treeData);
1871
+
1872
+ // Enrich notes with tags
1873
+ const promises = Object.keys(allNotes).map(async path => {
1874
+ try {
1875
+ const note = await fetchJSON(`/api/note/${path}`);
1876
+ if (!note.error) allNotes[path] = { title: note.title, tags: note.tags || [] };
1877
+ } catch {}
1878
+ });
1879
+ await Promise.all(promises);
1880
+
1881
+ renderSidebar(sessionsData);
1882
+ $('stat-notes').textContent = stats.notes;
1883
+ $('stat-folders').textContent = stats.folders;
1884
+ loadTokenEconomics();
1885
+
1886
+ // Show empty state
1887
+ $('content-area').innerHTML = `
1888
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px">
1889
+ <span class="logo" style="font-size:28px;opacity:0.6"><span class="logo-mark" style="width:12px;height:12px"></span>KYP·MEM</span>
1890
+ <span class="dim" style="font-size:var(--fz-sm);letter-spacing:3px;text-transform:uppercase">know your project memory</span>
1891
+ <span class="dim" style="font-size:var(--fz-sm);margin-top:24px">Select a note or press <span style="border:1px solid var(--line);border-radius:3px;padding:0 5px;color:var(--muted)">⌘O</span> to quick-switch</span>
1892
+ </div>
1893
+ `;
1894
+ }
2943
1895
 
2944
- initResize();
2945
- initGraphResize();
2946
1896
  init();
1897
+ setInterval(pollForChanges, 3000);
2947
1898
  </script>
2948
1899
  </body>
2949
1900
  </html>