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