granite-mem 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1160 -0
- package/dist/public/app.js +588 -0
- package/dist/public/canvas-renderer.js +524 -0
- package/dist/public/graph.js +380 -0
- package/dist/public/index.html +157 -0
- package/dist/public/markdown-renderer.js +189 -0
- package/dist/public/style.css +923 -0
- package/package.json +68 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granite — Force-Directed Graph Visualization
|
|
3
|
+
*
|
|
4
|
+
* Interactive canvas graph showing note connections.
|
|
5
|
+
* Physics: repulsion between all nodes, attraction along edges, gravity to center.
|
|
6
|
+
* Interactions: drag, zoom, pan, click-to-navigate, hover highlight.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const GraphEngine = (() => {
|
|
10
|
+
|
|
11
|
+
// ── Type colors ──
|
|
12
|
+
const TYPE_COLORS = {
|
|
13
|
+
fleeting: { fill: '#fbbf24', glow: 'rgba(251, 191, 36, 0.3)' },
|
|
14
|
+
permanent: { fill: '#a5b4fc', glow: 'rgba(165, 180, 252, 0.3)' },
|
|
15
|
+
reference: { fill: '#34d399', glow: 'rgba(52, 211, 153, 0.3)' },
|
|
16
|
+
person: { fill: '#f9a8d4', glow: 'rgba(249, 168, 212, 0.3)' },
|
|
17
|
+
meeting: { fill: '#93c5fd', glow: 'rgba(147, 197, 253, 0.3)' },
|
|
18
|
+
project: { fill: '#c4b5fd', glow: 'rgba(196, 181, 253, 0.3)' },
|
|
19
|
+
decision: { fill: '#fca5a5', glow: 'rgba(252, 165, 165, 0.3)' },
|
|
20
|
+
_default: { fill: '#6e6e7a', glow: 'rgba(110, 110, 122, 0.3)' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const BG_COLOR = '#08080a';
|
|
24
|
+
const EDGE_COLOR = 'rgba(70, 70, 85, 0.35)';
|
|
25
|
+
const EDGE_HIGHLIGHT = 'rgba(165, 180, 252, 0.6)';
|
|
26
|
+
const LABEL_COLOR = '#b0b0b8';
|
|
27
|
+
const LABEL_DIM = 'rgba(110, 110, 122, 0.3)';
|
|
28
|
+
const LABEL_FONT = '11px "Instrument Sans", sans-serif';
|
|
29
|
+
const NODE_ACTIVE_RING = '#6366f1';
|
|
30
|
+
|
|
31
|
+
// ── State ──
|
|
32
|
+
let canvas, ctx;
|
|
33
|
+
let nodes = [], edges = [];
|
|
34
|
+
let width, height;
|
|
35
|
+
let transform = { x: 0, y: 0, scale: 1 };
|
|
36
|
+
let animFrame = null;
|
|
37
|
+
let hoveredNode = null;
|
|
38
|
+
let dragNode = null;
|
|
39
|
+
let isPanning = false;
|
|
40
|
+
let panStart = { x: 0, y: 0 };
|
|
41
|
+
let activeSlug = null;
|
|
42
|
+
let onNavigate = null;
|
|
43
|
+
let isRunning = false;
|
|
44
|
+
let simulationAlpha = 1;
|
|
45
|
+
|
|
46
|
+
// ── Physics constants ──
|
|
47
|
+
const REPULSION = 800;
|
|
48
|
+
const ATTRACTION = 0.008;
|
|
49
|
+
const GRAVITY = 0.02;
|
|
50
|
+
const DAMPING = 0.88;
|
|
51
|
+
const MIN_ALPHA = 0.001;
|
|
52
|
+
const VELOCITY_LIMIT = 8;
|
|
53
|
+
|
|
54
|
+
function getTypeColor(type) {
|
|
55
|
+
return TYPE_COLORS[type] || TYPE_COLORS._default;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function init(canvasEl, graphData, options = {}) {
|
|
59
|
+
canvas = canvasEl;
|
|
60
|
+
ctx = canvas.getContext('2d');
|
|
61
|
+
onNavigate = options.onNavigate || null;
|
|
62
|
+
activeSlug = options.activeSlug || null;
|
|
63
|
+
|
|
64
|
+
// Build nodes
|
|
65
|
+
const connectionCount = {};
|
|
66
|
+
for (const e of graphData.edges) {
|
|
67
|
+
connectionCount[e.source] = (connectionCount[e.source] || 0) + 1;
|
|
68
|
+
connectionCount[e.target] = (connectionCount[e.target] || 0) + 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
nodes = graphData.nodes.map((n, i) => {
|
|
72
|
+
const connections = connectionCount[n.slug] || 0;
|
|
73
|
+
const angle = (i / graphData.nodes.length) * Math.PI * 2;
|
|
74
|
+
const spread = Math.min(300, graphData.nodes.length * 15);
|
|
75
|
+
return {
|
|
76
|
+
...n,
|
|
77
|
+
x: Math.cos(angle) * spread * (0.5 + Math.random() * 0.5),
|
|
78
|
+
y: Math.sin(angle) * spread * (0.5 + Math.random() * 0.5),
|
|
79
|
+
vx: 0, vy: 0,
|
|
80
|
+
radius: Math.max(4, Math.min(14, 4 + connections * 1.5)),
|
|
81
|
+
connections,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Build edge references
|
|
86
|
+
const nodeMap = {};
|
|
87
|
+
nodes.forEach(n => nodeMap[n.slug] = n);
|
|
88
|
+
edges = graphData.edges
|
|
89
|
+
.filter(e => nodeMap[e.source] && nodeMap[e.target])
|
|
90
|
+
.map(e => ({ source: nodeMap[e.source], target: nodeMap[e.target] }));
|
|
91
|
+
|
|
92
|
+
// Reset
|
|
93
|
+
transform = { x: 0, y: 0, scale: 1 };
|
|
94
|
+
simulationAlpha = 1;
|
|
95
|
+
|
|
96
|
+
resize();
|
|
97
|
+
setupEvents();
|
|
98
|
+
start();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resize() {
|
|
102
|
+
const dpr = window.devicePixelRatio || 1;
|
|
103
|
+
const container = canvas.parentElement;
|
|
104
|
+
width = container.clientWidth;
|
|
105
|
+
height = container.clientHeight;
|
|
106
|
+
canvas.width = width * dpr;
|
|
107
|
+
canvas.height = height * dpr;
|
|
108
|
+
canvas.style.width = width + 'px';
|
|
109
|
+
canvas.style.height = height + 'px';
|
|
110
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Physics simulation ──
|
|
114
|
+
function simulate() {
|
|
115
|
+
if (simulationAlpha < MIN_ALPHA) return;
|
|
116
|
+
|
|
117
|
+
// Repulsion (all pairs)
|
|
118
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
119
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
120
|
+
const a = nodes[i], b = nodes[j];
|
|
121
|
+
let dx = b.x - a.x;
|
|
122
|
+
let dy = b.y - a.y;
|
|
123
|
+
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
124
|
+
const force = REPULSION / (dist * dist) * simulationAlpha;
|
|
125
|
+
const fx = (dx / dist) * force;
|
|
126
|
+
const fy = (dy / dist) * force;
|
|
127
|
+
a.vx -= fx; a.vy -= fy;
|
|
128
|
+
b.vx += fx; b.vy += fy;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Attraction (edges)
|
|
133
|
+
for (const edge of edges) {
|
|
134
|
+
const dx = edge.target.x - edge.source.x;
|
|
135
|
+
const dy = edge.target.y - edge.source.y;
|
|
136
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
137
|
+
const force = dist * ATTRACTION * simulationAlpha;
|
|
138
|
+
const fx = (dx / dist) * force;
|
|
139
|
+
const fy = (dy / dist) * force;
|
|
140
|
+
edge.source.vx += fx; edge.source.vy += fy;
|
|
141
|
+
edge.target.vx -= fx; edge.target.vy -= fy;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Gravity toward center
|
|
145
|
+
for (const node of nodes) {
|
|
146
|
+
node.vx -= node.x * GRAVITY * simulationAlpha;
|
|
147
|
+
node.vy -= node.y * GRAVITY * simulationAlpha;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Integrate
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
if (node === dragNode) continue;
|
|
153
|
+
node.vx *= DAMPING;
|
|
154
|
+
node.vy *= DAMPING;
|
|
155
|
+
// Clamp velocity
|
|
156
|
+
const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy);
|
|
157
|
+
if (speed > VELOCITY_LIMIT) {
|
|
158
|
+
node.vx = (node.vx / speed) * VELOCITY_LIMIT;
|
|
159
|
+
node.vy = (node.vy / speed) * VELOCITY_LIMIT;
|
|
160
|
+
}
|
|
161
|
+
node.x += node.vx;
|
|
162
|
+
node.y += node.vy;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
simulationAlpha *= 0.995;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Render ──
|
|
169
|
+
function draw() {
|
|
170
|
+
ctx.clearRect(0, 0, width, height);
|
|
171
|
+
ctx.fillStyle = BG_COLOR;
|
|
172
|
+
ctx.fillRect(0, 0, width, height);
|
|
173
|
+
|
|
174
|
+
ctx.save();
|
|
175
|
+
ctx.translate(width / 2 + transform.x, height / 2 + transform.y);
|
|
176
|
+
ctx.scale(transform.scale, transform.scale);
|
|
177
|
+
|
|
178
|
+
const hoveredNeighbors = new Set();
|
|
179
|
+
if (hoveredNode) {
|
|
180
|
+
for (const e of edges) {
|
|
181
|
+
if (e.source === hoveredNode) hoveredNeighbors.add(e.target);
|
|
182
|
+
if (e.target === hoveredNode) hoveredNeighbors.add(e.source);
|
|
183
|
+
}
|
|
184
|
+
hoveredNeighbors.add(hoveredNode);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const dimming = hoveredNode !== null;
|
|
188
|
+
|
|
189
|
+
// Draw edges
|
|
190
|
+
for (const edge of edges) {
|
|
191
|
+
const isHighlighted = hoveredNode && (edge.source === hoveredNode || edge.target === hoveredNode);
|
|
192
|
+
ctx.strokeStyle = isHighlighted ? EDGE_HIGHLIGHT : (dimming ? 'rgba(50, 50, 60, 0.15)' : EDGE_COLOR);
|
|
193
|
+
ctx.lineWidth = isHighlighted ? 1.5 : 0.75;
|
|
194
|
+
ctx.beginPath();
|
|
195
|
+
ctx.moveTo(edge.source.x, edge.source.y);
|
|
196
|
+
ctx.lineTo(edge.target.x, edge.target.y);
|
|
197
|
+
ctx.stroke();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Draw nodes
|
|
201
|
+
for (const node of nodes) {
|
|
202
|
+
const colors = getTypeColor(node.type);
|
|
203
|
+
const isNeighbor = hoveredNeighbors.has(node);
|
|
204
|
+
const isDimmed = dimming && !isNeighbor;
|
|
205
|
+
const isActive = node.slug === activeSlug;
|
|
206
|
+
|
|
207
|
+
// Glow for hovered/active
|
|
208
|
+
if ((node === hoveredNode || isActive) && !isDimmed) {
|
|
209
|
+
ctx.fillStyle = colors.glow;
|
|
210
|
+
ctx.beginPath();
|
|
211
|
+
ctx.arc(node.x, node.y, node.radius + 6, 0, Math.PI * 2);
|
|
212
|
+
ctx.fill();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Active ring
|
|
216
|
+
if (isActive) {
|
|
217
|
+
ctx.strokeStyle = NODE_ACTIVE_RING;
|
|
218
|
+
ctx.lineWidth = 2;
|
|
219
|
+
ctx.beginPath();
|
|
220
|
+
ctx.arc(node.x, node.y, node.radius + 3, 0, Math.PI * 2);
|
|
221
|
+
ctx.stroke();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Node circle
|
|
225
|
+
ctx.globalAlpha = isDimmed ? 0.15 : 1;
|
|
226
|
+
ctx.fillStyle = colors.fill;
|
|
227
|
+
ctx.beginPath();
|
|
228
|
+
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
|
|
229
|
+
ctx.fill();
|
|
230
|
+
|
|
231
|
+
// Label
|
|
232
|
+
ctx.font = LABEL_FONT;
|
|
233
|
+
ctx.fillStyle = isDimmed ? LABEL_DIM : LABEL_COLOR;
|
|
234
|
+
ctx.textAlign = 'center';
|
|
235
|
+
ctx.textBaseline = 'top';
|
|
236
|
+
const label = node.title.length > 24 ? node.title.slice(0, 22) + '…' : node.title;
|
|
237
|
+
ctx.fillText(label, node.x, node.y + node.radius + 5);
|
|
238
|
+
ctx.globalAlpha = 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ctx.restore();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function tick() {
|
|
245
|
+
simulate();
|
|
246
|
+
draw();
|
|
247
|
+
if (isRunning) animFrame = requestAnimationFrame(tick);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function start() {
|
|
251
|
+
if (isRunning) return;
|
|
252
|
+
isRunning = true;
|
|
253
|
+
tick();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function stop() {
|
|
257
|
+
isRunning = false;
|
|
258
|
+
if (animFrame) cancelAnimationFrame(animFrame);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Coordinate transforms ──
|
|
262
|
+
function screenToWorld(sx, sy) {
|
|
263
|
+
return {
|
|
264
|
+
x: (sx - width / 2 - transform.x) / transform.scale,
|
|
265
|
+
y: (sy - height / 2 - transform.y) / transform.scale,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function findNodeAt(sx, sy) {
|
|
270
|
+
const { x, y } = screenToWorld(sx, sy);
|
|
271
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
272
|
+
const n = nodes[i];
|
|
273
|
+
const dx = n.x - x, dy = n.y - y;
|
|
274
|
+
if (dx * dx + dy * dy < (n.radius + 4) * (n.radius + 4)) return n;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Events ──
|
|
280
|
+
function setupEvents() {
|
|
281
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
282
|
+
const rect = canvas.getBoundingClientRect();
|
|
283
|
+
const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
284
|
+
const node = findNodeAt(sx, sy);
|
|
285
|
+
if (node) {
|
|
286
|
+
dragNode = node;
|
|
287
|
+
dragNode.vx = 0;
|
|
288
|
+
dragNode.vy = 0;
|
|
289
|
+
simulationAlpha = Math.max(simulationAlpha, 0.3);
|
|
290
|
+
} else {
|
|
291
|
+
isPanning = true;
|
|
292
|
+
panStart = { x: e.clientX - transform.x, y: e.clientY - transform.y };
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
297
|
+
const rect = canvas.getBoundingClientRect();
|
|
298
|
+
const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
299
|
+
|
|
300
|
+
if (dragNode) {
|
|
301
|
+
const { x, y } = screenToWorld(sx, sy);
|
|
302
|
+
dragNode.x = x;
|
|
303
|
+
dragNode.y = y;
|
|
304
|
+
simulationAlpha = Math.max(simulationAlpha, 0.1);
|
|
305
|
+
} else if (isPanning) {
|
|
306
|
+
transform.x = e.clientX - panStart.x;
|
|
307
|
+
transform.y = e.clientY - panStart.y;
|
|
308
|
+
} else {
|
|
309
|
+
const node = findNodeAt(sx, sy);
|
|
310
|
+
if (node !== hoveredNode) {
|
|
311
|
+
hoveredNode = node;
|
|
312
|
+
canvas.style.cursor = node ? 'pointer' : 'grab';
|
|
313
|
+
// Show tooltip
|
|
314
|
+
const tooltip = document.getElementById('graph-tooltip');
|
|
315
|
+
if (node) {
|
|
316
|
+
tooltip.textContent = node.title;
|
|
317
|
+
tooltip.style.left = (e.clientX + 12) + 'px';
|
|
318
|
+
tooltip.style.top = (e.clientY - 8) + 'px';
|
|
319
|
+
tooltip.style.opacity = '1';
|
|
320
|
+
} else {
|
|
321
|
+
tooltip.style.opacity = '0';
|
|
322
|
+
}
|
|
323
|
+
} else if (node) {
|
|
324
|
+
const tooltip = document.getElementById('graph-tooltip');
|
|
325
|
+
tooltip.style.left = (e.clientX + 12) + 'px';
|
|
326
|
+
tooltip.style.top = (e.clientY - 8) + 'px';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
canvas.addEventListener('mouseup', (e) => {
|
|
332
|
+
if (dragNode) {
|
|
333
|
+
// If barely moved, treat as click
|
|
334
|
+
const rect = canvas.getBoundingClientRect();
|
|
335
|
+
const node = findNodeAt(e.clientX - rect.left, e.clientY - rect.top);
|
|
336
|
+
if (node && node === dragNode && onNavigate) {
|
|
337
|
+
onNavigate(node.slug);
|
|
338
|
+
}
|
|
339
|
+
dragNode = null;
|
|
340
|
+
}
|
|
341
|
+
isPanning = false;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
canvas.addEventListener('mouseleave', () => {
|
|
345
|
+
hoveredNode = null;
|
|
346
|
+
isPanning = false;
|
|
347
|
+
dragNode = null;
|
|
348
|
+
document.getElementById('graph-tooltip').style.opacity = '0';
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
canvas.addEventListener('wheel', (e) => {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
const scaleFactor = e.deltaY > 0 ? 0.92 : 1.08;
|
|
354
|
+
const newScale = Math.max(0.1, Math.min(5, transform.scale * scaleFactor));
|
|
355
|
+
|
|
356
|
+
// Zoom toward cursor
|
|
357
|
+
const rect = canvas.getBoundingClientRect();
|
|
358
|
+
const mx = e.clientX - rect.left - width / 2;
|
|
359
|
+
const my = e.clientY - rect.top - height / 2;
|
|
360
|
+
transform.x = mx - (mx - transform.x) * (newScale / transform.scale);
|
|
361
|
+
transform.y = my - (my - transform.y) * (newScale / transform.scale);
|
|
362
|
+
transform.scale = newScale;
|
|
363
|
+
}, { passive: false });
|
|
364
|
+
|
|
365
|
+
// Resize observer
|
|
366
|
+
const ro = new ResizeObserver(() => { resize(); });
|
|
367
|
+
ro.observe(canvas.parentElement);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setActiveSlug(slug) {
|
|
371
|
+
activeSlug = slug;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function reheat() {
|
|
375
|
+
simulationAlpha = 0.5;
|
|
376
|
+
if (!isRunning) start();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return { init, start, stop, resize, setActiveSlug, reheat, draw };
|
|
380
|
+
})();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Granite</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/style.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app">
|
|
14
|
+
<!-- Sidebar rail + panel -->
|
|
15
|
+
<aside id="sidebar" class="expanded">
|
|
16
|
+
<div id="sidebar-rail">
|
|
17
|
+
<button id="brand-mark" title="Granite">
|
|
18
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
19
|
+
<rect x="1" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.9"/>
|
|
20
|
+
<rect x="11" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.5"/>
|
|
21
|
+
<rect x="1" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.5"/>
|
|
22
|
+
<rect x="11" y="11" width="6" height="6" rx="1.5" fill="currentColor" opacity="0.3"/>
|
|
23
|
+
</svg>
|
|
24
|
+
</button>
|
|
25
|
+
<button id="rail-search" class="rail-btn" title="Search (⌘K)">
|
|
26
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
27
|
+
<circle cx="7" cy="7" r="4.5"/>
|
|
28
|
+
<path d="M10.5 10.5L14 14"/>
|
|
29
|
+
</svg>
|
|
30
|
+
</button>
|
|
31
|
+
<button id="rail-graph" class="rail-btn" title="Graph (⌘G)">
|
|
32
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
33
|
+
<circle cx="4" cy="4" r="2"/>
|
|
34
|
+
<circle cx="12" cy="4" r="2"/>
|
|
35
|
+
<circle cx="8" cy="13" r="2"/>
|
|
36
|
+
<path d="M5.5 5.5L7 11M10.5 5.5L9 11M6 4h4"/>
|
|
37
|
+
</svg>
|
|
38
|
+
</button>
|
|
39
|
+
<button id="rail-new" class="rail-btn" title="New note (⌘N)">
|
|
40
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
41
|
+
<path d="M8 3v10M3 8h10"/>
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
<div style="flex:1"></div>
|
|
45
|
+
<button id="rail-toggle" class="rail-btn" title="Toggle sidebar">
|
|
46
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
47
|
+
<rect x="1" y="2" width="14" height="12" rx="2"/>
|
|
48
|
+
<path d="M5.5 2v12"/>
|
|
49
|
+
</svg>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="sidebar-panel">
|
|
53
|
+
<!-- Search -->
|
|
54
|
+
<div id="sidebar-search">
|
|
55
|
+
<div id="search-wrapper">
|
|
56
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.4">
|
|
57
|
+
<circle cx="7" cy="7" r="4.5"/>
|
|
58
|
+
<path d="M10.5 10.5L14 14"/>
|
|
59
|
+
</svg>
|
|
60
|
+
<input type="text" id="search-box" placeholder="Search…" autocomplete="off" spellcheck="false">
|
|
61
|
+
<kbd>⌘K</kbd>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="type-filters">
|
|
65
|
+
<button class="type-chip active" data-type="">All</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Note list (default view) -->
|
|
69
|
+
<div id="note-list"></div>
|
|
70
|
+
|
|
71
|
+
<!-- Creation panel (hidden by default) -->
|
|
72
|
+
<div id="create-panel" style="display:none">
|
|
73
|
+
<div id="create-panel-header">
|
|
74
|
+
<span class="create-panel-title">New note</span>
|
|
75
|
+
<button id="create-panel-close" class="create-panel-close" title="Cancel">
|
|
76
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
77
|
+
<path d="M4 4l8 8M12 4l-8 8"/>
|
|
78
|
+
</svg>
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div id="create-form">
|
|
82
|
+
<label class="create-label">Type</label>
|
|
83
|
+
<select id="create-type" class="create-select"></select>
|
|
84
|
+
<label class="create-label">Title</label>
|
|
85
|
+
<input type="text" id="create-title" class="create-input" placeholder="Note title…" autocomplete="off" spellcheck="false">
|
|
86
|
+
<label class="create-label">Content</label>
|
|
87
|
+
<textarea id="create-body" class="create-textarea" placeholder="Start writing… (markdown supported)" spellcheck="false"></textarea>
|
|
88
|
+
<button id="create-submit" class="create-submit">Create note</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</aside>
|
|
93
|
+
|
|
94
|
+
<!-- Main content -->
|
|
95
|
+
<main id="main">
|
|
96
|
+
<!-- Note view -->
|
|
97
|
+
<div id="content-wrapper">
|
|
98
|
+
<header id="note-header">
|
|
99
|
+
<div id="note-breadcrumb"></div>
|
|
100
|
+
<h1 id="note-title"></h1>
|
|
101
|
+
<div id="note-meta">
|
|
102
|
+
<span id="note-type-badge" class="meta-badge"></span>
|
|
103
|
+
<span id="note-date" class="meta-date"></span>
|
|
104
|
+
<div id="note-tags"></div>
|
|
105
|
+
</div>
|
|
106
|
+
</header>
|
|
107
|
+
|
|
108
|
+
<!-- HTML rendered note body (replaces canvas) -->
|
|
109
|
+
<article id="note-body"></article>
|
|
110
|
+
|
|
111
|
+
<!-- Backlinks -->
|
|
112
|
+
<section id="backlinks-panel">
|
|
113
|
+
<div id="backlinks-header">
|
|
114
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
|
|
115
|
+
<path d="M10 2L6 14M4 5L1 8l3 3M12 5l3 3-3 3"/>
|
|
116
|
+
</svg>
|
|
117
|
+
<span id="backlinks-count">0</span> references
|
|
118
|
+
</div>
|
|
119
|
+
<div id="backlinks-list"></div>
|
|
120
|
+
</section>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Graph view -->
|
|
124
|
+
<div id="graph-view" style="display:none">
|
|
125
|
+
<canvas id="graph-canvas"></canvas>
|
|
126
|
+
<div id="graph-tooltip"></div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Empty state -->
|
|
130
|
+
<div id="empty-state">
|
|
131
|
+
<div id="empty-icon">
|
|
132
|
+
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="0.75" opacity="0.15">
|
|
133
|
+
<circle cx="12" cy="12" r="4"/>
|
|
134
|
+
<circle cx="28" cy="12" r="4"/>
|
|
135
|
+
<circle cx="20" cy="30" r="4"/>
|
|
136
|
+
<path d="M15.5 14L18 26M24.5 14L22 26M16 12h8" stroke-dasharray="2 3"/>
|
|
137
|
+
</svg>
|
|
138
|
+
</div>
|
|
139
|
+
<p>Select a note to begin</p>
|
|
140
|
+
<p class="empty-hint"><kbd>⌘K</kbd> search · <kbd>⌘N</kbd> new note · <kbd>⌘G</kbd> graph</p>
|
|
141
|
+
</div>
|
|
142
|
+
</main>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Wikilink preview popup -->
|
|
146
|
+
<div id="wikilink-preview" class="wikilink-preview">
|
|
147
|
+
<div class="preview-type"></div>
|
|
148
|
+
<div class="preview-title"></div>
|
|
149
|
+
<div class="preview-snippet"></div>
|
|
150
|
+
<div class="preview-tags"></div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<script src="/markdown-renderer.js"></script>
|
|
154
|
+
<script src="/graph.js"></script>
|
|
155
|
+
<script src="/app.js"></script>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|