somtum 1.3.0 → 1.3.2

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.
@@ -7,160 +7,153 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link
10
- href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
10
+ href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Fira+Code:wght@300;400;500&display=swap"
11
11
  rel="stylesheet"
12
12
  />
13
13
  <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
14
14
 
15
15
  <style>
16
- *,
17
- *::before,
18
- *::after {
19
- box-sizing: border-box;
20
- margin: 0;
21
- padding: 0;
22
- }
16
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
23
17
 
24
18
  :root {
25
- --bg: #0b0d12;
26
- --surface: #111318;
27
- --surface2: #181c24;
28
- --surface3: #1d2230;
29
- --border: #1f2535;
30
- --border2: #2a3245;
31
- --text: #dde1ea;
32
- --muted: #4d5870;
33
- --muted2: #6b7a96;
34
- --accent: #4f8dff;
35
- --accent-g: #27c27d;
36
- --accent-a: #f0a638;
37
- --accent-r: #ef5858;
38
- --accent-v: #9b72f5;
39
- --accent-c: #22d0c8;
40
- --radius: 8px;
41
- --sidebar: 300px;
42
- --header: 52px;
43
- }
44
-
45
- html,
46
- body {
47
- height: 100%;
48
- overflow: hidden;
19
+ --bg: #0c0a07;
20
+ --surface: #111009;
21
+ --surface2: #19160e;
22
+ --surface3: #201d13;
23
+ --border: #2d2918;
24
+ --border2: #3e3920;
25
+ --text: #ede8da;
26
+ --muted: #6b6350;
27
+ --muted2: #9e9278;
28
+ --accent: #f0a030;
29
+ --accent-g: #34d399;
30
+ --accent-a: #f0a030;
31
+ --accent-r: #e8685a;
32
+ --accent-v: #a78bfa;
33
+ --accent-c: #38bdf8;
34
+ --radius: 7px;
35
+ --sidebar: 304px;
36
+ --header: 52px;
37
+ }
38
+
39
+ html, body {
40
+ height: 100%; overflow: hidden;
49
41
  background: var(--bg);
50
42
  color: var(--text);
51
- font-family: 'Bricolage Grotesque', sans-serif;
43
+ font-family: 'Syne', sans-serif;
52
44
  font-size: 13px;
53
45
  line-height: 1.5;
54
46
  -webkit-font-smoothing: antialiased;
55
47
  }
56
48
 
57
- /* ── Scrollbars ─────────────────────────────────────────────── */
58
- ::-webkit-scrollbar {
59
- width: 4px;
60
- height: 4px;
61
- }
62
- ::-webkit-scrollbar-track {
63
- background: transparent;
64
- }
65
- ::-webkit-scrollbar-thumb {
66
- background: var(--border2);
67
- border-radius: 4px;
68
- }
69
- ::-webkit-scrollbar-thumb:hover {
70
- background: var(--muted);
49
+ /* Grain overlay */
50
+ body::after {
51
+ content: '';
52
+ position: fixed;
53
+ inset: 0;
54
+ pointer-events: none;
55
+ z-index: 9999;
56
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
57
+ opacity: 0.022;
58
+ background-size: 180px 180px;
71
59
  }
72
60
 
73
- /* ── Mono text ──────────────────────────────────────────────── */
74
- .mono {
75
- font-family: 'JetBrains Mono', monospace;
76
- }
61
+ /* ── Scrollbars ── */
62
+ ::-webkit-scrollbar { width: 3px; height: 3px; }
63
+ ::-webkit-scrollbar-track { background: transparent; }
64
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
65
+ ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
77
66
 
78
- /* ── Header ─────────────────────────────────────────────────── */
67
+ .mono { font-family: 'Fira Code', monospace; }
68
+
69
+ /* ══ HEADER ══════════════════════════════════════════════════════ */
79
70
  #header {
80
71
  height: var(--header);
81
- border-bottom: 1px solid var(--border);
82
- padding: 0 20px;
72
+ padding: 0 18px;
83
73
  display: flex;
84
74
  align-items: center;
85
- gap: 14px;
75
+ gap: 12px;
86
76
  flex-shrink: 0;
87
77
  background: var(--surface);
78
+ border-bottom: 1px solid var(--border);
79
+ position: relative;
88
80
  }
81
+ /* accent line under header */
82
+ #header::after {
83
+ content: '';
84
+ position: absolute;
85
+ bottom: -1px; left: 0;
86
+ width: 100%; height: 1px;
87
+ background: linear-gradient(90deg, var(--accent) 0%, transparent 40%);
88
+ opacity: 0.35;
89
+ }
90
+
89
91
  #logo {
90
- font-size: 19px;
92
+ font-size: 17px;
91
93
  font-weight: 800;
92
94
  color: var(--accent);
93
- letter-spacing: -0.04em;
95
+ letter-spacing: -0.02em;
94
96
  line-height: 1;
95
97
  display: flex;
96
98
  align-items: center;
97
- gap: 7px;
99
+ gap: 8px;
100
+ flex-shrink: 0;
98
101
  }
99
102
  #logo-mark {
100
- width: 26px;
101
- height: 26px;
102
- border-radius: 7px;
103
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent-v) 100%);
104
- display: flex;
105
- align-items: center;
106
- justify-content: center;
103
+ width: 28px; height: 28px;
104
+ border-radius: 8px;
105
+ background: linear-gradient(135deg, var(--accent) 0%, #e07820 100%);
106
+ display: flex; align-items: center; justify-content: center;
107
107
  flex-shrink: 0;
108
+ box-shadow: 0 0 16px rgba(240,160,48,0.3);
108
109
  }
109
- #logo-mark svg {
110
- width: 14px;
111
- height: 14px;
112
- color: #fff;
113
- }
114
- #header-sep {
115
- width: 1px;
116
- height: 20px;
117
- background: var(--border);
118
- }
110
+ #logo-mark svg { width: 14px; height: 14px; color: #0c0a07; }
111
+
112
+ #header-sep { width: 1px; height: 18px; background: var(--border2); }
113
+
119
114
  #project-label {
120
115
  font-size: 11px;
121
116
  color: var(--muted);
122
- font-family: 'JetBrains Mono', monospace;
117
+ font-family: 'Fira Code', monospace;
123
118
  white-space: nowrap;
124
119
  overflow: hidden;
125
120
  text-overflow: ellipsis;
126
- max-width: 240px;
127
- }
128
- #stat-bar {
129
- display: flex;
130
- gap: 6px;
131
- margin-left: auto;
121
+ max-width: 200px;
122
+ background: var(--surface2);
123
+ border: 1px solid var(--border);
124
+ padding: 3px 8px;
125
+ border-radius: 4px;
132
126
  }
127
+
128
+ #stat-bar { display: flex; gap: 4px; margin-left: auto; }
133
129
  .stat-pill {
134
- display: flex;
135
- align-items: center;
136
- gap: 8px;
137
- padding: 5px 11px;
138
- border-radius: 6px;
130
+ display: flex; align-items: center; gap: 6px;
131
+ padding: 4px 10px;
132
+ border-radius: 5px;
139
133
  background: var(--surface2);
140
134
  border: 1px solid var(--border);
141
135
  cursor: default;
136
+ transition: border-color 0.15s;
142
137
  }
138
+ .stat-pill:hover { border-color: var(--border2); }
143
139
  .stat-pill .s-lbl {
144
- font-size: 10px;
145
- font-weight: 600;
146
- text-transform: uppercase;
147
- letter-spacing: 0.08em;
140
+ font-size: 9px; font-weight: 700;
141
+ text-transform: uppercase; letter-spacing: 0.1em;
148
142
  color: var(--muted);
149
143
  }
150
144
  .stat-pill .s-val {
151
- font-family: 'JetBrains Mono', monospace;
152
- font-size: 13px;
153
- font-weight: 600;
145
+ font-family: 'Fira Code', monospace;
146
+ font-size: 12.5px; font-weight: 500;
154
147
  }
155
148
 
156
- /* ── Layout ─────────────────────────────────────────────────── */
149
+ /* ══ LAYOUT ══════════════════════════════════════════════════════ */
157
150
  #layout {
158
151
  display: flex;
159
152
  height: calc(100vh - var(--header));
160
153
  overflow: hidden;
161
154
  }
162
155
 
163
- /* ── Sidebar ─────────────────────────────────────────────────── */
156
+ /* ══ SIDEBAR ══════════════════════════════════════════════════════ */
164
157
  #sidebar {
165
158
  width: var(--sidebar);
166
159
  flex-shrink: 0;
@@ -175,319 +168,231 @@
175
168
  #search-wrap {
176
169
  padding: 10px;
177
170
  border-bottom: 1px solid var(--border);
178
- display: flex;
179
- flex-direction: column;
180
- gap: 8px;
181
- }
182
- .search-box {
183
- position: relative;
171
+ display: flex; flex-direction: column; gap: 8px;
172
+ background: var(--bg);
184
173
  }
174
+ .search-box { position: relative; }
185
175
  .search-box svg {
186
- position: absolute;
187
- left: 9px;
188
- top: 50%;
189
- transform: translateY(-50%);
190
- width: 14px;
191
- height: 14px;
192
- color: var(--muted);
193
- pointer-events: none;
176
+ position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
177
+ width: 13px; height: 13px; color: var(--muted); pointer-events: none;
194
178
  }
195
179
  #search-input {
196
180
  width: 100%;
197
- background: var(--bg);
198
- border: 1px solid var(--border);
181
+ background: var(--surface);
182
+ border: 1px solid var(--border2);
199
183
  border-radius: var(--radius);
200
- padding: 7px 9px 7px 30px;
184
+ padding: 7px 9px 7px 28px;
201
185
  font-size: 12.5px;
202
- font-family: 'JetBrains Mono', monospace;
186
+ font-family: 'Fira Code', monospace;
203
187
  color: var(--text);
204
188
  outline: none;
205
- transition:
206
- border-color 0.15s,
207
- box-shadow 0.15s;
208
- }
209
- #search-input::placeholder {
210
- color: var(--muted);
189
+ transition: border-color 0.15s, box-shadow 0.15s;
211
190
  }
191
+ #search-input::placeholder { color: var(--muted); }
212
192
  #search-input:focus {
213
193
  border-color: var(--accent);
214
- box-shadow: 0 0 0 2px rgba(79, 141, 255, 0.12);
194
+ box-shadow: 0 0 0 2px rgba(240,160,48,0.1);
215
195
  }
216
196
  #search-meta {
217
- display: flex;
218
- align-items: center;
219
- justify-content: space-between;
220
- }
221
- .strat-row {
222
- display: flex;
223
- gap: 4px;
197
+ display: flex; align-items: center; justify-content: space-between;
224
198
  }
199
+ .strat-row { display: flex; gap: 3px; }
225
200
  .strat-btn {
226
201
  padding: 3px 8px;
227
- font-size: 10px;
228
- font-weight: 600;
229
- border: 1px solid var(--border);
230
- border-radius: 4px;
202
+ font-size: 10px; font-weight: 600;
203
+ border: 1px solid var(--border2); border-radius: 4px;
231
204
  cursor: pointer;
232
- background: var(--surface2);
233
- color: var(--muted2);
205
+ background: var(--surface2); color: var(--muted2);
234
206
  transition: all 0.12s;
235
- font-family: 'Bricolage Grotesque', sans-serif;
236
- }
237
- .strat-btn:hover {
238
- color: var(--text);
239
- border-color: var(--border2);
207
+ font-family: 'Syne', sans-serif;
208
+ letter-spacing: 0.03em;
240
209
  }
210
+ .strat-btn:hover { color: var(--text); border-color: var(--muted); }
241
211
  .strat-btn.active {
242
- background: rgba(79, 141, 255, 0.1);
243
- border-color: rgba(79, 141, 255, 0.4);
212
+ background: rgba(240,160,48,0.1);
213
+ border-color: rgba(240,160,48,0.4);
244
214
  color: var(--accent);
245
215
  }
246
- #search-spinner {
247
- display: none;
248
- }
249
- #search-spinner svg {
250
- animation: spin 0.7s linear infinite;
251
- display: block;
252
- }
216
+ #search-spinner { display: none; }
217
+ #search-spinner svg { animation: spin 0.7s linear infinite; display: block; }
253
218
 
254
219
  /* Kind filters */
255
220
  #kind-filters {
256
221
  padding: 7px 10px;
257
222
  border-bottom: 1px solid var(--border);
258
- display: flex;
259
- flex-wrap: wrap;
260
- gap: 4px;
223
+ display: flex; flex-wrap: wrap; gap: 3px;
261
224
  flex-shrink: 0;
262
225
  }
263
226
  .kind-tag {
264
- padding: 2px 8px;
265
- font-size: 10px;
266
- font-weight: 600;
267
- border-radius: 4px;
268
- border: 1px solid transparent;
269
- cursor: pointer;
270
- transition: all 0.12s;
271
- color: var(--muted2);
272
- background: var(--surface2);
273
- }
274
- .kind-tag:hover {
275
- color: var(--text);
276
- border-color: var(--border2);
277
- }
278
- .kind-tag.active {
279
- color: #fff;
227
+ padding: 2px 8px; font-size: 10px; font-weight: 700;
228
+ border-radius: 4px; border: 1px solid transparent;
229
+ cursor: pointer; transition: all 0.12s;
230
+ color: var(--muted2); background: var(--surface2);
231
+ letter-spacing: 0.04em;
280
232
  }
233
+ .kind-tag:hover { color: var(--text); border-color: var(--border2); }
234
+ .kind-tag.active { color: #fff; }
281
235
 
282
236
  /* List header */
283
237
  #list-header {
284
- padding: 6px 10px 5px;
285
- display: flex;
286
- align-items: center;
287
- justify-content: space-between;
238
+ padding: 5px 10px 5px;
239
+ display: flex; align-items: center; justify-content: space-between;
288
240
  border-bottom: 1px solid var(--border);
289
241
  }
290
242
  #list-header .h-label {
291
- font-size: 10px;
292
- font-weight: 700;
293
- text-transform: uppercase;
294
- letter-spacing: 0.08em;
243
+ font-size: 9px; font-weight: 700;
244
+ text-transform: uppercase; letter-spacing: 0.12em;
295
245
  color: var(--muted);
296
246
  }
297
247
  #list-count {
298
- font-size: 10px;
299
- font-family: 'JetBrains Mono', monospace;
300
- color: var(--muted);
248
+ font-size: 10px; font-family: 'Fira Code', monospace; color: var(--muted);
301
249
  }
302
250
 
303
251
  /* Memory list */
304
252
  #memory-list {
305
- flex: 1;
306
- overflow-y: auto;
253
+ flex: 1; overflow-y: auto;
307
254
  padding: 6px;
308
- display: flex;
309
- flex-direction: column;
310
- gap: 3px;
255
+ display: flex; flex-direction: column; gap: 2px;
311
256
  }
312
257
 
313
258
  /* Memory card */
314
259
  .mem-card {
315
- padding: 9px 10px;
260
+ padding: 10px 10px 8px;
316
261
  border-radius: 6px;
317
262
  background: var(--surface2);
318
263
  border: 1px solid var(--border);
319
264
  cursor: pointer;
320
- transition:
321
- border-color 0.12s,
322
- background 0.12s;
265
+ transition: border-color 0.12s, background 0.12s, transform 0.1s;
323
266
  position: relative;
324
267
  overflow: hidden;
325
268
  }
269
+ /* kind color bar on left */
326
270
  .mem-card::before {
327
271
  content: '';
328
- position: absolute;
329
- left: 0;
330
- top: 0;
331
- bottom: 0;
332
- width: 3px;
333
- border-radius: 3px 0 0 3px;
272
+ position: absolute; left: 0; top: 0; bottom: 0;
273
+ width: 2.5px;
274
+ border-radius: 6px 0 0 6px;
275
+ background: var(--card-accent, var(--muted));
276
+ opacity: 0.6;
277
+ transition: opacity 0.12s;
334
278
  }
335
279
  .mem-card:hover {
336
280
  border-color: var(--border2);
337
281
  background: var(--surface3);
338
282
  }
283
+ .mem-card:hover::before { opacity: 1; }
339
284
  .mem-card.selected {
340
285
  border-color: var(--accent);
286
+ background: rgba(240,160,48,0.04);
341
287
  }
342
- .mem-card.selected::before {
343
- background: var(--accent);
344
- }
288
+ .mem-card.selected::before { background: var(--accent); opacity: 1; }
345
289
 
346
290
  .mc-head {
347
- display: flex;
348
- align-items: flex-start;
349
- gap: 7px;
350
- margin-bottom: 3px;
291
+ display: flex; align-items: flex-start; gap: 6px;
292
+ margin-bottom: 4px;
351
293
  }
352
294
  .mc-title {
353
- font-size: 12.5px;
354
- font-weight: 600;
355
- color: var(--text);
356
- line-height: 1.35;
357
- flex: 1;
358
- min-width: 0;
295
+ font-size: 12.5px; font-weight: 600; color: var(--text);
296
+ line-height: 1.35; flex: 1; min-width: 0;
359
297
  }
360
298
  .mc-kind {
361
299
  flex-shrink: 0;
362
- font-size: 9px;
363
- font-weight: 700;
364
- text-transform: uppercase;
365
- letter-spacing: 0.08em;
366
- padding: 2px 6px;
367
- border-radius: 3px;
300
+ font-size: 8.5px; font-weight: 700;
301
+ text-transform: uppercase; letter-spacing: 0.1em;
302
+ padding: 2px 6px; border-radius: 3px;
368
303
  }
369
304
  .mc-body {
370
- font-size: 11px;
371
- color: var(--muted2);
372
- line-height: 1.4;
305
+ font-size: 11px; color: var(--muted2);
306
+ line-height: 1.45;
373
307
  overflow: hidden;
374
308
  display: -webkit-box;
375
309
  -webkit-line-clamp: 2;
376
310
  -webkit-box-orient: vertical;
377
311
  }
378
312
  .mc-footer {
379
- display: flex;
380
- align-items: center;
381
- gap: 5px;
382
- margin-top: 5px;
313
+ display: flex; align-items: center; gap: 5px; margin-top: 6px;
383
314
  }
384
315
  .mc-time {
385
- font-size: 10px;
386
- color: var(--muted);
387
- font-family: 'JetBrains Mono', monospace;
388
- }
389
- .mc-tags {
390
- display: flex;
391
- gap: 3px;
392
- flex-wrap: wrap;
393
- flex: 1;
316
+ font-size: 9.5px; color: var(--muted);
317
+ font-family: 'Fira Code', monospace;
394
318
  }
319
+ .mc-tags { display: flex; gap: 3px; flex-wrap: wrap; flex: 1; }
395
320
  .mc-tag {
396
- font-size: 9px;
397
- font-weight: 600;
321
+ font-size: 9px; font-weight: 600;
398
322
  padding: 1px 5px;
399
323
  background: var(--surface3);
400
324
  border: 1px solid var(--border2);
401
- border-radius: 3px;
402
- color: var(--muted2);
325
+ border-radius: 3px; color: var(--muted);
326
+ font-family: 'Fira Code', monospace;
403
327
  }
404
328
  .mc-forget {
405
329
  display: none;
406
- width: 20px;
407
- height: 20px;
408
- align-items: center;
409
- justify-content: center;
410
- border-radius: 4px;
411
- cursor: pointer;
412
- background: rgba(239, 88, 88, 0.08);
413
- border: 1px solid rgba(239, 88, 88, 0.2);
330
+ width: 20px; height: 20px;
331
+ align-items: center; justify-content: center;
332
+ border-radius: 4px; cursor: pointer;
333
+ background: rgba(232,104,90,0.08);
334
+ border: 1px solid rgba(232,104,90,0.2);
414
335
  color: var(--accent-r);
415
- font-size: 10px;
336
+ font-size: 13px; font-weight: 300;
416
337
  transition: background 0.12s;
417
338
  flex-shrink: 0;
418
- position: absolute;
419
- top: 7px;
420
- right: 7px;
421
- }
422
- .mem-card:hover .mc-forget {
423
- display: flex;
424
- }
425
- .mc-forget:hover {
426
- background: rgba(239, 88, 88, 0.18);
339
+ position: absolute; top: 7px; right: 7px;
340
+ line-height: 1;
427
341
  }
342
+ .mem-card:hover .mc-forget { display: flex; }
343
+ .mc-forget:hover { background: rgba(232,104,90,0.2); }
428
344
 
429
- /* ── Main panel ──────────────────────────────────────────────── */
345
+ /* ══ MAIN ══════════════════════════════════════════════════════════ */
430
346
  #main {
431
- flex: 1;
432
- display: flex;
433
- flex-direction: column;
434
- overflow: hidden;
435
- min-width: 0;
347
+ flex: 1; display: flex; flex-direction: column;
348
+ overflow: hidden; min-width: 0;
436
349
  }
437
350
 
438
351
  /* Tab bar */
439
352
  #tab-bar {
440
- height: 42px;
441
- border-bottom: 1px solid var(--border);
442
- padding: 0 8px;
443
- display: flex;
444
- align-items: center;
445
- flex-shrink: 0;
353
+ height: 40px;
354
+ padding: 0 14px;
355
+ display: flex; align-items: center; flex-shrink: 0;
446
356
  background: var(--surface);
447
- gap: 2px;
357
+ border-bottom: 1px solid var(--border);
358
+ gap: 0;
448
359
  }
449
360
  .tab-btn {
450
- padding: 6px 14px;
451
- font-size: 11.5px;
452
- font-weight: 600;
361
+ padding: 0 14px;
362
+ height: 100%;
363
+ font-size: 11.5px; font-weight: 600;
453
364
  color: var(--muted2);
454
365
  border: none;
455
- border-radius: 5px;
456
366
  background: none;
457
367
  cursor: pointer;
458
- transition:
459
- color 0.12s,
460
- background 0.12s;
461
- }
462
- .tab-btn:hover {
463
- color: var(--text);
464
- background: var(--surface2);
368
+ transition: color 0.12s;
369
+ position: relative;
370
+ font-family: 'Syne', sans-serif;
371
+ letter-spacing: 0.04em;
465
372
  }
466
- .tab-btn.active {
467
- color: var(--accent);
468
- background: rgba(79, 141, 255, 0.1);
373
+ .tab-btn::after {
374
+ content: '';
375
+ position: absolute;
376
+ bottom: 0; left: 14px; right: 14px;
377
+ height: 2px;
378
+ background: var(--accent);
379
+ border-radius: 2px 2px 0 0;
380
+ opacity: 0;
381
+ transition: opacity 0.15s;
469
382
  }
383
+ .tab-btn:hover { color: var(--text); }
384
+ .tab-btn.active { color: var(--accent); }
385
+ .tab-btn.active::after { opacity: 1; }
470
386
  #tab-hint {
471
387
  margin-left: auto;
472
- font-size: 10px;
473
- color: var(--muted);
474
- padding-right: 10px;
475
- font-family: 'JetBrains Mono', monospace;
388
+ font-size: 10px; color: var(--muted);
389
+ font-family: 'Fira Code', monospace;
390
+ padding-right: 4px;
476
391
  }
477
392
 
478
393
  /* Tab panes */
479
- #tab-graph {
480
- flex: 1;
481
- display: flex;
482
- flex-direction: column;
483
- overflow: hidden;
484
- }
485
- #tab-analytics {
486
- display: none;
487
- flex: 1;
488
- overflow-y: auto;
489
- padding: 14px;
490
- }
394
+ #tab-graph { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
395
+ #tab-analytics { display: none; flex: 1; overflow-y: auto; padding: 14px; }
491
396
 
492
397
  /* Graph */
493
398
  #graph-container {
@@ -495,8 +400,20 @@
495
400
  margin: 10px;
496
401
  border: 1px solid var(--border);
497
402
  border-radius: var(--radius);
498
- background: #08090f;
403
+ background: #09080500;
499
404
  overflow: hidden;
405
+ position: relative;
406
+ }
407
+ /* ambient glow behind graph */
408
+ #graph-container::before {
409
+ content: '';
410
+ position: absolute;
411
+ top: 50%; left: 50%;
412
+ width: 400px; height: 300px;
413
+ transform: translate(-50%, -50%);
414
+ background: radial-gradient(ellipse, rgba(240,160,48,0.04) 0%, transparent 70%);
415
+ pointer-events: none;
416
+ z-index: 0;
500
417
  }
501
418
 
502
419
  /* Detail panel */
@@ -504,90 +421,59 @@
504
421
  display: none;
505
422
  border-top: 1px solid var(--border);
506
423
  background: var(--surface);
507
- overflow-y: auto;
508
- flex-shrink: 0;
509
- max-height: 38vh;
424
+ overflow-y: auto; flex-shrink: 0;
425
+ max-height: 36vh;
426
+ animation: slideUp 0.18s ease forwards;
510
427
  }
511
- #detail-inner {
512
- padding: 16px 20px;
428
+ @keyframes slideUp {
429
+ from { opacity: 0; transform: translateY(6px); }
430
+ to { opacity: 1; transform: translateY(0); }
513
431
  }
432
+ #detail-inner { padding: 16px 20px; }
514
433
  .detail-head {
515
- display: flex;
516
- align-items: flex-start;
517
- gap: 10px;
518
- margin-bottom: 12px;
434
+ display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;
519
435
  }
520
436
  .detail-title {
521
- font-size: 15px;
522
- font-weight: 700;
523
- color: var(--text);
524
- flex: 1;
525
- line-height: 1.3;
437
+ font-size: 15px; font-weight: 700; color: var(--text);
438
+ flex: 1; line-height: 1.3;
526
439
  }
527
440
  .detail-meta {
528
- display: flex;
529
- gap: 16px;
530
- margin-bottom: 12px;
531
- font-size: 11px;
532
- color: var(--muted);
533
- font-family: 'JetBrains Mono', monospace;
441
+ display: flex; gap: 16px; margin-bottom: 12px;
442
+ font-size: 10.5px; color: var(--muted);
443
+ font-family: 'Fira Code', monospace;
534
444
  }
535
445
  .detail-body {
536
- font-size: 12.5px;
537
- color: var(--muted2);
538
- line-height: 1.65;
539
- white-space: pre-wrap;
540
- word-break: break-word;
446
+ font-size: 12.5px; color: var(--muted2);
447
+ line-height: 1.7; white-space: pre-wrap; word-break: break-word;
541
448
  margin-bottom: 12px;
542
449
  }
543
- .detail-chips {
544
- display: flex;
545
- flex-wrap: wrap;
546
- gap: 5px;
547
- }
450
+ .detail-chips { display: flex; flex-wrap: wrap; gap: 5px; }
548
451
  .detail-chip {
549
- font-size: 10px;
550
- font-weight: 500;
551
- padding: 2px 8px;
552
- border-radius: 4px;
553
- background: var(--surface2);
554
- border: 1px solid var(--border2);
452
+ font-size: 10px; font-weight: 500;
453
+ padding: 2px 8px; border-radius: 4px;
454
+ background: var(--surface2); border: 1px solid var(--border2);
555
455
  color: var(--muted2);
456
+ font-family: 'Fira Code', monospace;
556
457
  }
557
458
  .detail-chip.file {
558
- color: var(--accent-c);
559
- border-color: rgba(34, 208, 200, 0.3);
560
- background: rgba(34, 208, 200, 0.07);
459
+ color: var(--accent-c); border-color: rgba(56,189,248,0.3);
460
+ background: rgba(56,189,248,0.06);
561
461
  }
562
462
  .detail-close {
563
- width: 24px;
564
- height: 24px;
565
- border-radius: 5px;
463
+ width: 24px; height: 24px; border-radius: 5px;
566
464
  flex-shrink: 0;
567
- display: flex;
568
- align-items: center;
569
- justify-content: center;
570
- background: var(--surface2);
571
- border: 1px solid var(--border);
572
- cursor: pointer;
573
- color: var(--muted2);
574
- transition: color 0.12s;
575
- }
576
- .detail-close:hover {
577
- color: var(--text);
465
+ display: flex; align-items: center; justify-content: center;
466
+ background: var(--surface2); border: 1px solid var(--border);
467
+ cursor: pointer; color: var(--muted2);
468
+ transition: color 0.12s, border-color 0.12s;
578
469
  }
470
+ .detail-close:hover { color: var(--text); border-color: var(--border2); }
579
471
 
580
472
  /* Analytics */
581
473
  .a-grid {
582
- display: grid;
583
- grid-template-columns: 1fr 1fr;
584
- gap: 10px;
585
- }
586
- @media (max-width: 900px) {
587
- .a-grid {
588
- grid-template-columns: 1fr;
589
- }
474
+ display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
590
475
  }
476
+ @media (max-width: 900px) { .a-grid { grid-template-columns: 1fr; } }
591
477
  .a-card {
592
478
  background: var(--surface);
593
479
  border: 1px solid var(--border);
@@ -595,215 +481,97 @@
595
481
  padding: 16px;
596
482
  }
597
483
  .a-title {
598
- font-size: 10px;
599
- font-weight: 700;
600
- text-transform: uppercase;
601
- letter-spacing: 0.1em;
602
- color: var(--muted);
603
- margin-bottom: 14px;
484
+ font-size: 9px; font-weight: 700;
485
+ text-transform: uppercase; letter-spacing: 0.14em;
486
+ color: var(--muted); margin-bottom: 14px;
604
487
  }
605
488
  .bar-row {
606
- display: flex;
607
- align-items: center;
608
- gap: 8px;
609
- margin-bottom: 8px;
489
+ display: flex; align-items: center; gap: 8px; margin-bottom: 9px;
610
490
  }
611
491
  .bar-label {
612
- font-size: 11.5px;
613
- font-weight: 600;
614
- width: 100px;
615
- flex-shrink: 0;
492
+ font-size: 11.5px; font-weight: 600;
493
+ width: 100px; flex-shrink: 0;
616
494
  }
617
495
  .bar-track {
618
- flex: 1;
619
- height: 4px;
620
- background: var(--surface3);
621
- border-radius: 2px;
622
- overflow: hidden;
496
+ flex: 1; height: 3px;
497
+ background: var(--surface3); border-radius: 2px; overflow: hidden;
623
498
  }
624
499
  .bar-fill {
625
- height: 100%;
626
- border-radius: 2px;
627
- width: 0;
628
- transition: width 0.65s cubic-bezier(0.16, 1, 0.3, 1);
500
+ height: 100%; border-radius: 2px;
501
+ width: 0; transition: width 0.7s cubic-bezier(0.16,1,0.3,1);
629
502
  }
630
503
  .bar-count {
631
- font-size: 11px;
632
- font-family: 'JetBrains Mono', monospace;
633
- color: var(--muted2);
634
- width: 36px;
635
- text-align: right;
636
- flex-shrink: 0;
504
+ font-size: 11px; font-family: 'Fira Code', monospace;
505
+ color: var(--muted2); width: 36px; text-align: right; flex-shrink: 0;
637
506
  }
638
507
  .cache-stat {
639
- display: flex;
640
- justify-content: space-between;
641
- align-items: center;
642
- padding: 7px 0;
643
- border-bottom: 1px solid var(--border);
644
- }
645
- .cache-stat:last-child {
646
- border-bottom: none;
647
- }
648
- .cache-stat-lbl {
649
- font-size: 12px;
650
- color: var(--muted2);
651
- }
652
- .cache-stat-val {
653
- font-family: 'JetBrains Mono', monospace;
654
- font-size: 13px;
655
- font-weight: 600;
508
+ display: flex; justify-content: space-between; align-items: center;
509
+ padding: 8px 0; border-bottom: 1px solid var(--border);
656
510
  }
511
+ .cache-stat:last-child { border-bottom: none; }
512
+ .cache-stat-lbl { font-size: 12px; color: var(--muted2); }
513
+ .cache-stat-val { font-family: 'Fira Code', monospace; font-size: 13px; font-weight: 500; }
657
514
  .file-row {
658
- display: flex;
659
- align-items: center;
660
- gap: 8px;
661
- padding: 6px 0;
662
- border-bottom: 1px solid var(--border);
663
- }
664
- .file-row:last-child {
665
- border-bottom: none;
515
+ display: flex; align-items: center; gap: 8px;
516
+ padding: 6px 0; border-bottom: 1px solid var(--border);
666
517
  }
518
+ .file-row:last-child { border-bottom: none; }
667
519
  .file-name {
668
- font-size: 11px;
669
- font-family: 'JetBrains Mono', monospace;
670
- color: var(--text);
671
- flex: 1;
672
- overflow: hidden;
673
- text-overflow: ellipsis;
674
- white-space: nowrap;
675
- min-width: 0;
520
+ font-size: 11px; font-family: 'Fira Code', monospace;
521
+ color: var(--text); flex: 1;
522
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
676
523
  }
677
524
  .file-count {
678
- font-size: 11px;
679
- font-family: 'JetBrains Mono', monospace;
680
- color: var(--muted2);
681
- flex-shrink: 0;
525
+ font-size: 11px; font-family: 'Fira Code', monospace;
526
+ color: var(--muted2); flex-shrink: 0;
682
527
  }
683
528
 
684
529
  /* Empty state */
685
530
  .empty {
686
- display: flex;
687
- flex-direction: column;
688
- align-items: center;
689
- justify-content: center;
690
- gap: 10px;
691
- padding: 48px 24px;
692
- color: var(--muted);
693
- text-align: center;
694
- }
695
- .empty-icon {
696
- opacity: 0.25;
697
- margin-bottom: 2px;
698
- }
699
- .empty-title {
700
- font-size: 13px;
701
- font-weight: 600;
702
- color: var(--muted2);
531
+ display: flex; flex-direction: column; align-items: center;
532
+ justify-content: center; gap: 8px;
533
+ padding: 48px 24px; color: var(--muted); text-align: center;
703
534
  }
535
+ .empty-icon { opacity: 0.2; margin-bottom: 4px; }
536
+ .empty-title { font-size: 13px; font-weight: 600; color: var(--muted2); }
704
537
  .empty-sub {
705
- font-size: 11.5px;
706
- color: var(--muted);
707
- max-width: 220px;
708
- line-height: 1.5;
538
+ font-size: 11.5px; color: var(--muted);
539
+ max-width: 220px; line-height: 1.55;
709
540
  }
710
541
 
711
542
  /* Skeleton */
712
543
  .skel {
713
- background: var(--surface2);
714
- border-radius: 5px;
715
- animation: shimmer 1.5s ease-in-out infinite;
544
+ background: var(--surface2); border-radius: 5px;
545
+ animation: shimmer 1.8s ease-in-out infinite;
716
546
  }
717
547
 
718
- /* Animations */
719
- @keyframes spin {
720
- to {
721
- transform: rotate(360deg);
722
- }
723
- }
548
+ /* ── Animations ── */
549
+ @keyframes spin { to { transform: rotate(360deg); } }
724
550
  @keyframes shimmer {
725
- 0%,
726
- 100% {
727
- opacity: 0.4;
728
- }
729
- 50% {
730
- opacity: 0.7;
731
- }
551
+ 0%, 100% { opacity: 0.35; }
552
+ 50% { opacity: 0.6; }
732
553
  }
733
554
  @keyframes fadeIn {
734
- from {
735
- opacity: 0;
736
- transform: translateY(4px);
737
- }
738
- to {
739
- opacity: 1;
740
- transform: translateY(0);
741
- }
742
- }
743
- .fade-in {
744
- animation: fadeIn 0.16s ease forwards;
745
- }
746
-
747
- /* Kind colour map */
748
- .kind-decision {
749
- background: rgba(79, 141, 255, 0.12);
750
- color: #4f8dff;
751
- }
752
- .kind-learning {
753
- background: rgba(155, 114, 245, 0.12);
754
- color: #9b72f5;
755
- }
756
- .kind-bugfix {
757
- background: rgba(239, 88, 88, 0.12);
758
- color: #ef5858;
759
- }
760
- .kind-command {
761
- background: rgba(240, 166, 56, 0.12);
762
- color: #f0a638;
763
- }
764
- .kind-file_summary {
765
- background: rgba(34, 208, 200, 0.1);
766
- color: #22d0c8;
767
- }
768
- .kind-other {
769
- background: rgba(107, 122, 150, 0.12);
770
- color: #6b7a96;
771
- }
772
- .kf-decision {
773
- border-color: rgba(79, 141, 255, 0.3);
774
- background: rgba(79, 141, 255, 0.08);
775
- color: #4f8dff;
776
- }
777
- .kf-learning {
778
- border-color: rgba(155, 114, 245, 0.3);
779
- background: rgba(155, 114, 245, 0.08);
780
- color: #9b72f5;
781
- }
782
- .kf-bugfix {
783
- border-color: rgba(239, 88, 88, 0.3);
784
- background: rgba(239, 88, 88, 0.08);
785
- color: #ef5858;
786
- }
787
- .kf-command {
788
- border-color: rgba(240, 166, 56, 0.3);
789
- background: rgba(240, 166, 56, 0.08);
790
- color: #f0a638;
791
- }
792
- .kf-file_summary {
793
- border-color: rgba(34, 208, 200, 0.3);
794
- background: rgba(34, 208, 200, 0.07);
795
- color: #22d0c8;
796
- }
797
- .kf-other {
798
- border-color: rgba(107, 122, 150, 0.3);
799
- background: rgba(107, 122, 150, 0.08);
800
- color: #6b7a96;
801
- }
802
- .kf-all {
803
- border-color: rgba(79, 141, 255, 0.3);
804
- background: rgba(79, 141, 255, 0.08);
805
- color: #4f8dff;
806
- }
555
+ from { opacity: 0; transform: translateY(3px); }
556
+ to { opacity: 1; transform: translateY(0); }
557
+ }
558
+ .fade-in { animation: fadeIn 0.14s ease forwards; }
559
+
560
+ /* ── Kind colour map ── */
561
+ .kind-decision { background: rgba(79,141,255,0.12); color: #6ba3ff; }
562
+ .kind-learning { background: rgba(167,139,250,0.12); color: #a78bfa; }
563
+ .kind-bugfix { background: rgba(232,104,90,0.12); color: #e8685a; }
564
+ .kind-command { background: rgba(240,160,48,0.12); color: #f0a030; }
565
+ .kind-file_summary { background: rgba(56,189,248,0.1); color: #38bdf8; }
566
+ .kind-other { background: rgba(107,99,80,0.15); color: #9e9278; }
567
+
568
+ .kf-decision { border-color: rgba(79,141,255,0.3); background: rgba(79,141,255,0.07); color: #6ba3ff; }
569
+ .kf-learning { border-color: rgba(167,139,250,0.3); background: rgba(167,139,250,0.07); color: #a78bfa; }
570
+ .kf-bugfix { border-color: rgba(232,104,90,0.3); background: rgba(232,104,90,0.07); color: #e8685a; }
571
+ .kf-command { border-color: rgba(240,160,48,0.3); background: rgba(240,160,48,0.07); color: #f0a030; }
572
+ .kf-file_summary { border-color: rgba(56,189,248,0.3); background: rgba(56,189,248,0.06); color: #38bdf8; }
573
+ .kf-other { border-color: rgba(107,99,80,0.3); background: rgba(107,99,80,0.07); color: #9e9278; }
574
+ .kf-all { border-color: rgba(240,160,48,0.35); background: rgba(240,160,48,0.08); color: #f0a030; }
807
575
  </style>
808
576
  </head>
809
577
  <body>
@@ -811,17 +579,8 @@
811
579
  <header id="header">
812
580
  <div id="logo">
813
581
  <div id="logo-mark">
814
- <svg
815
- viewBox="0 0 24 24"
816
- fill="none"
817
- stroke="currentColor"
818
- stroke-width="2.2"
819
- stroke-linecap="round"
820
- stroke-linejoin="round"
821
- >
822
- <path
823
- d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.46L7 19M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.46l.04-.54"
824
- />
582
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
583
+ <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.46L7 19M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.46l.04-.54" />
825
584
  </svg>
826
585
  </div>
827
586
  somtum
@@ -831,112 +590,84 @@
831
590
  <div id="stat-bar">
832
591
  <div class="stat-pill">
833
592
  <span class="s-lbl">Memories</span>
834
- <span id="s-mem" class="s-val" style="color: var(--accent-g)">—</span>
593
+ <span id="s-mem" class="s-val" style="color:var(--accent-g)">—</span>
835
594
  </div>
836
595
  <div class="stat-pill">
837
596
  <span class="s-lbl">Saved</span>
838
- <span id="s-saved" class="s-val" style="color: var(--accent-a)">—</span>
597
+ <span id="s-saved" class="s-val" style="color:var(--accent)">—</span>
839
598
  </div>
840
599
  <div class="stat-pill">
841
600
  <span class="s-lbl">Spent</span>
842
- <span id="s-spent" class="s-val" style="color: var(--muted2)">—</span>
601
+ <span id="s-spent" class="s-val" style="color:var(--muted2)">—</span>
843
602
  </div>
844
603
  <div class="stat-pill">
845
604
  <span class="s-lbl">ROI</span>
846
- <span id="s-roi" class="s-val" style="color: var(--accent)">—</span>
605
+ <span id="s-roi" class="s-val" style="color:var(--accent-v)">—</span>
847
606
  </div>
848
607
  <div class="stat-pill">
849
608
  <span class="s-lbl">Cache</span>
850
- <span id="s-cache" class="s-val" style="color: var(--accent-v)">—</span>
609
+ <span id="s-cache" class="s-val" style="color:var(--accent-c)">—</span>
851
610
  </div>
852
611
  </div>
853
612
  </header>
854
613
 
855
614
  <!-- ═══ LAYOUT ══════════════════════════════════════════════════════ -->
856
615
  <div id="layout">
857
- <!-- ─── SIDEBAR ───────────────────────────────────────────────── -->
616
+ <!-- ─── SIDEBAR ─────────────────────────────────────────────────── -->
858
617
  <aside id="sidebar">
859
- <!-- Search -->
860
618
  <div id="search-wrap">
861
619
  <div class="search-box">
862
620
  <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
863
- <circle cx="11" cy="11" r="7" />
864
- <path d="M21 21l-5-5" stroke-linecap="round" />
621
+ <circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5" stroke-linecap="round"/>
865
622
  </svg>
866
- <input
867
- id="search-input"
868
- type="text"
869
- placeholder="Search memories…"
870
- autocomplete="off"
871
- spellcheck="false"
872
- />
623
+ <input id="search-input" type="text" placeholder="Search memories…" autocomplete="off" spellcheck="false" />
873
624
  </div>
874
625
  <div id="search-meta">
875
626
  <div class="strat-row" id="strategy-btns"></div>
876
627
  <div id="search-spinner">
877
- <svg
878
- width="13"
879
- height="13"
880
- viewBox="0 0 24 24"
881
- fill="none"
882
- stroke="var(--accent)"
883
- stroke-width="2.5"
884
- >
885
- <path
886
- d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"
887
- />
628
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5">
629
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
888
630
  </svg>
889
631
  </div>
890
632
  </div>
891
633
  </div>
892
634
 
893
- <!-- Kind filters -->
894
635
  <div id="kind-filters"></div>
895
636
 
896
- <!-- List header -->
897
637
  <div id="list-header">
898
638
  <span class="h-label">Memories</span>
899
639
  <span id="list-count"></span>
900
640
  </div>
901
641
 
902
- <!-- Memory list -->
903
642
  <div id="memory-list">
904
- <div class="skel" style="height: 68px; border-radius: 6px"></div>
905
- <div class="skel" style="height: 56px; border-radius: 6px; margin-top: 3px"></div>
906
- <div class="skel" style="height: 72px; border-radius: 6px; margin-top: 3px"></div>
643
+ <div class="skel" style="height:66px;border-radius:6px"></div>
644
+ <div class="skel" style="height:54px;border-radius:6px;margin-top:2px"></div>
645
+ <div class="skel" style="height:70px;border-radius:6px;margin-top:2px"></div>
907
646
  </div>
908
647
  </aside>
909
648
 
910
- <!-- ─── MAIN ──────────────────────────────────────────────────── -->
649
+ <!-- ─── MAIN ─────────────────────────────────────────────────────── -->
911
650
  <main id="main">
912
- <!-- Tab bar -->
913
651
  <div id="tab-bar">
914
652
  <button class="tab-btn active" data-tab="graph">Graph</button>
915
653
  <button class="tab-btn" data-tab="analytics">Analytics</button>
916
654
  <span id="tab-hint"></span>
917
655
  </div>
918
656
 
919
- <!-- Panes wrapper -->
920
- <div
921
- style="flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0"
922
- >
923
- <!-- GRAPH TAB -->
657
+ <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0">
924
658
  <div id="tab-graph">
925
659
  <div id="graph-container"></div>
926
660
  </div>
927
-
928
- <!-- ANALYTICS TAB -->
929
661
  <div id="tab-analytics">
930
662
  <div class="a-grid" id="analytics-grid">
931
- <div class="skel" style="height: 180px"></div>
932
- <div class="skel" style="height: 180px"></div>
933
- <div class="skel" style="height: 200px"></div>
934
- <div class="skel" style="height: 200px"></div>
663
+ <div class="skel" style="height:180px"></div>
664
+ <div class="skel" style="height:180px"></div>
665
+ <div class="skel" style="height:200px"></div>
666
+ <div class="skel" style="height:200px"></div>
935
667
  </div>
936
668
  </div>
937
669
  </div>
938
670
 
939
- <!-- Detail panel -->
940
671
  <div id="detail-panel">
941
672
  <div id="detail-inner"></div>
942
673
  </div>
@@ -946,27 +677,20 @@
946
677
  <script>
947
678
  // ─── State ───────────────────────────────────────────────────────────────────
948
679
  const S = {
949
- data: null,
950
- fullStats: null,
951
- activeTab: 'graph',
952
- activeKind: 'all',
953
- selectedId: null,
954
- searchQuery: '',
955
- searchResults: null,
956
- strategy: 'bm25',
957
- debounce: null,
958
- network: null,
959
- analyticsRendered: false,
680
+ data: null, fullStats: null, activeTab: 'graph',
681
+ activeKind: 'all', selectedId: null, searchQuery: '',
682
+ searchResults: null, strategy: 'bm25', debounce: null,
683
+ network: null, analyticsRendered: false,
960
684
  };
961
685
 
962
- // ─── Kind colours (for vis-network nodes) ────────────────────────────────────
686
+ // ─── Kind colours (vis-network nodes) ────────────────────────────────────────
963
687
  const KIND_COLOR = {
964
- decision: { bg: '#4f8dff', border: '#2d6edd' },
965
- learning: { bg: '#9b72f5', border: '#7a50d4' },
966
- bugfix: { bg: '#ef5858', border: '#d43737' },
967
- command: { bg: '#f0a638', border: '#d48820' },
968
- file_summary: { bg: '#22d0c8', border: '#16a8a1' },
969
- other: { bg: '#4d5870', border: '#3a4560' },
688
+ decision: { bg: '#6ba3ff', border: '#4f8dff' },
689
+ learning: { bg: '#a78bfa', border: '#8b6fe8' },
690
+ bugfix: { bg: '#e8685a', border: '#d04838' },
691
+ command: { bg: '#f0a030', border: '#d48020' },
692
+ file_summary: { bg: '#38bdf8', border: '#1aa8e8' },
693
+ other: { bg: '#6b6350', border: '#504c3c' },
970
694
  };
971
695
 
972
696
  // ─── Utilities ────────────────────────────────────────────────────────────────
@@ -988,9 +712,7 @@
988
712
  return `${Math.floor(hr / 24)}d ago`;
989
713
  }
990
714
 
991
- function kindClass(k) {
992
- return 'kind-' + (k || 'other').replace(/[^a-z_]/g, '');
993
- }
715
+ function kindClass(k) { return 'kind-' + (k || 'other').replace(/[^a-z_]/g, ''); }
994
716
 
995
717
  function h(tag, attrs, ...children) {
996
718
  const el = document.createElement(tag);
@@ -1018,9 +740,7 @@
1018
740
  } catch (e) {
1019
741
  document.getElementById('memory-list').innerHTML =
1020
742
  '<div class="empty"><div class="empty-title">Could not load data</div>' +
1021
- '<div class="empty-sub">' +
1022
- e.message +
1023
- '</div></div>';
743
+ '<div class="empty-sub">' + e.message + '</div></div>';
1024
744
  }
1025
745
  }
1026
746
 
@@ -1034,13 +754,10 @@
1034
754
  document.getElementById('s-saved').textContent = fmtK(saved);
1035
755
  document.getElementById('s-spent').textContent = fmtK(spent);
1036
756
  document.getElementById('s-roi').textContent = roi;
1037
- // cache from full stats, placeholder for now
1038
757
  document.getElementById('s-cache').textContent = '—';
1039
-
1040
758
  buildKindFilters(memories);
1041
759
  renderList(memories);
1042
760
  buildGraph(graph);
1043
- // load full stats in background for cache pill
1044
761
  loadFullStats();
1045
762
  }
1046
763
 
@@ -1048,66 +765,41 @@
1048
765
  function buildStrategyBtns() {
1049
766
  const wrap = document.getElementById('strategy-btns');
1050
767
  ['bm25', 'hybrid', 'embeddings'].forEach((s) => {
1051
- const btn = h(
1052
- 'button',
1053
- {
1054
- class: 'strat-btn' + (s === S.strategy ? ' active' : ''),
1055
- onclick: () => {
1056
- S.strategy = s;
1057
- wrap.querySelectorAll('.strat-btn').forEach((b) => b.classList.remove('active'));
1058
- btn.classList.add('active');
1059
- if (S.searchQuery) runSearch(S.searchQuery);
1060
- },
768
+ const btn = h('button', {
769
+ class: 'strat-btn' + (s === S.strategy ? ' active' : ''),
770
+ onclick: () => {
771
+ S.strategy = s;
772
+ wrap.querySelectorAll('.strat-btn').forEach((b) => b.classList.remove('active'));
773
+ btn.classList.add('active');
774
+ if (S.searchQuery) runSearch(S.searchQuery);
1061
775
  },
1062
- s,
1063
- );
776
+ }, s);
1064
777
  wrap.append(btn);
1065
778
  });
1066
779
  }
1067
780
 
1068
781
  // ─── Kind filter chips ────────────────────────────────────────────────────────
1069
- const KIND_ORDER = [
1070
- 'all',
1071
- 'decision',
1072
- 'learning',
1073
- 'bugfix',
1074
- 'command',
1075
- 'file_summary',
1076
- 'other',
1077
- ];
782
+ const KIND_ORDER = ['all','decision','learning','bugfix','command','file_summary','other'];
1078
783
 
1079
784
  function buildKindFilters(memories) {
1080
785
  const wrap = document.getElementById('kind-filters');
1081
786
  wrap.innerHTML = '';
1082
787
  const counts = {};
1083
- memories.forEach((m) => {
1084
- counts[m.kind] = (counts[m.kind] || 0) + 1;
1085
- });
1086
-
1087
- const kinds = [
1088
- 'all',
1089
- ...Object.keys(counts).sort((a, b) => KIND_ORDER.indexOf(a) - KIND_ORDER.indexOf(b)),
1090
- ];
1091
-
788
+ memories.forEach((m) => { counts[m.kind] = (counts[m.kind] || 0) + 1; });
789
+ const kinds = ['all', ...Object.keys(counts).sort((a, b) => KIND_ORDER.indexOf(a) - KIND_ORDER.indexOf(b))];
1092
790
  kinds.forEach((k) => {
1093
791
  const isAll = k === 'all';
1094
792
  const label = isAll ? 'All' : k.replace('_', ' ');
1095
793
  const count = isAll ? memories.length : counts[k] || 0;
1096
- const btn = h(
1097
- 'div',
1098
- {
1099
- class: 'kind-tag' + (S.activeKind === k ? ' active kf-' + k : ''),
1100
- onclick: () => {
1101
- S.activeKind = k;
1102
- wrap.querySelectorAll('.kind-tag').forEach((b) => {
1103
- b.className = 'kind-tag';
1104
- });
1105
- btn.className = 'kind-tag active kf-' + k;
1106
- renderList(S.searchResults ?? S.data?.memories ?? []);
1107
- },
794
+ const btn = h('div', {
795
+ class: 'kind-tag' + (S.activeKind === k ? ' active kf-' + k : ''),
796
+ onclick: () => {
797
+ S.activeKind = k;
798
+ wrap.querySelectorAll('.kind-tag').forEach((b) => { b.className = 'kind-tag'; });
799
+ btn.className = 'kind-tag active kf-' + k;
800
+ renderList(S.searchResults ?? S.data?.memories ?? []);
1108
801
  },
1109
- label + (count > 0 ? ' ' + count : ''),
1110
- );
802
+ }, label + (count > 0 ? ' ' + count : ''));
1111
803
  wrap.append(btn);
1112
804
  });
1113
805
  }
@@ -1115,9 +807,7 @@
1115
807
  // ─── Memory list ──────────────────────────────────────────────────────────────
1116
808
  function renderList(memories) {
1117
809
  const list = document.getElementById('memory-list');
1118
- const filtered =
1119
- S.activeKind === 'all' ? memories : memories.filter((m) => m.kind === S.activeKind);
1120
-
810
+ const filtered = S.activeKind === 'all' ? memories : memories.filter((m) => m.kind === S.activeKind);
1121
811
  document.getElementById('list-count').textContent =
1122
812
  filtered.length !== memories.length
1123
813
  ? filtered.length + ' / ' + memories.length
@@ -1125,24 +815,16 @@
1125
815
 
1126
816
  if (filtered.length === 0) {
1127
817
  list.innerHTML = '';
1128
- list.append(
1129
- h(
1130
- 'div',
1131
- { class: 'empty' },
1132
- h('div', { class: 'empty-icon' }, svgIcon('search')),
1133
- h('div', { class: 'empty-title' }, S.searchQuery ? 'No results' : 'No memories yet'),
1134
- h(
1135
- 'div',
1136
- { class: 'empty-sub' },
1137
- S.searchQuery
1138
- ? 'Try a different query or strategy.'
1139
- : 'End a Claude session — the hook will extract observations automatically.',
1140
- ),
1141
- ),
1142
- );
818
+ list.append(h('div', { class: 'empty' },
819
+ h('div', { class: 'empty-icon' }, svgIcon('search')),
820
+ h('div', { class: 'empty-title' }, S.searchQuery ? 'No results' : 'No memories yet'),
821
+ h('div', { class: 'empty-sub' },
822
+ S.searchQuery
823
+ ? 'Try a different query or strategy.'
824
+ : 'End a Claude session — observations are extracted automatically.'),
825
+ ));
1143
826
  return;
1144
827
  }
1145
-
1146
828
  list.innerHTML = '';
1147
829
  filtered.forEach((m) => list.append(memCard(m)));
1148
830
  }
@@ -1153,62 +835,35 @@
1153
835
  onclick: () => selectMemory(m),
1154
836
  });
1155
837
 
1156
- // Left accent color
1157
838
  const kc = KIND_COLOR[m.kind] || KIND_COLOR.other;
1158
839
  card.style.setProperty('--card-accent', kc.bg);
1159
- if (m.id === S.selectedId) {
1160
- card.style.borderLeftColor = kc.bg;
1161
- }
1162
- card.style.cssText += `--before-bg: ${kc.bg};`;
1163
-
1164
- const forget = h(
1165
- 'div',
1166
- {
1167
- class: 'mc-forget',
1168
- title: 'Forget this memory',
1169
- onclick: async (e) => {
1170
- e.stopPropagation();
1171
- if (!confirm('Remove this memory?')) return;
1172
- await fetch('/api/memories/' + m.id, { method: 'DELETE' });
1173
- S.data.memories = S.data.memories.filter((x) => x.id !== m.id);
1174
- if (S.selectedId === m.id) {
1175
- S.selectedId = null;
1176
- hideDetail();
1177
- }
1178
- buildKindFilters(S.data.memories);
1179
- renderList(S.searchResults ?? S.data.memories);
1180
- if (S.network) {
1181
- try {
1182
- S.network.getBody().data.nodes.remove(m.id);
1183
- } catch {}
1184
- }
1185
- document.getElementById('s-mem').textContent = fmtK(S.data.memories.length);
1186
- },
840
+ if (m.id === S.selectedId) card.style.borderLeftColor = kc.bg;
841
+
842
+ const forget = h('div', {
843
+ class: 'mc-forget', title: 'Forget this memory',
844
+ onclick: async (e) => {
845
+ e.stopPropagation();
846
+ if (!confirm('Remove this memory?')) return;
847
+ await fetch('/api/memories/' + m.id, { method: 'DELETE' });
848
+ S.data.memories = S.data.memories.filter((x) => x.id !== m.id);
849
+ if (S.selectedId === m.id) { S.selectedId = null; hideDetail(); }
850
+ buildKindFilters(S.data.memories);
851
+ renderList(S.searchResults ?? S.data.memories);
852
+ if (S.network) { try { S.network.getBody().data.nodes.remove(m.id); } catch {} }
853
+ document.getElementById('s-mem').textContent = fmtK(S.data.memories.length);
1187
854
  },
1188
- '×',
1189
- );
855
+ }, '×');
1190
856
 
1191
- const head = h(
1192
- 'div',
1193
- { class: 'mc-head' },
857
+ const head = h('div', { class: 'mc-head' },
1194
858
  h('span', { class: 'mc-title' }, m.title),
1195
- h(
1196
- 'span',
1197
- { class: 'mc-kind ' + kindClass(m.kind) },
1198
- (m.kind || 'other').replace('_', ' '),
1199
- ),
859
+ h('span', { class: 'mc-kind ' + kindClass(m.kind) }, (m.kind || 'other').replace('_', ' ')),
1200
860
  );
1201
-
1202
861
  const preview = m.body ? h('div', { class: 'mc-body' }, m.body) : null;
1203
-
1204
862
  const tags = (m.tags || []).slice(0, 3).map((t) => h('span', { class: 'mc-tag' }, t));
1205
- const footer = h(
1206
- 'div',
1207
- { class: 'mc-footer' },
863
+ const footer = h('div', { class: 'mc-footer' },
1208
864
  h('span', { class: 'mc-time' }, relTime(m.created_at)),
1209
865
  h('div', { class: 'mc-tags' }, ...tags),
1210
866
  );
1211
-
1212
867
  card.append(forget, head);
1213
868
  if (preview) card.append(preview);
1214
869
  card.append(footer);
@@ -1218,18 +873,12 @@
1218
873
  // ─── Detail panel ─────────────────────────────────────────────────────────────
1219
874
  function selectMemory(m) {
1220
875
  S.selectedId = m.id;
1221
- document.querySelectorAll('.mem-card').forEach((c) => c.classList.remove('selected'));
1222
- const cards = document.querySelectorAll('.mem-card');
1223
- // find by checking onclick would match but instead we just re-render is expensive
1224
- // so find by matching title as a quick approach
1225
876
  renderList(S.searchResults ?? S.data?.memories ?? []);
1226
877
  showDetail(m);
1227
878
  if (S.network) {
1228
879
  try {
1229
880
  S.network.selectNodes([m.id]);
1230
- S.network.focus(m.id, {
1231
- animation: { duration: 400, easingFunction: 'easeInOutQuad' },
1232
- });
881
+ S.network.focus(m.id, { animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
1233
882
  } catch {}
1234
883
  }
1235
884
  }
@@ -1238,57 +887,29 @@
1238
887
  const panel = document.getElementById('detail-panel');
1239
888
  const inner = document.getElementById('detail-inner');
1240
889
  panel.style.display = 'block';
890
+ panel.style.animation = 'none';
891
+ requestAnimationFrame(() => { panel.style.animation = ''; });
1241
892
 
1242
- const kc = KIND_COLOR[m.kind] || KIND_COLOR.other;
1243
893
  const saved = m.tokens_saved ? fmtK(m.tokens_saved) + ' tk saved' : '';
1244
894
  const spent = m.tokens_spent ? fmtK(m.tokens_spent) + ' tk spent' : '';
1245
-
1246
895
  const fileChips = (m.files || []).map((f) => {
1247
896
  const short = f.split('/').slice(-2).join('/');
1248
897
  return h('span', { class: 'detail-chip file', title: f }, short);
1249
898
  });
1250
899
  const tagChips = (m.tags || []).map((t) => h('span', { class: 'detail-chip' }, t));
1251
-
1252
900
  inner.innerHTML = '';
1253
901
  inner.append(
1254
- h(
1255
- 'div',
1256
- { class: 'detail-head' },
902
+ h('div', { class: 'detail-head' },
1257
903
  h('span', { class: 'detail-title' }, m.title),
1258
- h(
1259
- 'span',
1260
- { class: 'mc-kind ' + kindClass(m.kind) },
1261
- (m.kind || 'other').replace('_', ' '),
1262
- ),
1263
- h(
1264
- 'div',
1265
- {
1266
- class: 'detail-close',
1267
- title: 'Close',
1268
- onclick: () => {
1269
- S.selectedId = null;
1270
- hideDetail();
1271
- renderList(S.searchResults ?? S.data?.memories ?? []);
1272
- },
1273
- },
1274
- h(
1275
- 'svg',
1276
- {
1277
- width: '10',
1278
- height: '10',
1279
- viewBox: '0 0 10 10',
1280
- fill: 'none',
1281
- stroke: 'currentColor',
1282
- 'stroke-width': '1.6',
1283
- 'stroke-linecap': 'round',
1284
- },
1285
- h('path', { d: 'M1 1l8 8M9 1L1 9' }),
1286
- ),
904
+ h('span', { class: 'mc-kind ' + kindClass(m.kind) }, (m.kind || 'other').replace('_', ' ')),
905
+ h('div', { class: 'detail-close', title: 'Close', onclick: () => {
906
+ S.selectedId = null; hideDetail(); renderList(S.searchResults ?? S.data?.memories ?? []);
907
+ }},
908
+ h('svg', { width: '10', height: '10', viewBox: '0 0 10 10', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.6', 'stroke-linecap': 'round' },
909
+ h('path', { d: 'M1 1l8 8M9 1L1 9' }))
1287
910
  ),
1288
911
  ),
1289
- h(
1290
- 'div',
1291
- { class: 'detail-meta' },
912
+ h('div', { class: 'detail-meta' },
1292
913
  h('span', {}, relTime(m.created_at)),
1293
914
  saved ? h('span', { style: 'color:var(--accent-g)' }, saved) : null,
1294
915
  spent ? h('span', {}, spent) : null,
@@ -1302,11 +923,7 @@
1302
923
 
1303
924
  function hideDetail() {
1304
925
  document.getElementById('detail-panel').style.display = 'none';
1305
- if (S.network) {
1306
- try {
1307
- S.network.unselectAll();
1308
- } catch {}
1309
- }
926
+ if (S.network) { try { S.network.unselectAll(); } catch {} }
1310
927
  }
1311
928
 
1312
929
  // ─── Search ────────────────────────────────────────────────────────────────────
@@ -1314,11 +931,7 @@
1314
931
  const q = e.target.value.trim();
1315
932
  S.searchQuery = q;
1316
933
  clearTimeout(S.debounce);
1317
- if (!q) {
1318
- S.searchResults = null;
1319
- renderList(S.data?.memories ?? []);
1320
- return;
1321
- }
934
+ if (!q) { S.searchResults = null; renderList(S.data?.memories ?? []); return; }
1322
935
  S.debounce = setTimeout(() => runSearch(q), 320);
1323
936
  });
1324
937
 
@@ -1326,13 +939,10 @@
1326
939
  const spin = document.getElementById('search-spinner');
1327
940
  spin.style.display = 'block';
1328
941
  try {
1329
- const res = await fetch(
1330
- `/api/search?q=${encodeURIComponent(q)}&strategy=${S.strategy}&k=20`,
1331
- );
942
+ const res = await fetch(`/api/search?q=${encodeURIComponent(q)}&strategy=${S.strategy}&k=20`);
1332
943
  const json = await res.json();
1333
944
  S.searchResults = json.results;
1334
- document.getElementById('tab-hint').textContent =
1335
- `${json.strategy} · ${json.results.length} results`;
945
+ document.getElementById('tab-hint').textContent = `${json.strategy} · ${json.results.length} results`;
1336
946
  renderList(json.results);
1337
947
  } catch {
1338
948
  S.searchResults = null;
@@ -1348,16 +958,11 @@
1348
958
  btn.addEventListener('click', () => {
1349
959
  const t = btn.dataset.tab;
1350
960
  S.activeTab = t;
1351
- document
1352
- .querySelectorAll('.tab-btn[data-tab]')
1353
- .forEach((b) => b.classList.remove('active'));
961
+ document.querySelectorAll('.tab-btn[data-tab]').forEach((b) => b.classList.remove('active'));
1354
962
  btn.classList.add('active');
1355
963
  document.getElementById('tab-graph').style.display = t === 'graph' ? 'flex' : 'none';
1356
- document.getElementById('tab-analytics').style.display =
1357
- t === 'analytics' ? 'flex' : 'none';
1358
- if (t === 'analytics' && !S.analyticsRendered && S.fullStats) {
1359
- renderAnalytics(S.fullStats);
1360
- }
964
+ document.getElementById('tab-analytics').style.display = t === 'analytics' ? 'flex' : 'none';
965
+ if (t === 'analytics' && !S.analyticsRendered && S.fullStats) renderAnalytics(S.fullStats);
1361
966
  });
1362
967
  });
1363
968
  }
@@ -1367,70 +972,41 @@
1367
972
  const container = document.getElementById('graph-container');
1368
973
  if (!nodes?.length) {
1369
974
  container.innerHTML =
1370
- '<div class="empty" style="height:100%">' +
1371
- svgIcon('graph') +
975
+ '<div class="empty" style="height:100%">' + svgIcon('graph').outerHTML +
1372
976
  '<div class="empty-title">No graph yet</div>' +
1373
- '<div class="empty-sub">Memories with shared files or tags will appear here.</div></div>';
977
+ '<div class="empty-sub">Memories with shared files or tags appear here.</div></div>';
1374
978
  return;
1375
979
  }
1376
-
1377
- const vNodes = new vis.DataSet(
1378
- nodes.map((n) => {
1379
- const kc = KIND_COLOR[n.kind] || KIND_COLOR.other;
1380
- return {
1381
- id: n.id,
1382
- label: n.label,
1383
- title: n.title,
1384
- color: {
1385
- background: kc.bg,
1386
- border: kc.border,
1387
- highlight: { background: kc.bg, border: '#fff' },
1388
- },
1389
- font: { color: '#dde1ea', size: 11, face: 'Bricolage Grotesque' },
1390
- shape: 'dot',
1391
- size: 10,
1392
- borderWidth: 1.5,
1393
- };
1394
- }),
1395
- );
1396
-
1397
- const vEdges = new vis.DataSet(
1398
- edges.map((e, i) => ({
1399
- id: i,
1400
- from: e.from,
1401
- to: e.to,
1402
- color: { color: '#1f2535', highlight: '#4f8dff', opacity: 0.8 },
1403
- width: 1,
1404
- smooth: { type: 'dynamic' },
1405
- })),
1406
- );
1407
-
980
+ const vNodes = new vis.DataSet(nodes.map((n) => {
981
+ const kc = KIND_COLOR[n.kind] || KIND_COLOR.other;
982
+ return {
983
+ id: n.id, label: n.label, title: n.title,
984
+ color: { background: kc.bg, border: kc.border, highlight: { background: kc.bg, border: '#fff' } },
985
+ font: { color: '#ede8da', size: 11, face: 'Syne' },
986
+ shape: 'dot', size: 10, borderWidth: 1.5,
987
+ };
988
+ }));
989
+ const vEdges = new vis.DataSet(edges.map((e, i) => ({
990
+ id: i, from: e.from, to: e.to,
991
+ color: { color: '#2d2918', highlight: '#f0a030', opacity: 0.9 },
992
+ width: 1, smooth: { type: 'dynamic' },
993
+ })));
1408
994
  S.network = new vis.Network(
1409
995
  container,
1410
996
  { nodes: vNodes, edges: vEdges },
1411
997
  {
1412
- physics: {
1413
- stabilization: { iterations: 100 },
1414
- barnesHut: { gravitationalConstant: -2000, springLength: 120 },
1415
- },
998
+ physics: { stabilization: { iterations: 100 }, barnesHut: { gravitationalConstant: -2000, springLength: 120 } },
1416
999
  interaction: { hover: true, tooltipDelay: 200 },
1417
1000
  nodes: { borderWidthSelected: 2 },
1418
1001
  },
1419
1002
  );
1420
-
1421
1003
  S.network.on('click', ({ nodes: selected }) => {
1422
1004
  if (!selected.length) return;
1423
1005
  const id = selected[0];
1424
1006
  const mem = S.data?.memories?.find((m) => m.id === id);
1425
- if (mem) {
1426
- S.selectedId = id;
1427
- renderList(S.searchResults ?? S.data?.memories ?? []);
1428
- showDetail(mem);
1429
- }
1007
+ if (mem) { S.selectedId = id; renderList(S.searchResults ?? S.data?.memories ?? []); showDetail(mem); }
1430
1008
  });
1431
-
1432
- document.getElementById('tab-hint').textContent =
1433
- `${nodes.length} nodes · ${edges.length} edges`;
1009
+ document.getElementById('tab-hint').textContent = `${nodes.length} nodes · ${edges.length} edges`;
1434
1010
  }
1435
1011
 
1436
1012
  // ─── Full stats ────────────────────────────────────────────────────────────────
@@ -1438,10 +1014,8 @@
1438
1014
  try {
1439
1015
  const res = await fetch('/api/stats/full');
1440
1016
  S.fullStats = await res.json();
1441
- // Update cache pill
1442
1017
  const hit = S.fullStats.cache?.hit_rate;
1443
- document.getElementById('s-cache').textContent =
1444
- hit != null ? (hit * 100).toFixed(0) + '%' : '—';
1018
+ document.getElementById('s-cache').textContent = hit != null ? (hit * 100).toFixed(0) + '%' : '—';
1445
1019
  if (S.activeTab === 'analytics') renderAnalytics(S.fullStats);
1446
1020
  } catch {}
1447
1021
  }
@@ -1455,57 +1029,32 @@
1455
1029
  const kb = fs.kindBreakdown || {};
1456
1030
  const kbMax = Math.max(...Object.values(kb).map(Number), 1);
1457
1031
  const kbCard = card('Memory kinds');
1458
- Object.entries(kb)
1459
- .sort((a, b) => b[1] - a[1])
1460
- .forEach(([kind, count]) => {
1461
- const kc = KIND_COLOR[kind] || KIND_COLOR.other;
1462
- const row = h(
1463
- 'div',
1464
- { class: 'bar-row' },
1465
- h(
1466
- 'span',
1467
- { class: 'bar-label', style: 'font-size:11.5px;font-weight:600' },
1468
- kind.replace('_', ' '),
1469
- ),
1470
- h(
1471
- 'div',
1472
- { class: 'bar-track' },
1473
- h('div', { class: 'bar-fill', style: `background:${kc.bg}` }),
1474
- ),
1475
- h('span', { class: 'bar-count' }, String(count)),
1476
- );
1477
- kbCard.append(row);
1478
- requestAnimationFrame(() => {
1479
- row.querySelector('.bar-fill').style.width = (count / kbMax) * 100 + '%';
1480
- });
1481
- });
1482
- if (!Object.keys(kb).length)
1483
- kbCard.append(
1484
- h('div', { class: 'empty' }, h('div', { class: 'empty-sub' }, 'No data yet')),
1032
+ Object.entries(kb).sort((a, b) => b[1] - a[1]).forEach(([kind, count]) => {
1033
+ const kc = KIND_COLOR[kind] || KIND_COLOR.other;
1034
+ const row = h('div', { class: 'bar-row' },
1035
+ h('span', { class: 'bar-label' }, kind.replace('_', ' ')),
1036
+ h('div', { class: 'bar-track' }, h('div', { class: 'bar-fill', style: `background:${kc.bg}` })),
1037
+ h('span', { class: 'bar-count' }, String(count)),
1485
1038
  );
1039
+ kbCard.append(row);
1040
+ requestAnimationFrame(() => { row.querySelector('.bar-fill').style.width = (count / kbMax) * 100 + '%'; });
1041
+ });
1042
+ if (!Object.keys(kb).length) kbCard.append(h('div', { class: 'empty' }, h('div', { class: 'empty-sub' }, 'No data yet')));
1486
1043
  grid.append(wrap(kbCard));
1487
1044
 
1488
1045
  // 2. Cache performance
1489
1046
  const cc = fs.cache || {};
1490
1047
  const cacheCard = card('Cache performance');
1491
1048
  [
1492
- [
1493
- 'Hit rate',
1494
- cc.hit_rate != null ? (cc.hit_rate * 100).toFixed(1) + '%' : '—',
1495
- 'var(--accent-g)',
1496
- ],
1497
- ['Hits', cc.hit_count ?? 0, 'var(--accent-g)'],
1498
- ['Misses', cc.miss_count ?? 0, 'var(--muted2)'],
1499
- ['Entries', cc.entry_count ?? 0, 'var(--accent)'],
1049
+ ['Hit rate', cc.hit_rate != null ? (cc.hit_rate * 100).toFixed(1) + '%' : '—', 'var(--accent-g)'],
1050
+ ['Hits', cc.hit_count ?? 0, 'var(--accent-g)'],
1051
+ ['Misses', cc.miss_count ?? 0, 'var(--muted2)'],
1052
+ ['Entries', cc.entry_count ?? 0, 'var(--accent-c)'],
1500
1053
  ].forEach(([lbl, val, color]) => {
1501
- cacheCard.append(
1502
- h(
1503
- 'div',
1504
- { class: 'cache-stat' },
1505
- h('span', { class: 'cache-stat-lbl' }, lbl),
1506
- h('span', { class: 'cache-stat-val', style: `color:${color}` }, String(val)),
1507
- ),
1508
- );
1054
+ cacheCard.append(h('div', { class: 'cache-stat' },
1055
+ h('span', { class: 'cache-stat-lbl' }, lbl),
1056
+ h('span', { class: 'cache-stat-val', style: `color:${color}` }, String(val)),
1057
+ ));
1509
1058
  });
1510
1059
  grid.append(wrap(cacheCard));
1511
1060
 
@@ -1514,53 +1063,27 @@
1514
1063
  const ruMax = Math.max(...ru.map((r) => r.call_count), 1);
1515
1064
  const ruCard = card('Retrieval strategy');
1516
1065
  ru.sort((a, b) => b.call_count - a.call_count).forEach((r) => {
1517
- const row = h(
1518
- 'div',
1519
- { class: 'bar-row' },
1066
+ const row = h('div', { class: 'bar-row' },
1520
1067
  h('span', { class: 'bar-label' }, r.strategy),
1521
- h(
1522
- 'div',
1523
- { class: 'bar-track' },
1524
- h('div', { class: 'bar-fill', style: 'background:var(--accent-v)' }),
1525
- ),
1068
+ h('div', { class: 'bar-track' }, h('div', { class: 'bar-fill', style: 'background:var(--accent-v)' })),
1526
1069
  h('span', { class: 'bar-count' }, String(r.call_count)),
1527
1070
  );
1528
1071
  ruCard.append(row);
1529
- requestAnimationFrame(() => {
1530
- row.querySelector('.bar-fill').style.width = (r.call_count / ruMax) * 100 + '%';
1531
- });
1072
+ requestAnimationFrame(() => { row.querySelector('.bar-fill').style.width = (r.call_count / ruMax) * 100 + '%'; });
1532
1073
  });
1533
- if (!ru.length)
1534
- ruCard.append(
1535
- h('div', { class: 'empty' }, h('div', { class: 'empty-sub' }, 'No retrieval data yet')),
1536
- );
1074
+ if (!ru.length) ruCard.append(h('div', { class: 'empty' }, h('div', { class: 'empty-sub' }, 'No retrieval data yet')));
1537
1075
  grid.append(wrap(ruCard));
1538
1076
 
1539
1077
  // 4. Top files
1540
1078
  const tf = fs.topFiles || [];
1541
1079
  const tfCard = card('Top files referenced');
1542
1080
  tf.slice(0, 12).forEach((f) => {
1543
- tfCard.append(
1544
- h(
1545
- 'div',
1546
- { class: 'file-row' },
1547
- h(
1548
- 'span',
1549
- { class: 'file-name', title: f.file_path },
1550
- f.file_path.split('/').slice(-2).join('/'),
1551
- ),
1552
- h('span', { class: 'file-count' }, String(f.reference_count) + 'x'),
1553
- ),
1554
- );
1081
+ tfCard.append(h('div', { class: 'file-row' },
1082
+ h('span', { class: 'file-name', title: f.file_path }, f.file_path.split('/').slice(-2).join('/')),
1083
+ h('span', { class: 'file-count' }, String(f.reference_count) + 'x'),
1084
+ ));
1555
1085
  });
1556
- if (!tf.length)
1557
- tfCard.append(
1558
- h(
1559
- 'div',
1560
- { class: 'empty' },
1561
- h('div', { class: 'empty-sub' }, 'No file references yet'),
1562
- ),
1563
- );
1086
+ if (!tf.length) tfCard.append(h('div', { class: 'empty' }, h('div', { class: 'empty-sub' }, 'No file references yet')));
1564
1087
  grid.append(wrap(tfCard));
1565
1088
  }
1566
1089
 
@@ -1569,27 +1092,18 @@
1569
1092
  el.append(h('div', { class: 'a-title' }, title));
1570
1093
  return el;
1571
1094
  }
1572
- function wrap(el) {
1573
- const w = h('div', {});
1574
- w.append(el);
1575
- return w;
1576
- }
1095
+ function wrap(el) { const w = h('div', {}); w.append(el); return w; }
1577
1096
 
1578
1097
  // ─── SVG icons ────────────────────────────────────────────────────────────────
1579
1098
  function svgIcon(name) {
1580
1099
  const el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1581
- el.setAttribute('width', '32');
1582
- el.setAttribute('height', '32');
1583
- el.setAttribute('viewBox', '0 0 24 24');
1584
- el.setAttribute('fill', 'none');
1585
- el.setAttribute('stroke', 'currentColor');
1586
- el.setAttribute('stroke-width', '1.5');
1587
- el.setAttribute('stroke-linecap', 'round');
1588
- el.classList.add('empty-icon');
1100
+ el.setAttribute('width', '32'); el.setAttribute('height', '32');
1101
+ el.setAttribute('viewBox', '0 0 24 24'); el.setAttribute('fill', 'none');
1102
+ el.setAttribute('stroke', 'currentColor'); el.setAttribute('stroke-width', '1.5');
1103
+ el.setAttribute('stroke-linecap', 'round'); el.classList.add('empty-icon');
1589
1104
  const paths = {
1590
1105
  search: '<circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5"/>',
1591
- graph:
1592
- '<circle cx="5" cy="12" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="19" cy="19" r="2"/><path d="M7 11.5L17 6M7 12.5L17 18"/>',
1106
+ graph: '<circle cx="5" cy="12" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="19" cy="19" r="2"/><path d="M7 11.5L17 6M7 12.5L17 18"/>',
1593
1107
  };
1594
1108
  el.innerHTML = paths[name] || '';
1595
1109
  return el;