kyp-mem 0.4.4 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2949 +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: flex-start;
1298
- padding: 5px 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
- margin-top: 3px;
1332
- }
1333
-
1334
- .session-item .si-content {
1335
- display: flex;
1336
- flex-direction: column;
1337
- min-width: 0;
1338
- flex: 1;
1339
- }
1340
-
1341
- .session-item .si-time {
1342
- font-family: var(--font-mono);
1343
- font-size: 10px;
1344
- color: var(--text-secondary);
1345
- }
1346
-
1347
- .session-item .si-summary {
1348
- font-size: 10px;
1349
- color: var(--text-muted);
1350
- white-space: nowrap;
1351
- overflow: hidden;
1352
- text-overflow: ellipsis;
1353
- line-height: 1.3;
1354
- }
1355
-
1356
- .session-item.active .si-time { color: var(--neon-green); }
1357
- .session-item.active .si-summary { color: var(--text-secondary); }
1358
-
1359
- .session-badge {
1360
- display: inline-flex;
1361
- align-items: center;
1362
- gap: 8px;
1363
- font-family: var(--font-mono);
1364
- font-size: 10px;
1365
- color: var(--neon-green);
1366
- background: rgba(91,185,140,0.08);
1367
- border: 1px solid rgba(91,185,140,0.15);
1368
- padding: 4px 12px;
1369
- border-radius: var(--radius);
1370
- margin-bottom: 16px;
1371
- letter-spacing: 0.5px;
1372
- }
1373
-
1374
- .session-empty {
1375
- padding: 12px 18px;
1376
- font-family: var(--font-mono);
1377
- font-size: 10px;
1378
- color: var(--text-muted);
1379
- text-align: center;
1380
- }
1381
-
1382
- .session-create-overlay {
1383
- position: fixed;
1384
- inset: 0;
1385
- background: rgba(6,6,12,0.6);
1386
- z-index: 200;
1387
- display: none;
1388
- align-items: center;
1389
- justify-content: center;
1390
- backdrop-filter: blur(12px);
1391
- -webkit-backdrop-filter: blur(12px);
1392
- }
1393
-
1394
- .session-create-overlay.active { display: flex; }
1395
-
1396
- .session-create-modal {
1397
- width: 420px;
1398
- background: rgba(17,17,22,0.85);
1399
- border: 1px solid var(--border);
1400
- border-radius: var(--radius-lg);
1401
- box-shadow: 0 24px 64px rgba(0,0,0,0.8), 0 0 0 1px rgba(91,185,140,0.1);
1402
- overflow: hidden;
1403
- backdrop-filter: blur(20px);
1404
- -webkit-backdrop-filter: blur(20px);
1405
- }
1406
-
1407
- .session-create-header {
1408
- padding: 16px 20px 12px;
1409
- border-bottom: 1px solid var(--border);
1410
- font-family: var(--font-mono);
1411
- font-size: 11px;
1412
- font-weight: 600;
1413
- color: var(--text-secondary);
1414
- letter-spacing: 0.5px;
1415
- }
1416
-
1417
- .session-create-body {
1418
- padding: 16px 20px;
1419
- display: flex;
1420
- flex-direction: column;
1421
- gap: 12px;
1422
- }
1423
-
1424
- .session-field label {
1425
- display: block;
1426
- font-family: var(--font-mono);
1427
- font-size: 9px;
1428
- font-weight: 600;
1429
- text-transform: uppercase;
1430
- letter-spacing: 1px;
1431
- color: var(--text-muted);
1432
- margin-bottom: 4px;
1433
- }
1434
-
1435
- .session-field select,
1436
- .session-field input,
1437
- .session-field textarea {
1438
- width: 100%;
1439
- background: rgba(8,8,10,0.5);
1440
- border: 1px solid var(--border);
1441
- color: var(--text-primary);
1442
- font-family: var(--font-mono);
1443
- font-size: 12px;
1444
- padding: 8px 12px;
1445
- border-radius: var(--radius);
1446
- outline: none;
1447
- transition: all 0.2s;
1448
- }
1449
-
1450
- .session-field select:focus,
1451
- .session-field input:focus,
1452
- .session-field textarea:focus {
1453
- border-color: rgba(91,185,140,0.4);
1454
- box-shadow: 0 0 8px rgba(91,185,140,0.1);
1455
- background: var(--bg-surface);
1456
- }
1457
-
1458
- .session-field textarea {
1459
- min-height: 60px;
1460
- resize: vertical;
1461
- }
1462
-
1463
- .session-create-footer {
1464
- padding: 12px 20px;
1465
- border-top: 1px solid var(--border);
1466
- display: flex;
1467
- justify-content: flex-end;
1468
- gap: 8px;
1469
- }
1470
-
1471
- .session-create-footer .edit-btn.create {
1472
- background: rgba(91,185,140,0.1);
1473
- color: var(--neon-green);
1474
- border-color: rgba(91,185,140,0.3);
1475
- }
1476
-
1477
- .session-create-footer .edit-btn.create:hover {
1478
- background: rgba(91,185,140,0.2);
1479
- box-shadow: 0 0 10px rgba(91,185,140,0.15);
1480
- transform: translateY(-1px);
1481
- }
1482
-
1483
- /* ============ SCROLLBAR ============ */
1484
- ::-webkit-scrollbar { width: 5px; height: 5px; }
1485
- ::-webkit-scrollbar-track { background: transparent; }
1486
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
1487
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
1488
-
1489
- /* ============ EDIT MODAL ============ */
1490
- .edit-overlay {
1491
- position: fixed;
1492
- inset: 0;
1493
- background: rgba(6,6,12,0.6);
1494
- z-index: 200;
1495
- display: none;
1496
- align-items: center;
1497
- justify-content: center;
1498
- backdrop-filter: blur(12px);
1499
- -webkit-backdrop-filter: blur(12px);
1500
- }
1501
-
1502
- .edit-overlay.active { display: flex; }
1503
-
1504
- .edit-modal {
1505
- width: 640px;
1506
- max-height: 80vh;
1507
- background: rgba(17,17,22,0.85);
1508
- border: 1px solid var(--border);
1509
- border-radius: var(--radius-lg);
1510
- box-shadow: 0 24px 64px rgba(0,0,0,0.8), 0 0 0 1px rgba(217,119,87,0.1);
1511
- display: flex;
1512
- flex-direction: column;
1513
- overflow: hidden;
1514
- backdrop-filter: blur(20px);
1515
- -webkit-backdrop-filter: blur(20px);
1516
- }
1517
-
1518
- .edit-header {
1519
- display: flex;
1520
- align-items: center;
1521
- justify-content: space-between;
1522
- padding: 12px 16px;
1523
- border-bottom: 1px solid var(--border);
1524
- font-family: var(--font-mono);
1525
- font-size: 11px;
1526
- color: var(--text-secondary);
1527
- }
1528
-
1529
- .edit-header .edit-path { opacity: 0.6; }
1530
-
1531
- .edit-actions { display: flex; gap: 8px; }
1532
-
1533
- .edit-btn {
1534
- font-family: var(--font-mono);
1535
- font-size: 10px;
1536
- padding: 5px 14px;
1537
- border-radius: var(--radius);
1538
- border: 1px solid var(--border);
1539
- cursor: pointer;
1540
- transition: all 0.2s ease-out;
1541
- letter-spacing: 0.5px;
1542
- }
1543
-
1544
- .edit-btn.cancel {
1545
- background: transparent;
1546
- color: var(--text-muted);
1547
- }
1548
-
1549
- .edit-btn.cancel:hover { color: var(--text-secondary); border-color: var(--text-muted); transform: translateY(-1px); }
1550
-
1551
- .edit-btn.save {
1552
- background: rgba(217,119,87,0.1);
1553
- color: var(--neon-cyan);
1554
- border-color: rgba(217,119,87,0.3);
1555
- }
1556
-
1557
- .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); }
1558
-
1559
- .edit-textarea {
1560
- flex: 1;
1561
- min-height: 300px;
1562
- background: rgba(8,8,10,0.3);
1563
- border: none;
1564
- color: var(--text-primary);
1565
- font-family: var(--font-mono);
1566
- font-size: 13px;
1567
- line-height: 1.8;
1568
- padding: 16px 20px;
1569
- resize: none;
1570
- outline: none;
1571
- }
1572
-
1573
- .edit-textarea::placeholder { color: var(--text-muted); }
1574
-
1575
- .note-edit-btn {
1576
- background: transparent;
1577
- border: 1px solid var(--border);
1578
- color: var(--text-muted);
1579
- font-family: var(--font-mono);
1580
- font-size: 10px;
1581
- padding: 3px 10px;
1582
- border-radius: var(--radius-sm);
1583
- cursor: pointer;
1584
- transition: all 0.15s;
1585
- float: right;
1586
- margin-bottom: 12px;
1587
- }
1588
-
1589
- .note-edit-btn:hover {
1590
- border-color: var(--text-muted);
1591
- color: var(--text-secondary);
1592
- }
1593
-
1594
- /* ============ BACKLINK & UNLINKED ITEMS ============ */
1595
- .rp-link-item {
1596
- display: flex;
1597
- align-items: center;
1598
- padding: 4px 8px;
1599
- border-radius: var(--radius-sm);
1600
- cursor: pointer;
1601
- margin-bottom: 1px;
1602
- transition: background 0.1s;
1603
- gap: 6px;
1604
- }
1605
-
1606
- .rp-link-item:hover { background: var(--bg-hover); }
1607
-
1608
- .rp-link-item .rp-link-icon {
1609
- font-size: 9px;
1610
- flex-shrink: 0;
1611
- opacity: 0.5;
1612
- }
1613
-
1614
- .rp-link-item .rp-link-icon.backlink { color: var(--neon-cyan); }
1615
- .rp-link-item .rp-link-icon.unlinked { color: var(--text-muted); }
1616
-
1617
- .rp-link-item .rp-link-title {
1618
- font-size: 12px;
1619
- color: var(--neon-cyan);
1620
- font-weight: 500;
1621
- overflow: hidden;
1622
- text-overflow: ellipsis;
1623
- white-space: nowrap;
1624
- transition: all 0.15s;
1625
- }
1626
-
1627
- .rp-link-item:hover .rp-link-title {
1628
- text-decoration: underline;
1629
- text-underline-offset: 2px;
1630
- }
1631
-
1632
- .rp-link-item.unlinked .rp-link-title {
1633
- color: var(--text-secondary);
1634
- }
1635
-
1636
- .rp-link-item.unlinked:hover .rp-link-title {
1637
- color: var(--neon-cyan);
1638
- text-decoration: underline;
1639
- }
1640
-
1641
- /* ============ TRANSITIONS ============ */
1642
- .fade-in { animation: fadeIn 0.15s ease; }
1643
-
1644
- @keyframes fadeIn {
1645
- from { opacity: 0; transform: translateY(3px); }
1646
- to { opacity: 1; transform: translateY(0); }
1647
- }
1648
- </style>
1649
- </head>
1650
- <body>
1651
-
1652
- <div class="layout" id="layout">
1653
- <!-- Header -->
1654
- <div class="header">
1655
- <div class="logo">KYP-MEM <span class="logo-sub">know your project</span></div>
1656
- <div class="breadcrumb" id="breadcrumb"></div>
1657
- <div class="header-actions">
1658
- <button class="header-btn active" id="graph-toggle" title="Toggle graph">
1659
- <span class="dot"></span>
1660
- <span>GRAPH</span>
1661
- </button>
1662
- <button class="header-btn" id="new-project-btn" title="New project">
1663
- <span>+ PROJECT</span>
1664
- </button>
1665
- <div class="search-box">
1666
- <span class="search-icon">&#9906;</span>
1667
- <input type="text" id="search-input" placeholder="Search...">
1668
- <span class="search-hint">&#8984;K</span>
1669
- <div class="search-results" id="search-results"></div>
1670
- </div>
1671
- </div>
1672
- </div>
1673
-
1674
- <!-- Left sidebar -->
1675
- <div class="sidebar">
1676
- <div class="sidebar-scroll">
1677
- <!-- SESSIONS PILLAR (Claude-Mem) -->
1678
- <div class="pillar-label">
1679
- <span><span class="pillar-accent sessions"></span>SESSIONS</span>
1680
- <span class="pillar-action" id="new-session-btn">+ NEW</span>
1681
- </div>
1682
- <div class="session-search-box">
1683
- <span class="ss-icon">&#9906;</span>
1684
- <input type="text" id="session-search-input" placeholder="Semantic search sessions...">
1685
- </div>
1686
- <div class="session-search-results" id="session-search-results"></div>
1687
- <div id="session-tree"></div>
1688
-
1689
- <div class="pillar-divider"></div>
1690
-
1691
- <!-- PROJECTS PILLAR (Obsidian) -->
1692
- <div class="pillar-label">
1693
- <span><span class="pillar-accent projects"></span>PROJECTS</span>
1694
- <span class="pillar-action" id="new-project-sidebar-btn">+ NEW</span>
1695
- </div>
1696
- <div class="sidebar-section">
1697
- <div id="filter-info" class="filter-info" style="display:none;">
1698
- <span id="filter-count"></span>
1699
- <span class="clear-btn" id="clear-filters">clear</span>
1700
- </div>
1701
- <div id="file-tree"></div>
1702
- </div>
1703
- <div class="tag-filter-section">
1704
- <div class="tag-filter-header" id="tag-filter-toggle">
1705
- <span>Tags</span>
1706
- <span class="arrow open" id="tag-arrow">&#9654;</span>
1707
- </div>
1708
- <div class="tag-filter-body" id="tag-filter-body">
1709
- <div class="tag-filter-active" id="active-tags"></div>
1710
- <div class="tag-cloud" id="tag-cloud"></div>
1711
- </div>
1712
- </div>
1713
- </div>
1714
- <div class="stats-bar" id="stats-bar"></div>
1715
- </div>
1716
-
1717
- <!-- Resize handle: sidebar | content -->
1718
- <div class="resize-handle" id="resize-left"></div>
1719
-
1720
- <!-- Content -->
1721
- <div class="content" id="content">
1722
- <div class="empty-state">
1723
- <div class="es-logo">KYP-MEM</div>
1724
- <div class="es-tagline">Know Your Project Memory</div>
1725
- <div class="es-hint">Select a note or press <kbd>&#8984;O</kbd> to quick-switch &middot; <kbd>&#8984;K</kbd> to search</div>
1726
- </div>
1727
- </div>
1728
-
1729
- <!-- Resize handle: content | right panel -->
1730
- <div class="resize-handle" id="resize-right"></div>
1731
-
1732
- <!-- Right panel -->
1733
- <div class="right-panel" id="right-panel">
1734
- <div id="graph-section">
1735
- <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>
1736
- <div id="graph-container"></div>
1737
- <div id="graph-resize-handle" class="graph-resize-handle"></div>
1738
- </div>
1739
- <div id="rp-outline" class="rp-section" style="display:none;">
1740
- <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>
1741
- <div id="rp-outline-list" class="rp-section-body"></div>
1742
- </div>
1743
- <div id="rp-backlinks" class="rp-section" style="display:none;">
1744
- <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>
1745
- <div id="rp-backlinks-list" class="rp-section-body"></div>
1746
- </div>
1747
- <div id="rp-related" class="rp-section" style="display:none;">
1748
- <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>
1749
- <div id="rp-related-list" class="rp-section-body"></div>
1750
- </div>
1751
- <div id="rp-outlinks" class="rp-section" style="display:none;">
1752
- <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>
1753
- <div id="rp-outlinks-list" class="rp-section-body"></div>
1754
- </div>
1755
- <div id="rp-unlinked" class="rp-section" style="display:none;">
1756
- <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>
1757
- <div id="rp-unlinked-list" class="rp-section-body"></div>
1758
- </div>
1759
- </div>
1760
- </div>
1761
-
1762
- <!-- Edit Modal -->
1763
- <div class="edit-overlay" id="edit-overlay">
1764
- <div class="edit-modal">
1765
- <div class="edit-header">
1766
- <span class="edit-path" id="edit-path"></span>
1767
- <div class="edit-actions">
1768
- <button class="edit-btn cancel" id="edit-cancel">ESC</button>
1769
- <button class="edit-btn save" id="edit-save">SAVE</button>
1770
- </div>
1771
- </div>
1772
- <textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown..."></textarea>
1773
- </div>
1774
- </div>
1775
-
1776
- <!-- Quick Switcher Overlay -->
1777
- <div class="quick-switcher-overlay" id="qs-overlay">
1778
- <div class="quick-switcher">
1779
- <input type="text" id="qs-input" placeholder="Jump to note...">
1780
- <div class="quick-switcher-results" id="qs-results"></div>
1781
- </div>
1782
- </div>
1783
-
1784
- <!-- Session Create Modal -->
1785
- <div class="session-create-overlay" id="session-create-overlay">
1786
- <div class="session-create-modal">
1787
- <div class="session-create-header">NEW SESSION</div>
1788
- <div class="session-create-body">
1789
- <div class="session-field">
1790
- <label>Project</label>
1791
- <input type="text" id="session-project" list="project-list" placeholder="Project name...">
1792
- <datalist id="project-list"></datalist>
1793
- </div>
1794
- <div class="session-field">
1795
- <label>Summary</label>
1796
- <textarea id="session-summary" placeholder="What are you working on?"></textarea>
1797
- </div>
1798
- </div>
1799
- <div class="session-create-footer">
1800
- <button class="edit-btn cancel" id="session-cancel">CANCEL</button>
1801
- <button class="edit-btn create" id="session-create-btn">CREATE</button>
1802
- </div>
1803
- </div>
1804
- </div>
1805
-
1806
- <!-- Project Create Modal -->
1807
- <div class="session-create-overlay" id="project-create-overlay">
1808
- <div class="session-create-modal">
1809
- <div class="session-create-header">NEW PROJECT</div>
1810
- <div class="session-create-body">
1811
- <div class="session-field">
1812
- <label>Project Name</label>
1813
- <input type="text" id="project-name-input" placeholder="My Project...">
1814
- </div>
1815
- <div class="session-field">
1816
- <label>Overview (optional)</label>
1817
- <textarea id="project-overview-input" placeholder="Brief project description, goals, tech stack..."></textarea>
1818
- </div>
1819
- </div>
1820
- <div class="session-create-footer">
1821
- <button class="edit-btn cancel" id="project-cancel">CANCEL</button>
1822
- <button class="edit-btn create" id="project-create-btn">CREATE</button>
1823
- </div>
1824
- </div>
1825
- </div>
1826
-
1827
- <script>
1828
- let currentPath = null;
1829
- let treeData = null;
1830
- let allNotes = {};
1831
- let graphVisible = true;
1832
- let currentNote = null;
1833
- let activeTagFilters = new Set();
1834
- let qsSelectedIndex = 0;
1835
-
1836
- async function fetchJSON(url) {
1837
- const r = await fetch(url);
1838
- return r.json();
1839
- }
1840
-
1841
- // --- Graph Toggle ---
1842
- document.getElementById('graph-toggle').addEventListener('click', () => {
1843
- graphVisible = !graphVisible;
1844
- const btn = document.getElementById('graph-toggle');
1845
- const section = document.getElementById('graph-section');
1846
- const layout = document.getElementById('layout');
1847
- btn.classList.toggle('active', graphVisible);
1848
-
1849
- if (!graphVisible) {
1850
- section.classList.add('hidden');
1851
- layout.classList.add('no-right-panel');
1852
- } else {
1853
- section.classList.remove('hidden');
1854
- layout.classList.remove('no-right-panel');
1855
- if (currentPath) loadNote(currentPath);
1856
- }
1857
- });
1858
-
1859
- // --- Keyboard shortcuts ---
1860
- document.addEventListener('keydown', (e) => {
1861
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1862
- e.preventDefault();
1863
- document.getElementById('search-input').focus();
1864
- }
1865
- if ((e.metaKey || e.ctrlKey) && e.key === 'o') {
1866
- e.preventDefault();
1867
- openQuickSwitcher();
1868
- }
1869
- if (e.key === 'Escape') {
1870
- closeQuickSwitcher();
1871
- closeSessionCreate();
1872
- closeProjectCreate();
1873
- }
1874
- });
1875
-
1876
- // --- Quick Switcher ---
1877
- function openQuickSwitcher() {
1878
- const overlay = document.getElementById('qs-overlay');
1879
- const input = document.getElementById('qs-input');
1880
- overlay.classList.add('active');
1881
- input.value = '';
1882
- input.focus();
1883
- qsSelectedIndex = 0;
1884
- renderQsResults('');
1885
- }
1886
-
1887
- function closeQuickSwitcher() {
1888
- document.getElementById('qs-overlay').classList.remove('active');
1889
- }
1890
-
1891
- document.getElementById('qs-overlay').addEventListener('click', (e) => {
1892
- if (e.target === e.currentTarget) closeQuickSwitcher();
1893
- });
1894
-
1895
- document.getElementById('qs-input').addEventListener('input', (e) => {
1896
- qsSelectedIndex = 0;
1897
- renderQsResults(e.target.value.trim().toLowerCase());
1898
- });
1899
-
1900
- document.getElementById('qs-input').addEventListener('keydown', (e) => {
1901
- const items = document.querySelectorAll('.qs-item');
1902
- if (e.key === 'ArrowDown') {
1903
- e.preventDefault();
1904
- qsSelectedIndex = Math.min(qsSelectedIndex + 1, items.length - 1);
1905
- updateQsSelection();
1906
- } else if (e.key === 'ArrowUp') {
1907
- e.preventDefault();
1908
- qsSelectedIndex = Math.max(qsSelectedIndex - 1, 0);
1909
- updateQsSelection();
1910
- } else if (e.key === 'Enter') {
1911
- e.preventDefault();
1912
- const selected = items[qsSelectedIndex];
1913
- if (selected) {
1914
- loadNote(selected.dataset.path);
1915
- closeQuickSwitcher();
1916
- }
1917
- }
1918
- });
1919
-
1920
- function renderQsResults(query) {
1921
- const container = document.getElementById('qs-results');
1922
- const entries = Object.entries(allNotes);
1923
-
1924
- let filtered = entries;
1925
- if (query) {
1926
- filtered = entries.filter(([path, note]) => {
1927
- const name = note.title.toLowerCase();
1928
- const p = path.toLowerCase();
1929
- return name.includes(query) || p.includes(query);
1930
- });
1931
- }
1932
-
1933
- filtered.sort((a, b) => a[1].title.localeCompare(b[1].title));
1934
- filtered = filtered.slice(0, 15);
1935
-
1936
- if (filtered.length === 0) {
1937
- container.innerHTML = '<div class="qs-empty">No notes found</div>';
1938
- return;
1939
- }
1940
-
1941
- container.innerHTML = filtered.map(([path, note], i) => {
1942
- const folder = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '';
1943
- return `<div class="qs-item${i === qsSelectedIndex ? ' selected' : ''}" data-path="${path}">
1944
- <span class="qs-icon">&#9671;</span>
1945
- <span class="qs-name">${note.title}</span>
1946
- ${folder ? `<span class="qs-path">${folder}</span>` : ''}
1947
- </div>`;
1948
- }).join('');
1949
-
1950
- container.querySelectorAll('.qs-item').forEach((el, i) => {
1951
- el.addEventListener('click', () => {
1952
- loadNote(el.dataset.path);
1953
- closeQuickSwitcher();
1954
- });
1955
- el.addEventListener('mouseenter', () => {
1956
- qsSelectedIndex = i;
1957
- updateQsSelection();
1958
- });
1959
- });
1960
- }
1961
-
1962
- function updateQsSelection() {
1963
- document.querySelectorAll('.qs-item').forEach((el, i) => {
1964
- el.classList.toggle('selected', i === qsSelectedIndex);
1965
- });
1966
- }
1967
-
1968
- // --- File Tree (Projects pillar only — no Sessions folders) ---
1969
- function renderTree(node, container) {
1970
- if (node.type === 'folder' && node.name !== 'vault') {
1971
- if (node.name === 'Sessions') return;
1972
-
1973
- const item = document.createElement('div');
1974
- item.className = 'tree-item';
1975
- item.innerHTML = `<span class="arrow open">&#9654;</span><span class="icon folder-icon">&#9776;</span><span class="name">${node.name}</span>`;
1976
-
1977
- const children = document.createElement('div');
1978
- children.className = 'tree-children';
1979
-
1980
- item.addEventListener('click', (e) => {
1981
- e.stopPropagation();
1982
- item.querySelector('.arrow').classList.toggle('open');
1983
- children.classList.toggle('collapsed');
1984
- });
1985
-
1986
- container.appendChild(item);
1987
- container.appendChild(children);
1988
- (node.children || []).forEach(c => renderTree(c, children));
1989
-
1990
- } else if (node.type === 'note') {
1991
- const item = document.createElement('div');
1992
- item.className = 'tree-item';
1993
- item.dataset.path = node.path;
1994
- const displayName = node.name.replace('.md', '');
1995
- item.innerHTML = `<span class="arrow" style="visibility:hidden">&#9654;</span><span class="icon note-icon">&#9671;</span><span class="name">${displayName}</span>`;
1996
- item.addEventListener('click', () => loadNote(node.path));
1997
- container.appendChild(item);
1998
-
1999
- } else if (node.children) {
2000
- node.children.forEach(c => renderTree(c, container));
2001
- }
2002
- }
2003
-
2004
- // --- Session Tree (Sessions pillar — grouped by project with dropdowns) ---
2005
- function formatSessionTimestamp(filename) {
2006
- const stem = filename.replace('.md', '');
2007
- const match = stem.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})(\d{2})(\d{2})$/);
2008
- if (!match) return stem;
2009
- const [, y, mo, d, h, mi] = match;
2010
- const hr = parseInt(h);
2011
- const ampm = hr >= 12 ? 'PM' : 'AM';
2012
- const hr12 = hr % 12 || 12;
2013
- const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
2014
- return `${months[parseInt(mo)-1]} ${parseInt(d)}, ${hr12}:${mi} ${ampm}`;
2015
- }
2016
-
2017
- function renderSessionTree(sessionsData) {
2018
- const container = document.getElementById('session-tree');
2019
- container.innerHTML = '';
2020
-
2021
- if (Object.keys(sessionsData).length === 0) {
2022
- container.innerHTML = '<div class="session-empty">No sessions yet</div>';
2023
- return;
2024
- }
2025
-
2026
- const sortedProjects = Object.keys(sessionsData).sort();
2027
- sortedProjects.forEach(project => {
2028
- const sessions = sessionsData[project];
2029
- const group = document.createElement('div');
2030
- group.className = 'session-project-group';
2031
-
2032
- const header = document.createElement('div');
2033
- header.className = 'session-project-header';
2034
- header.innerHTML = `
2035
- <span class="sp-arrow open">&#9654;</span>
2036
- <span class="sp-icon">&#9716;</span>
2037
- <span class="sp-name">${project}</span>
2038
- <span class="sp-count">${sessions.length}</span>
2039
- <button class="sp-add" title="New session for ${project}">+</button>
2040
- `;
2041
-
2042
- const list = document.createElement('div');
2043
- list.className = 'session-list';
2044
-
2045
- sessions.sort((a, b) => b.path.localeCompare(a.path));
2046
- sessions.forEach(session => {
2047
- const item = document.createElement('div');
2048
- item.className = 'session-item';
2049
- item.dataset.path = session.path;
2050
- const displayTime = formatSessionTimestamp(session.path.split('/').pop());
2051
- item.innerHTML = `<span class="si-icon">&#9679;</span><span class="si-time">${displayTime}</span>`;
2052
- item.title = session.title || session.path;
2053
- item.addEventListener('click', () => loadNote(session.path));
2054
- list.appendChild(item);
2055
- });
2056
-
2057
- header.addEventListener('click', (e) => {
2058
- if (e.target.classList.contains('sp-add')) {
2059
- e.stopPropagation();
2060
- openSessionCreate(project);
2061
- return;
2062
- }
2063
- e.stopPropagation();
2064
- header.querySelector('.sp-arrow').classList.toggle('open');
2065
- list.classList.toggle('collapsed');
2066
- });
2067
-
2068
- group.appendChild(header);
2069
- group.appendChild(list);
2070
- container.appendChild(group);
2071
- });
2072
- }
2073
-
2074
- // --- Session Semantic Search ---
2075
- let sessionSearchTimeout;
2076
- const sessionSearchInput = document.getElementById('session-search-input');
2077
- const sessionSearchResults = document.getElementById('session-search-results');
2078
-
2079
- sessionSearchInput.addEventListener('input', () => {
2080
- clearTimeout(sessionSearchTimeout);
2081
- const q = sessionSearchInput.value.trim();
2082
- if (!q) { sessionSearchResults.innerHTML = ''; return; }
2083
- sessionSearchTimeout = setTimeout(async () => {
2084
- const results = await fetchJSON(`/api/sessions/search?q=${encodeURIComponent(q)}`);
2085
- sessionSearchResults.innerHTML = '';
2086
- if (results.length === 0) {
2087
- sessionSearchResults.innerHTML = '<div class="ss-result"><span class="ss-meta">No matching sessions</span></div>';
2088
- return;
2089
- }
2090
- results.forEach(r => {
2091
- const div = document.createElement('div');
2092
- div.className = 'ss-result';
2093
- const dist = r.distance !== undefined ? ` &middot; dist: ${r.distance.toFixed(2)}` : '';
2094
- 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>` : ''}`;
2095
- div.addEventListener('click', () => {
2096
- loadNote(r.path);
2097
- sessionSearchInput.value = '';
2098
- sessionSearchResults.innerHTML = '';
2099
- });
2100
- sessionSearchResults.appendChild(div);
2101
- });
2102
- }, 250);
2103
- });
2104
-
2105
- // --- Note rendering ---
2106
- async function loadNote(path) {
2107
- currentPath = path;
2108
- const note = await fetchJSON(`/api/note/${path}`);
2109
- if (note.error) return;
2110
- currentNote = note;
2111
-
2112
- document.querySelectorAll('.tree-item[data-path]').forEach(el => {
2113
- el.classList.toggle('active', el.dataset.path === path);
2114
- });
2115
- document.querySelectorAll('.session-item[data-path]').forEach(el => {
2116
- el.classList.toggle('active', el.dataset.path === path);
2117
- });
2118
-
2119
- const parts = path.replace('.md', '').split('/');
2120
- document.getElementById('breadcrumb').innerHTML = parts.map((p, i) =>
2121
- i < parts.length - 1
2122
- ? `<span>${p}</span><span class="sep">/</span>`
2123
- : `<span class="current">${p}</span>`
2124
- ).join('');
2125
-
2126
- const contentEl = document.getElementById('content');
2127
- let html = '<div class="fade-in">';
2128
-
2129
- html += `<button class="note-edit-btn" onclick="openEditor('${path}')">EDIT</button>`;
2130
-
2131
- const isSession = path.includes('/Sessions/') || path.startsWith('Sessions/');
2132
- if (isSession) {
2133
- const sessionProject = path.includes('/Sessions/') ? path.split('/Sessions/')[0] : 'General';
2134
- html += `<div class="session-badge">&#9716; SESSION &middot; ${sessionProject}</div>`;
2135
- }
2136
-
2137
- html += '<div class="note-properties">';
2138
- (note.tags || []).forEach(t => { html += `<span class="tag clickable-tag" data-tag="${t}">#${t}</span>`; });
2139
- Object.entries(note.properties || {}).forEach(([k, v]) => {
2140
- html += `<span class="prop-item"><span class="prop-key">${k}:</span> ${v}</span>`;
2141
- });
2142
- if (note.created) html += `<span class="prop-item"><span class="prop-key">created:</span> ${note.created}</span>`;
2143
- if (note.updated && note.updated !== note.created) html += `<span class="prop-item"><span class="prop-key">updated:</span> ${note.updated}</span>`;
2144
- html += '</div>';
2145
-
2146
- let md = note.content || '';
2147
- md = md.replace(/\[\[([^\]]+)\]\]/g, (_, link) => {
2148
- const display = link.includes('#') ? link.split('#').pop() : link;
2149
- return `<span class="wikilink" data-link="${link}">${display}</span>`;
2150
- });
2151
-
2152
- html += `<div class="md-body">${marked.parse(md)}</div>`;
2153
- html += '</div>';
2154
- contentEl.innerHTML = html;
2155
- contentEl.scrollTop = 0;
2156
-
2157
- contentEl.querySelectorAll('.wikilink').forEach(el => {
1109
+ // Wire wikilinks
1110
+ area.querySelectorAll('.wikilink').forEach(el => {
2158
1111
  el.addEventListener('click', () => {
2159
1112
  const target = findNotePath(el.dataset.link.split('#')[0]);
2160
1113
  if (target) loadNote(target);
2161
1114
  });
2162
1115
  });
2163
1116
 
2164
- contentEl.querySelectorAll('.clickable-tag').forEach(el => {
2165
- el.addEventListener('click', () => {
2166
- activeTagFilters.add(el.dataset.tag);
2167
- applyTagFilter();
2168
- });
2169
- });
2170
-
2171
- renderRightPanel(note);
2172
- renderOutline(note);
2173
- if (graphVisible) renderGraph(note);
2174
- }
2175
-
2176
- function findNotePath(name) {
2177
- const lower = name.toLowerCase();
2178
- for (const [path, note] of Object.entries(allNotes)) {
2179
- const stem = path.split('/').pop().replace('.md', '').toLowerCase();
2180
- if (stem === lower || note.title.toLowerCase() === lower) return path;
2181
- }
2182
- return null;
2183
- }
2184
-
2185
- // --- Outline ---
2186
- function renderOutline(note) {
2187
- const section = document.getElementById('rp-outline');
2188
- const list = document.getElementById('rp-outline-list');
2189
- list.innerHTML = '';
2190
-
2191
- const content = note.content || '';
2192
- const headings = [];
2193
- content.split('\n').forEach(line => {
2194
- const m = line.match(/^(#{1,3})\s+(.+)/);
2195
- if (m) headings.push({ level: m[1].length, text: m[2].trim() });
2196
- });
2197
-
2198
- if (headings.length < 2) {
2199
- section.style.display = 'none';
2200
- return;
2201
- }
2202
-
2203
- section.style.display = '';
2204
- document.getElementById('outline-count').textContent = headings.length;
2205
-
2206
- headings.forEach(h => {
2207
- const el = document.createElement('div');
2208
- el.className = `outline-item h${h.level}`;
2209
- el.textContent = h.text;
1117
+ // Wire tag clicks
1118
+ area.querySelectorAll('.tag-chip[data-tag]').forEach(el => {
2210
1119
  el.addEventListener('click', () => {
2211
- const contentEl = document.getElementById('content');
2212
- const target = Array.from(contentEl.querySelectorAll('h1,h2,h3')).find(
2213
- el => el.textContent.trim() === h.text
2214
- );
2215
- if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
2216
- });
2217
- list.appendChild(el);
2218
- });
2219
- }
2220
-
2221
- // --- Right Panel ---
2222
- function renderRightPanel(note) {
2223
- const blSection = document.getElementById('rp-backlinks');
2224
- const blList = document.getElementById('rp-backlinks-list');
2225
- blList.innerHTML = '';
2226
- const backlinks = note.backlinks || [];
2227
- if (backlinks.length) {
2228
- blSection.style.display = '';
2229
- document.getElementById('bl-count').textContent = backlinks.length;
2230
- backlinks.forEach(bl => {
2231
- const item = document.createElement('div');
2232
- item.className = 'rp-link-item';
2233
- item.innerHTML = `<span class="rp-link-icon backlink">&#8592;</span><span class="rp-link-title">${bl.title || bl.path || bl}</span>`;
2234
- item.addEventListener('click', () => loadNote(bl.path || bl));
2235
- blList.appendChild(item);
2236
- });
2237
- } else {
2238
- blSection.style.display = 'none';
2239
- }
2240
-
2241
- const relSection = document.getElementById('rp-related');
2242
- const relList = document.getElementById('rp-related-list');
2243
- relList.innerHTML = '';
2244
- if (note.related && note.related.length) {
2245
- relSection.style.display = '';
2246
- document.getElementById('rel-count').textContent = note.related.length;
2247
- note.related.forEach(r => {
2248
- const item = document.createElement('div');
2249
- item.className = 'rp-item';
2250
- item.innerHTML = `<span class="rp-score">${r.score.toFixed(2)}</span><span class="rp-title">${r.title}</span>`;
2251
- item.addEventListener('click', () => loadNote(r.path));
2252
- relList.appendChild(item);
2253
- });
2254
- } else {
2255
- relSection.style.display = 'none';
2256
- }
2257
-
2258
- const outSection = document.getElementById('rp-outlinks');
2259
- const outList = document.getElementById('rp-outlinks-list');
2260
- outList.innerHTML = '';
2261
- if (note.links && note.links.length) {
2262
- outSection.style.display = '';
2263
- document.getElementById('out-count').textContent = note.links.length;
2264
- note.links.forEach(link => {
2265
- const path = findNotePath(link);
2266
- const item = document.createElement('div');
2267
- item.className = 'rp-item';
2268
- item.innerHTML = `<span class="rp-title">${link}</span>`;
2269
- if (path) item.addEventListener('click', () => loadNote(path));
2270
- else item.style.opacity = '0.3';
2271
- outList.appendChild(item);
2272
- });
2273
- } else {
2274
- outSection.style.display = 'none';
2275
- }
2276
-
2277
- const ulSection = document.getElementById('rp-unlinked');
2278
- const ulList = document.getElementById('rp-unlinked-list');
2279
- ulList.innerHTML = '';
2280
- const unlinked = note.unlinked || [];
2281
- if (unlinked.length) {
2282
- ulSection.style.display = '';
2283
- document.getElementById('unlinked-count').textContent = unlinked.length;
2284
- unlinked.forEach(u => {
2285
- const item = document.createElement('div');
2286
- item.className = 'rp-link-item unlinked';
2287
- item.innerHTML = `<span class="rp-link-icon unlinked">&#8776;</span><span class="rp-link-title">${u.title}</span>`;
2288
- item.addEventListener('click', () => loadNote(u.path));
2289
- ulList.appendChild(item);
2290
- });
2291
- } else {
2292
- ulSection.style.display = 'none';
2293
- }
2294
- }
2295
-
2296
- // --- Editor ---
2297
- let editingPath = null;
2298
- let editingNote = null;
2299
-
2300
- async function openEditor(path) {
2301
- const note = await fetchJSON(`/api/note/${path}`);
2302
- if (note.error) return;
2303
- editingPath = path;
2304
- editingNote = note;
2305
- document.getElementById('edit-path').textContent = path;
2306
- document.getElementById('edit-textarea').value = note.content || '';
2307
- document.getElementById('edit-overlay').classList.add('active');
2308
- document.getElementById('edit-textarea').focus();
2309
- }
2310
-
2311
- document.getElementById('edit-cancel').addEventListener('click', closeEditor);
2312
- document.getElementById('edit-overlay').addEventListener('click', (e) => {
2313
- if (e.target === e.currentTarget) closeEditor();
2314
- });
2315
-
2316
- document.getElementById('edit-save').addEventListener('click', async () => {
2317
- if (!editingPath) return;
2318
- const content = document.getElementById('edit-textarea').value;
2319
- await fetch(`/api/note/${editingPath}`, {
2320
- method: 'POST',
2321
- headers: { 'Content-Type': 'application/json' },
2322
- body: JSON.stringify({
2323
- content,
2324
- tags: editingNote.tags || [],
2325
- properties: editingNote.properties || {},
2326
- }),
2327
- });
2328
- closeEditor();
2329
- await loadNote(editingPath);
2330
- });
2331
-
2332
- function closeEditor() {
2333
- document.getElementById('edit-overlay').classList.remove('active');
2334
- editingPath = null;
2335
- editingNote = null;
2336
- }
2337
-
2338
- document.addEventListener('keydown', (e) => {
2339
- if (e.key === 'Escape' && document.getElementById('edit-overlay').classList.contains('active')) {
2340
- closeEditor();
2341
- }
2342
- if ((e.metaKey || e.ctrlKey) && e.key === 's' && editingPath) {
2343
- e.preventDefault();
2344
- document.getElementById('edit-save').click();
2345
- }
2346
- });
2347
-
2348
- // --- Session Management ---
2349
- function openSessionCreate(defaultProject) {
2350
- const overlay = document.getElementById('session-create-overlay');
2351
- const projectInput = document.getElementById('session-project');
2352
- const datalist = document.getElementById('project-list');
2353
- const summaryInput = document.getElementById('session-summary');
2354
-
2355
- const projects = new Set();
2356
- for (const path of Object.keys(allNotes)) {
2357
- const parts = path.split('/');
2358
- if (parts.length > 1) projects.add(parts[0]);
2359
- }
2360
-
2361
- datalist.innerHTML = '';
2362
- for (const p of [...projects].sort()) {
2363
- const opt = document.createElement('option');
2364
- opt.value = p;
2365
- datalist.appendChild(opt);
2366
- }
2367
-
2368
- projectInput.value = defaultProject || '';
2369
- summaryInput.value = '';
2370
- overlay.classList.add('active');
2371
-
2372
- if (defaultProject) summaryInput.focus();
2373
- else projectInput.focus();
2374
- }
2375
-
2376
- function closeSessionCreate() {
2377
- document.getElementById('session-create-overlay').classList.remove('active');
2378
- }
2379
-
2380
- document.getElementById('session-cancel').addEventListener('click', closeSessionCreate);
2381
- document.getElementById('session-create-overlay').addEventListener('click', (e) => {
2382
- if (e.target === e.currentTarget) closeSessionCreate();
2383
- });
2384
-
2385
- document.getElementById('session-create-btn').addEventListener('click', async () => {
2386
- const project = document.getElementById('session-project').value.trim();
2387
- const summary = document.getElementById('session-summary').value.trim();
2388
-
2389
- if (!project) {
2390
- document.getElementById('session-project').focus();
2391
- return;
2392
- }
2393
-
2394
- const resp = await fetch('/api/sessions/create', {
2395
- method: 'POST',
2396
- headers: { 'Content-Type': 'application/json' },
2397
- body: JSON.stringify({ project, summary }),
2398
- });
2399
-
2400
- const result = await resp.json();
2401
- if (result.ok) {
2402
- closeSessionCreate();
2403
- await refreshTree();
2404
- loadNote(result.path);
2405
- }
2406
- });
2407
-
2408
- // --- Sidebar pillar buttons ---
2409
- document.getElementById('new-session-btn').addEventListener('click', () => openSessionCreate(''));
2410
-
2411
- document.getElementById('new-project-sidebar-btn').addEventListener('click', () => {
2412
- document.getElementById('project-create-overlay').classList.add('active');
2413
- document.getElementById('project-name-input').value = '';
2414
- document.getElementById('project-overview-input').value = '';
2415
- document.getElementById('project-name-input').focus();
2416
- });
2417
-
2418
- // --- Project Management ---
2419
- function closeProjectCreate() {
2420
- document.getElementById('project-create-overlay').classList.remove('active');
2421
- }
2422
-
2423
- document.getElementById('new-project-btn').addEventListener('click', () => {
2424
- document.getElementById('project-create-overlay').classList.add('active');
2425
- document.getElementById('project-name-input').value = '';
2426
- document.getElementById('project-overview-input').value = '';
2427
- document.getElementById('project-name-input').focus();
2428
- });
2429
-
2430
- document.getElementById('project-cancel').addEventListener('click', closeProjectCreate);
2431
- document.getElementById('project-create-overlay').addEventListener('click', (e) => {
2432
- if (e.target === e.currentTarget) closeProjectCreate();
2433
- });
2434
-
2435
- document.getElementById('project-create-btn').addEventListener('click', async () => {
2436
- const name = document.getElementById('project-name-input').value.trim();
2437
- const overview = document.getElementById('project-overview-input').value.trim();
2438
- if (!name) {
2439
- document.getElementById('project-name-input').focus();
2440
- return;
2441
- }
2442
- const resp = await fetch('/api/projects/create', {
2443
- method: 'POST',
2444
- headers: { 'Content-Type': 'application/json' },
2445
- body: JSON.stringify({ name, overview }),
2446
- });
2447
- const result = await resp.json();
2448
- if (result.ok) {
2449
- closeProjectCreate();
2450
- await refreshTree();
2451
- loadNote(result.path);
2452
- }
2453
- });
2454
-
2455
- async function refreshTree() {
2456
- const [rawTreeData, sessionsData, stats] = await Promise.all([
2457
- fetchJSON('/api/tree'),
2458
- fetchJSON('/api/sessions'),
2459
- fetchJSON('/api/stats'),
2460
- ]);
2461
-
2462
- allNotes = {};
2463
- function walk(node) {
2464
- if (node.type === 'note') {
2465
- allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
2466
- }
2467
- (node.children || []).forEach(walk);
2468
- }
2469
- walk(rawTreeData);
2470
-
2471
- const treeEl = document.getElementById('file-tree');
2472
- treeEl.innerHTML = '';
2473
- renderTree(rawTreeData, treeEl);
2474
-
2475
- renderSessionTree(sessionsData);
2476
- renderTagCloud(collectAllTags());
1120
+ activeTagFilters.add(el.dataset.tag);
1121
+ applyTagFilter();
1122
+ });
1123
+ });
2477
1124
 
2478
- document.getElementById('stats-bar').innerHTML = `
2479
- <span><span class="stat-val">${stats.notes}</span> notes</span>
2480
- <span><span class="stat-val">${stats.folders}</span> folders</span>
2481
- <span><span class="stat-val">${stats.tags}</span> tags</span>
2482
- <span><span class="stat-val">${stats.links}</span> links</span>
2483
- `;
1125
+ // Wire edit button
1126
+ area.querySelector('#note-edit-btn')?.addEventListener('click', () => openEditor(note.path));
2484
1127
 
2485
- markActiveItems();
1128
+ // Render rail
1129
+ if (railOpen) renderRail(note);
2486
1130
  }
2487
1131
 
2488
- function markActiveItems() {
2489
- if (!currentPath) return;
2490
- document.querySelectorAll('.tree-item[data-path]').forEach(el => {
2491
- 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>`
2492
1141
  });
2493
- document.querySelectorAll('.session-item[data-path]').forEach(el => {
2494
- 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() });
2495
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
+ }
2496
1193
  }
2497
1194
 
2498
- // --- Graph ---
2499
- function renderGraph(note) {
2500
- const container = document.getElementById('graph-container');
2501
- 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
+ }
2502
1219
 
1220
+ // ─── Local Graph ─────────────────────────────────────────────────────────────
1221
+ function renderLocalGraph(container, note) {
2503
1222
  const nodes = new Map();
2504
1223
  const links = [];
2505
1224
 
2506
- nodes.set(note.path, { id: note.path, title: note.title, active: true, tags: note.tags || [] });
2507
-
2508
- const projectPrefix = note.path.includes('/') ? note.path.split('/')[0] + '/' : '';
2509
- const inSameProject = (p) => !projectPrefix ? !p.includes('/') : p.startsWith(projectPrefix);
1225
+ nodes.set(note.path, { id: note.path, title: note.title, kind: 'note', center: true });
2510
1226
 
2511
1227
  (note.links || []).forEach(link => {
2512
1228
  const path = findNotePath(link);
2513
- if (path && inSameProject(path)) {
2514
- if (!nodes.has(path)) {
2515
- const n = allNotes[path];
2516
- nodes.set(path, { id: path, title: n ? n.title : link, active: false, tags: n ? n.tags : [] });
2517
- }
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) {
2518
1233
  links.push({ source: note.path, target: path });
2519
1234
  }
2520
1235
  });
2521
1236
 
2522
1237
  (note.backlinks || []).forEach(bl => {
2523
- const blPath = typeof bl === 'string' ? bl : (bl && bl.path);
2524
- if (!blPath) return;
2525
- if (inSameProject(blPath)) {
2526
- if (!nodes.has(blPath)) {
2527
- const n = allNotes[blPath];
2528
- const title = (bl && typeof bl === 'object' && bl.title) ? bl.title : (n ? n.title : blPath);
2529
- nodes.set(blPath, { id: blPath, title, active: false, tags: n ? n.tags : [] });
2530
- }
2531
- 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 });
2532
1241
  }
1242
+ if (p) links.push({ source: p, target: note.path });
2533
1243
  });
2534
1244
 
2535
- (note.related || []).forEach(r => {
2536
- if (r.path && inSameProject(r.path)) {
2537
- if (!nodes.has(r.path)) {
2538
- const n = allNotes[r.path];
2539
- nodes.set(r.path, { id: r.path, title: r.title, active: false, tags: n ? n.tags : [] });
2540
- }
2541
- 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 });
2542
1248
  }
1249
+ if (r.path) links.push({ source: note.path, target: r.path, session: isSessionPath(r.path) });
2543
1250
  });
2544
1251
 
2545
1252
  const nodeArray = Array.from(nodes.values());
2546
- nodeArray.forEach(n => n.degree = 0);
2547
- links.forEach(l => {
2548
- const s = nodes.get(l.source);
2549
- const t = nodes.get(l.target);
2550
- if (s) s.degree++;
2551
- if (t) t.degree++;
2552
- });
2553
-
2554
1253
  if (nodeArray.length < 2) {
2555
- 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>';
2556
1255
  return;
2557
1256
  }
2558
1257
 
2559
- const isMaximized = document.getElementById('graph-section').classList.contains('maximized');
2560
- const width = container.clientWidth || 248;
2561
- const height = container.clientHeight || 200;
2562
- const margin = isMaximized ? 60 : 24;
2563
-
1258
+ const width = 100, height = 100;
2564
1259
  const svg = d3.select(container).append('svg')
2565
- .attr('viewBox', `0 0 ${width} ${height}`);
2566
-
2567
- const g = svg.append('g');
2568
-
2569
- svg.call(d3.zoom()
2570
- .scaleExtent([0.2, 4])
2571
- .on('zoom', (event) => g.attr('transform', event.transform))
2572
- );
1260
+ .attr('viewBox', '0 0 100 100')
1261
+ .attr('preserveAspectRatio', 'xMidYMid meet')
1262
+ .style('width', '100%').style('height', '100%');
2573
1263
 
2574
1264
  const simulation = d3.forceSimulation(nodeArray)
2575
- .force('link', d3.forceLink(links).id(d => d.id).distance(isMaximized ? 150 : 80))
2576
- .force('charge', d3.forceManyBody().strength(d => isMaximized ? -300 - (d.degree * 40) : -100 - (d.degree * 20)))
2577
- .force('center', d3.forceCenter(width / 2, height / 2))
2578
- .force('collision', d3.forceCollide().radius(d => {
2579
- const textWidth = d.title.length * (isMaximized ? 6 : 4.5);
2580
- return (isMaximized ? textWidth + 10 : 12) + (d.degree * 2);
2581
- }));
2582
-
2583
- const linkEl = g.selectAll('.graph-link')
2584
- .data(links).enter().append('line')
2585
- .attr('class', 'graph-link')
2586
- .attr('stroke-dasharray', d => d.dashed ? '3,3' : null)
2587
- .style('opacity', d => d.dashed ? 0.3 : 0.6);
2588
-
2589
- const node = g.selectAll('.graph-node')
2590
- .data(nodeArray).enter().append('g')
2591
- .attr('class', d => 'graph-node' + (d.active ? ' active' : ''))
2592
- .on('click', (e, d) => {
2593
- e.stopPropagation();
2594
- loadNote(d.id);
2595
- if (isMaximized) {
2596
- document.getElementById('graph-section').classList.remove('maximized');
2597
- isGraphMaximized = false;
2598
- renderGraph(currentNote);
2599
- }
2600
- })
2601
- .call(d3.drag()
2602
- .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
2603
- .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
2604
- .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
2605
- );
2606
-
2607
- node.append('circle').attr('r', d => d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2)));
2608
- 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);
2609
-
2610
- if (isMaximized) {
2611
- node.append('text')
2612
- .text(d => {
2613
- let hash = 0;
2614
- for (let i = 0; i < d.id.length; i++) hash = Math.imul(31, hash) + d.id.charCodeAt(i) | 0;
2615
- const v1 = ((Math.abs(hash) % 1000) / 1000).toFixed(3);
2616
- const v2 = ((Math.abs(hash * 13) % 1000) / 1000).toFixed(3);
2617
- const v3 = ((Math.abs(hash * 17) % 1000) / 1000).toFixed(3);
2618
- return `[${v1}, ${v2}, ${v3}, ...]`;
2619
- })
2620
- .attr('dx', d => (d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2))) + 6)
2621
- .attr('dy', 16)
2622
- .style('fill', 'var(--neon-purple)')
2623
- .style('font-size', '8px')
2624
- .style('opacity', '0.8')
2625
- .style('pointer-events', 'none')
2626
- .style('font-family', 'var(--font-mono)');
2627
-
2628
- node.append('text')
2629
- .text(d => d.id ? d.id.split('/').slice(0, -1).join('/') : '')
2630
- .attr('dx', d => (d.active ? 8 : Math.min(12, 3 + (d.degree * 1.2))) + 6)
2631
- .attr('dy', 26)
2632
- .style('fill', 'var(--text-muted)')
2633
- .style('font-size', '8px')
2634
- .style('pointer-events', 'none');
2635
- }
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));
2636
1269
 
2637
1270
  simulation.on('tick', () => {
2638
- linkEl
2639
- .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
2640
- .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
2641
- node.attr('transform', d => {
2642
- d.x = Math.max(margin, Math.min(width - margin, d.x));
2643
- d.y = Math.max(margin, Math.min(height - margin, d.y));
2644
- 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));
2645
1274
  });
2646
1275
  });
2647
- }
2648
-
2649
- // --- Graph Resize ---
2650
- let graphHeight = parseInt(localStorage.getItem('kyp-graph-h')) || 200;
2651
- let isGraphMaximized = false;
2652
1276
 
2653
- function initGraphResize() {
2654
- const container = document.getElementById('graph-container');
2655
- const section = document.getElementById('graph-section');
2656
- container.style.height = graphHeight + 'px';
1277
+ // Run simulation
1278
+ for (let i = 0; i < 120; i++) simulation.tick();
1279
+ simulation.stop();
2657
1280
 
2658
- document.getElementById('graph-max').addEventListener('click', () => {
2659
- isGraphMaximized = !isGraphMaximized;
2660
- section.classList.toggle('maximized', isGraphMaximized);
2661
- 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');
2662
1294
  });
2663
1295
 
2664
- document.getElementById('graph-shrink').addEventListener('click', () => {
2665
- graphHeight = Math.max(100, graphHeight - 50);
2666
- container.style.height = graphHeight + 'px';
2667
- localStorage.setItem('kyp-graph-h', graphHeight);
2668
- if (graphVisible && currentNote) renderGraph(currentNote);
2669
- });
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
+ }
2670
1318
 
2671
- document.getElementById('graph-grow').addEventListener('click', () => {
2672
- graphHeight = Math.min(600, graphHeight + 50);
2673
- container.style.height = graphHeight + 'px';
2674
- localStorage.setItem('kyp-graph-h', graphHeight);
2675
- if (graphVisible && currentNote) renderGraph(currentNote);
1319
+ g.on('click', () => { if (!n.center) loadNote(n.id); });
2676
1320
  });
1321
+ }
2677
1322
 
2678
- const handle = document.getElementById('graph-resize-handle');
2679
- handle.addEventListener('mousedown', (e) => {
2680
- e.preventDefault();
2681
- const startY = e.clientY;
2682
- const startH = graphHeight;
2683
- document.body.style.cursor = 'ns-resize';
2684
- document.body.style.userSelect = 'none';
2685
-
2686
- function onMove(ev) {
2687
- graphHeight = Math.max(100, Math.min(600, startH + (ev.clientY - startY)));
2688
- container.style.height = graphHeight + 'px';
2689
- }
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
+ }
2690
1331
 
2691
- function onUp() {
2692
- document.body.style.cursor = '';
2693
- document.body.style.userSelect = '';
2694
- localStorage.setItem('kyp-graph-h', graphHeight);
2695
- if (graphVisible && currentNote) renderGraph(currentNote);
2696
- document.removeEventListener('mousemove', onMove);
2697
- document.removeEventListener('mouseup', onUp);
2698
- }
1332
+ function renderSessionView(note) {
1333
+ const area = $('content-area');
1334
+ const content = note.content || '';
2699
1335
 
2700
- document.addEventListener('mousemove', onMove);
2701
- document.addEventListener('mouseup', onUp);
2702
- });
2703
- }
1336
+ // Extract summary
1337
+ let summary = extractSection(content, 'Summary');
2704
1338
 
2705
- // --- Search ---
2706
- let searchTimeout;
2707
- const searchInput = document.getElementById('search-input');
2708
- 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] || '';
2709
1343
 
2710
- searchInput.addEventListener('input', () => {
2711
- clearTimeout(searchTimeout);
2712
- const q = searchInput.value.trim();
2713
- if (!q) { searchResults.classList.remove('active'); return; }
2714
- searchTimeout = setTimeout(async () => {
2715
- const results = await fetchJSON(`/api/search?q=${encodeURIComponent(q)}`);
2716
- searchResults.innerHTML = '';
2717
- if (results.length === 0) {
2718
- searchResults.innerHTML = '<div class="search-result"><div class="sr-title" style="color:var(--text-muted)">No results</div></div>';
2719
- } else {
2720
- results.forEach(r => {
2721
- const div = document.createElement('div');
2722
- div.className = 'search-result';
2723
- 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>` : ''}`;
2724
- div.addEventListener('click', () => {
2725
- loadNote(r.path);
2726
- searchResults.classList.remove('active');
2727
- searchInput.value = '';
2728
- });
2729
- searchResults.appendChild(div);
2730
- });
2731
- }
2732
- searchResults.classList.add('active');
2733
- }, 200);
2734
- });
1344
+ const wordCount = content.split(/\s+/).filter(Boolean).length;
2735
1345
 
2736
- searchInput.addEventListener('focus', () => {
2737
- document.querySelector('.search-hint').style.display = 'none';
2738
- });
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');
2739
1352
 
2740
- searchInput.addEventListener('blur', () => {
2741
- if (!searchInput.value) document.querySelector('.search-hint').style.display = '';
2742
- });
1353
+ const sectionNames = ['Summary', 'PROMPTS', 'INVESTIGATED', 'LEARNED', 'COMPLETED', 'NEXT STEPS'];
1354
+ const presentSections = sectionNames.filter(s => content.includes(`## ${s}`));
2743
1355
 
2744
- document.addEventListener('click', (e) => {
2745
- if (!e.target.closest('.search-box')) searchResults.classList.remove('active');
2746
- });
1356
+ // Count prompt entries
1357
+ const promptEntries = promptsRaw.split(/^###\s+/m).filter(p => p.trim());
1358
+ const hasMultiplePrompts = promptEntries.length > 1;
2747
1359
 
2748
- // --- Tag Filter ---
2749
- document.getElementById('tag-filter-toggle').addEventListener('click', () => {
2750
- const body = document.getElementById('tag-filter-body');
2751
- const arrow = document.getElementById('tag-arrow');
2752
- body.classList.toggle('collapsed');
2753
- arrow.classList.toggle('open');
2754
- });
1360
+ const tagsHtml = (note.tags || ['session', 'auto-captured']).map(t =>
1361
+ `<button class="tag-chip"><span class="tc-hash">#</span>${t}</button>`
1362
+ ).join('');
2755
1363
 
2756
- document.getElementById('clear-filters').addEventListener('click', () => {
2757
- activeTagFilters.clear();
2758
- applyTagFilter();
2759
- });
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
+ `;
2760
1417
 
2761
- function renderTagCloud(tags) {
2762
- const cloud = document.getElementById('tag-cloud');
2763
- cloud.innerHTML = '';
2764
- const sorted = Object.entries(tags).sort((a, b) => b[1] - a[1]);
2765
- sorted.forEach(([tag, count]) => {
2766
- const chip = document.createElement('span');
2767
- chip.className = 'tag-chip' + (activeTagFilters.has(tag) ? ' selected' : '');
2768
- chip.innerHTML = `#${tag}<span class="tag-count">${count}</span>`;
2769
- chip.addEventListener('click', () => {
2770
- if (activeTagFilters.has(tag)) {
2771
- activeTagFilters.delete(tag);
2772
- } else {
2773
- activeTagFilters.add(tag);
2774
- }
2775
- 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);
2776
1426
  });
2777
- cloud.appendChild(chip);
2778
- });
1427
+ }
2779
1428
  }
2780
1429
 
2781
- function applyTagFilter() {
2782
- const activeEl = document.getElementById('active-tags');
2783
- const filterInfo = document.getElementById('filter-info');
2784
- const filterCount = document.getElementById('filter-count');
2785
-
2786
- activeEl.innerHTML = '';
2787
- activeTagFilters.forEach(tag => {
2788
- const el = document.createElement('span');
2789
- el.className = 'active-tag';
2790
- el.innerHTML = `#${tag}<span class="remove">&times;</span>`;
2791
- el.addEventListener('click', () => {
2792
- activeTagFilters.delete(tag);
2793
- 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());
2794
1482
  });
2795
- activeEl.appendChild(el);
2796
1483
  });
2797
1484
 
2798
- document.querySelectorAll('.tag-chip').forEach(chip => {
2799
- const tag = chip.textContent.replace('#', '').replace(/\d+$/, '');
2800
- chip.classList.toggle('selected', activeTagFilters.has(tag));
2801
- });
1485
+ buildFullGraph('force');
1486
+ }
2802
1487
 
2803
- const treeItems = document.querySelectorAll('.tree-item[data-path]');
2804
- let visibleCount = 0;
1488
+ async function buildFullGraph(layout) {
1489
+ const wrap = $('graph-svg-wrap');
1490
+ if (!wrap) return;
1491
+ wrap.innerHTML = '';
2805
1492
 
2806
- if (activeTagFilters.size === 0) {
2807
- treeItems.forEach(el => { el.style.display = ''; });
2808
- document.querySelectorAll('.tree-children').forEach(el => el.classList.remove('collapsed'));
2809
- filterInfo.style.display = 'none';
2810
- renderTagCloud(collectAllTags());
2811
- return;
1493
+ if (!_graphData) {
1494
+ _graphData = await fetchJSON('/api/graph');
2812
1495
  }
2813
1496
 
2814
- 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]));
2815
1500
 
2816
- for (const [path, note] of Object.entries(allNotes)) {
2817
- const noteTags = (note.tags || []).map(t => t.toLowerCase());
2818
- const matches = [...activeTagFilters].every(f => noteTags.includes(f.toLowerCase()));
2819
- 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();
2820
1558
  }
2821
1559
 
2822
- treeItems.forEach(el => {
2823
- const path = el.dataset.path;
2824
- if (matchingPaths.has(path)) {
2825
- el.style.display = '';
2826
- 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);
2827
1589
  } else {
2828
- 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);
2829
1594
  }
2830
- });
2831
1595
 
2832
- 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);
2833
1601
 
2834
- filterInfo.style.display = '';
2835
- filterCount.textContent = `${visibleCount} note${visibleCount !== 1 ? 's' : ''}`;
2836
- renderTagCloud(collectAllTags());
2837
- }
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
+ });
2838
1607
 
2839
- function collectAllTags() {
2840
- const tags = {};
2841
- for (const note of Object.values(allNotes)) {
2842
- (note.tags || []).forEach(t => { tags[t] = (tags[t] || 0) + 1; });
2843
- }
2844
- return tags;
1608
+ ng.on('mouseenter', () => updateGraphRail(n, links, nodes));
1609
+ });
2845
1610
  }
2846
1611
 
2847
- // --- Resizable Panels ---
2848
- function initResize() {
2849
- const layout = document.getElementById('layout');
2850
- const leftHandle = document.getElementById('resize-left');
2851
- 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;
2852
1616
 
2853
- let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 256;
2854
- 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
+ });
2855
1624
 
2856
- layout.style.setProperty('--sidebar-w', sidebarW + 'px');
2857
- 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
+ `;
2858
1633
 
2859
- function makeDraggable(handle, opts) {
2860
- handle.addEventListener('mousedown', (e) => {
2861
- e.preventDefault();
2862
- const startX = e.clientX;
2863
- const startW = opts.getWidth();
2864
- handle.classList.add('dragging');
2865
- 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
+ }
2866
1649
 
2867
- function onMove(e) {
2868
- const delta = e.clientX - startX;
2869
- const adjusted = opts.invert ? startW - delta : startW + delta;
2870
- const clamped = Math.max(opts.min, Math.min(opts.max, adjusted));
2871
- opts.setWidth(clamped);
2872
- }
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
+ }
2873
1661
 
2874
- function onUp() {
2875
- handle.classList.remove('dragging');
2876
- document.body.classList.remove('resizing');
2877
- opts.save();
2878
- if (graphVisible && currentPath) loadNote(currentPath);
2879
- document.removeEventListener('mousemove', onMove);
2880
- document.removeEventListener('mouseup', onUp);
2881
- }
1662
+ function closeEditor() { $('edit-overlay').classList.remove('active'); editingPath = null; }
2882
1663
 
2883
- document.addEventListener('mousemove', onMove);
2884
- document.addEventListener('mouseup', onUp);
2885
- });
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);
2886
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
+ });
2887
1705
 
2888
- makeDraggable(leftHandle, {
2889
- getWidth: () => sidebarW,
2890
- setWidth: (w) => { sidebarW = w; layout.style.setProperty('--sidebar-w', w + 'px'); },
2891
- save: () => localStorage.setItem('kyp-sidebar-w', sidebarW),
2892
- invert: false,
2893
- min: 180,
2894
- max: 400,
2895
- });
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
+ });
2896
1724
 
2897
- makeDraggable(rightHandle, {
2898
- getWidth: () => rightW,
2899
- setWidth: (w) => { rightW = w; layout.style.setProperty('--right-w', w + 'px'); },
2900
- save: () => localStorage.setItem('kyp-right-w', rightW),
2901
- invert: true,
2902
- min: 200,
2903
- max: 450,
2904
- });
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('');
2905
1732
  }
1733
+ function closeQuickSwitcher() { $('qs-overlay').classList.remove('active'); }
1734
+ $('qs-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeQuickSwitcher(); });
2906
1735
 
2907
- // --- Init ---
2908
- async function init() {
2909
- const [rawTreeData, sessionsData, stats] = await Promise.all([
2910
- fetchJSON('/api/tree'),
2911
- fetchJSON('/api/sessions'),
2912
- fetchJSON('/api/stats'),
2913
- ]);
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
+ }
2914
1763
 
2915
- treeData = rawTreeData;
1764
+ // ─── Search ──────────────────────────────────────────────────────────────────
1765
+ const searchInput = $('search-input');
1766
+ const searchDropdown = $('search-dropdown');
2916
1767
 
2917
- function walk(node) {
2918
- if (node.type === 'note') {
2919
- 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
+ });
2920
1785
  }
2921
- (node.children || []).forEach(walk);
2922
- }
2923
- walk(treeData);
1786
+ searchDropdown.classList.add('active');
1787
+ }, 200);
1788
+ });
2924
1789
 
2925
- const promises = Object.keys(allNotes).map(async path => {
2926
- try {
2927
- const note = await fetchJSON(`/api/note/${path}`);
2928
- if (!note.error) allNotes[path] = { title: note.title, tags: note.tags };
2929
- } catch {}
2930
- });
2931
- 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'); });
2932
1793
 
2933
- renderTree(treeData, document.getElementById('file-tree'));
2934
- renderSessionTree(sessionsData);
2935
- 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
+ });
2936
1801
 
2937
- document.getElementById('stats-bar').innerHTML = `
2938
- <span><span class="stat-val">${stats.notes}</span> notes</span>
2939
- <span><span class="stat-val">${stats.folders}</span> folders</span>
2940
- <span><span class="stat-val">${stats.tags}</span> tags</span>
2941
- <span><span class="stat-val">${stats.links}</span> links</span>
2942
- `;
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);
2943
1807
  }
2944
1808
 
2945
- // --- Auto-refresh: poll for vault changes ---
2946
- 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
+ }
2947
1843
 
2948
1844
  async function pollForChanges() {
2949
1845
  try {
@@ -2957,11 +1853,48 @@ async function pollForChanges() {
2957
1853
  } catch {}
2958
1854
  }
2959
1855
 
2960
- 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
+ }
2961
1895
 
2962
- initResize();
2963
- initGraphResize();
2964
1896
  init();
1897
+ setInterval(pollForChanges, 3000);
2965
1898
  </script>
2966
1899
  </body>
2967
1900
  </html>