@andespindola/brainlink 0.1.0-beta.2 → 0.1.0-beta.20

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.
Files changed (55) hide show
  1. package/AGENTS.md +5 -5
  2. package/CHANGELOG.md +50 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.md +157 -20
  5. package/SECURITY.md +1 -1
  6. package/dist/application/add-note.js +62 -13
  7. package/dist/application/analyze-vault.js +95 -8
  8. package/dist/application/frontend/client-css.js +190 -99
  9. package/dist/application/frontend/client-html.js +57 -45
  10. package/dist/application/frontend/client-js.js +416 -85
  11. package/dist/application/get-graph-layout.js +22 -7
  12. package/dist/application/get-graph-node.js +12 -0
  13. package/dist/application/get-graph-summary.js +12 -0
  14. package/dist/application/get-graph.js +3 -3
  15. package/dist/application/index-vault.js +11 -4
  16. package/dist/application/list-agents.js +3 -3
  17. package/dist/application/list-links.js +5 -5
  18. package/dist/application/migrate-vault.js +91 -0
  19. package/dist/application/search-graph-node-ids.js +12 -0
  20. package/dist/application/search-knowledge.js +75 -5
  21. package/dist/application/server/routes.js +27 -1
  22. package/dist/benchmarks/large-vault.js +1 -1
  23. package/dist/cli/commands/agent-commands.js +412 -0
  24. package/dist/cli/commands/config-commands.js +167 -0
  25. package/dist/cli/commands/read-commands.js +25 -8
  26. package/dist/cli/commands/write-commands.js +173 -4
  27. package/dist/cli/main.js +4 -0
  28. package/dist/cli/runtime.js +5 -2
  29. package/dist/domain/context.js +53 -11
  30. package/dist/domain/embeddings.js +2 -1
  31. package/dist/domain/graph-layout.js +20 -14
  32. package/dist/domain/markdown.js +36 -4
  33. package/dist/domain/middle-out.js +18 -0
  34. package/dist/infrastructure/config.js +94 -8
  35. package/dist/infrastructure/file-index.js +294 -0
  36. package/dist/infrastructure/file-system-vault.js +15 -0
  37. package/dist/infrastructure/paths.js +9 -1
  38. package/dist/infrastructure/private-pack-codec.js +73 -0
  39. package/dist/infrastructure/search-packs.js +348 -0
  40. package/dist/infrastructure/session-state.js +172 -0
  41. package/dist/mcp/main.js +11 -3
  42. package/dist/mcp/server.js +17 -2
  43. package/dist/mcp/startup.js +35 -0
  44. package/dist/mcp/tools.js +571 -19
  45. package/docs/AGENT_USAGE.md +99 -15
  46. package/docs/ARCHITECTURE.md +37 -26
  47. package/docs/QUICKSTART.md +104 -0
  48. package/docs/RELEASE.md +3 -3
  49. package/package.json +1 -3
  50. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  51. package/dist/infrastructure/sqlite/graph-reader.js +0 -120
  52. package/dist/infrastructure/sqlite/schema.js +0 -111
  53. package/dist/infrastructure/sqlite/search-reader.js +0 -156
  54. package/dist/infrastructure/sqlite/types.js +0 -1
  55. package/dist/infrastructure/sqlite-index.js +0 -25
@@ -1,10 +1,89 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { performance } from 'node:perf_hooks';
1
3
  import { validateGraph, getBrokenLinks, getOrphanNodes, getVaultStats } from '../domain/graph-analysis.js';
2
- import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
3
- import { getGraph } from './get-graph.js';
4
- export const getStats = async (vaultPath, agentId) => getVaultStats(await getGraph(vaultPath, agentId));
5
- export const getBrokenLinksReport = async (vaultPath, agentId) => getBrokenLinks(await getGraph(vaultPath, agentId));
6
- export const getOrphansReport = async (vaultPath, agentId) => getOrphanNodes(await getGraph(vaultPath, agentId));
7
- export const validateVault = async (vaultPath, agentId) => validateGraph(await getGraph(vaultPath, agentId));
4
+ import { ensureVault, listVaultFiles, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
5
+ import { resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
6
+ import { getGraphSummary } from './get-graph-summary.js';
7
+ import { buildContextPackage } from './build-context.js';
8
+ import { indexVault } from './index-vault.js';
9
+ import { searchKnowledge } from './search-knowledge.js';
10
+ import { loadBrainlinkConfig } from '../infrastructure/config.js';
11
+ export const getStats = async (vaultPath, agentId) => getVaultStats(await getGraphSummary(vaultPath, agentId));
12
+ export const getBrokenLinksReport = async (vaultPath, agentId) => getBrokenLinks(await getGraphSummary(vaultPath, agentId));
13
+ export const getOrphansReport = async (vaultPath, agentId) => getOrphanNodes(await getGraphSummary(vaultPath, agentId));
14
+ export const validateVault = async (vaultPath, agentId) => validateGraph(await getGraphSummary(vaultPath, agentId));
15
+ const toRatio = (part, total) => total === 0 ? 0 : Number((part / total).toFixed(4));
16
+ export const getExtendedStats = async (vaultPath, agentId) => {
17
+ const absoluteVaultPath = await ensureVault(vaultPath);
18
+ const graph = await getGraphSummary(absoluteVaultPath, agentId);
19
+ const stats = getVaultStats(graph);
20
+ const markdownFiles = await readMarkdownFiles(absoluteVaultPath);
21
+ const allFiles = await listVaultFiles(absoluteVaultPath);
22
+ const totalBytes = (await Promise.all(allFiles.map(async (filePath) => {
23
+ try {
24
+ return (await stat(filePath)).size;
25
+ }
26
+ catch {
27
+ return 0;
28
+ }
29
+ }))).reduce((sum, value) => sum + value, 0);
30
+ const updatedAt = markdownFiles
31
+ .map((file) => file.updatedAt.getTime())
32
+ .filter((time) => Number.isFinite(time))
33
+ .sort((left, right) => left - right);
34
+ const priorities = graph.edges.reduce((state, edge) => ({
35
+ ...state,
36
+ [edge.priority]: state[edge.priority] + 1
37
+ }), {
38
+ low: 0,
39
+ normal: 0,
40
+ high: 0,
41
+ critical: 0
42
+ });
43
+ const config = await loadBrainlinkConfig();
44
+ const defaults = resolveAgentRuntimeDefaults(config, agentId);
45
+ const probeQuery = graph.nodes[0]?.title ?? 'architecture';
46
+ const indexStart = performance.now();
47
+ await indexVault(absoluteVaultPath);
48
+ const indexLatency = performance.now() - indexStart;
49
+ const searchStart = performance.now();
50
+ await searchKnowledge(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), agentId, 'hybrid');
51
+ const searchLatency = performance.now() - searchStart;
52
+ const contextStart = performance.now();
53
+ await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid');
54
+ const contextLatency = performance.now() - contextStart;
55
+ return {
56
+ stats,
57
+ storage: {
58
+ markdownFileCount: markdownFiles.length,
59
+ totalFileCount: allFiles.length,
60
+ totalBytes,
61
+ averageMarkdownBytes: markdownFiles.length === 0
62
+ ? 0
63
+ : Math.round(markdownFiles.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) / markdownFiles.length),
64
+ ...(updatedAt.length > 0
65
+ ? {
66
+ oldestNoteUpdatedAt: new Date(updatedAt[0]).toISOString(),
67
+ newestNoteUpdatedAt: new Date(updatedAt[updatedAt.length - 1]).toISOString()
68
+ }
69
+ : {})
70
+ },
71
+ quality: {
72
+ resolvedLinkRatio: toRatio(stats.resolvedLinkCount, stats.linkCount),
73
+ brokenLinkRatio: toRatio(stats.brokenLinkCount, stats.linkCount),
74
+ orphanRatio: toRatio(stats.orphanCount, Math.max(stats.documentCount, 1)),
75
+ priorityDistribution: priorities
76
+ },
77
+ observability: {
78
+ probeQuery,
79
+ latenciesMs: {
80
+ index: Number(indexLatency.toFixed(2)),
81
+ search: Number(searchLatency.toFixed(2)),
82
+ context: Number(contextLatency.toFixed(2))
83
+ }
84
+ }
85
+ };
86
+ };
8
87
  const createCheck = (name, ok, message) => ({
9
88
  name,
10
89
  ok,
@@ -13,7 +92,7 @@ const createCheck = (name, ok, message) => ({
13
92
  export const doctorVault = async (vaultPath) => {
14
93
  const absoluteVaultPath = await ensureVault(vaultPath);
15
94
  const files = await readMarkdownFiles(absoluteVaultPath);
16
- const graph = await getGraph(absoluteVaultPath);
95
+ const graph = await getGraphSummary(absoluteVaultPath);
17
96
  const validation = validateGraph(graph);
18
97
  const checks = [
19
98
  createCheck('vault', true, `Vault ready at ${absoluteVaultPath}`),
@@ -21,8 +100,16 @@ export const doctorVault = async (vaultPath) => {
21
100
  createCheck('index', graph.nodes.length > 0, `${graph.nodes.length} indexed documents found`),
22
101
  createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`)
23
102
  ];
103
+ const recommendations = files.length === 0 && graph.nodes.length === 0
104
+ ? [
105
+ `Vault is empty. Add your first note: blink add "Architecture" --vault "${absoluteVaultPath}" --content "Markdown source of truth. #architecture"`,
106
+ `If this path is not the expected vault, inspect active config: blink config where`,
107
+ `If you changed vault recently, migrate existing memory: blink migrate-vault --from ~/.brainlink/vault --to "${absoluteVaultPath}"`
108
+ ]
109
+ : [];
24
110
  return {
25
111
  ok: checks.every((check) => check.ok),
26
- checks
112
+ checks,
113
+ ...(recommendations.length > 0 ? { recommendations } : {})
27
114
  };
28
115
  };
@@ -32,19 +32,43 @@ select {
32
32
  }
33
33
 
34
34
  .shell {
35
- display: grid;
36
- grid-template-columns: minmax(0, 1fr) 360px;
37
35
  width: 100%;
38
36
  height: 100svh;
39
37
  overflow: hidden;
40
38
  }
41
39
 
42
40
  .workspace {
41
+ display: grid;
42
+ grid-template-rows: auto minmax(0, 1fr);
43
43
  position: relative;
44
+ width: 100%;
45
+ height: 100%;
44
46
  min-width: 0;
45
47
  min-height: 0;
46
48
  }
47
49
 
50
+ .graph-header {
51
+ z-index: 5;
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 12px;
55
+ min-height: 72px;
56
+ padding: 10px 16px;
57
+ border-bottom: 1px solid var(--line);
58
+ background: linear-gradient(180deg, rgba(17, 21, 27, 0.96) 0%, rgba(17, 21, 27, 0.86) 100%);
59
+ backdrop-filter: blur(8px);
60
+ }
61
+
62
+ .brand-block {
63
+ display: grid;
64
+ gap: 2px;
65
+ min-width: max-content;
66
+ }
67
+
68
+ .brand-block strong {
69
+ font-size: 18px;
70
+ }
71
+
48
72
  #graph {
49
73
  display: block;
50
74
  width: 100%;
@@ -59,43 +83,25 @@ select {
59
83
  cursor: grabbing;
60
84
  }
61
85
 
62
- .topbar {
63
- position: absolute;
64
- top: 18px;
65
- left: 18px;
66
- right: 18px;
67
- display: flex;
68
- align-items: center;
69
- justify-content: space-between;
70
- gap: 18px;
71
- pointer-events: none;
72
- }
73
-
74
- .topbar > div {
75
- display: flex;
76
- align-items: baseline;
77
- gap: 12px;
78
- }
79
-
80
- .topbar strong {
81
- font-size: 18px;
82
- }
83
-
84
- .topbar span,
85
- .eyebrow,
86
- .inspector small {
86
+ .eyebrow {
87
87
  color: var(--muted);
88
88
  font-size: 12px;
89
89
  }
90
90
 
91
91
  .search {
92
- width: min(420px, 42vw);
93
- pointer-events: auto;
92
+ flex: 1 1 320px;
93
+ min-width: 220px;
94
94
  }
95
95
 
96
96
  .agent-filter {
97
97
  width: min(220px, 28vw);
98
- pointer-events: auto;
98
+ }
99
+
100
+ .header-actions {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 10px;
104
+ margin-left: auto;
99
105
  }
100
106
 
101
107
  .search input,
@@ -116,9 +122,6 @@ select {
116
122
  }
117
123
 
118
124
  .toolbar {
119
- position: absolute;
120
- left: 18px;
121
- bottom: 18px;
122
125
  display: flex;
123
126
  gap: 8px;
124
127
  }
@@ -138,70 +141,34 @@ select {
138
141
  color: var(--accent);
139
142
  }
140
143
 
141
- .inspector {
142
- display: grid;
143
- grid-template-rows: auto auto auto auto auto 1fr 1fr;
144
- gap: 22px;
145
- min-width: 0;
146
- height: 100%;
147
- padding: 24px;
148
- border-left: 1px solid var(--line);
149
- background: var(--panel);
150
- overflow: auto;
144
+ .floating-metrics {
145
+ display: flex;
146
+ gap: 10px;
147
+ flex-wrap: wrap;
151
148
  }
152
149
 
153
- .inspector h1,
154
- .inspector h2,
155
- .inspector p {
156
- margin: 0;
150
+ .metric-chip {
151
+ min-width: 94px;
152
+ padding: 10px 12px;
153
+ border: 1px solid var(--line);
154
+ border-radius: 10px;
155
+ background: rgba(21, 25, 31, 0.88);
156
+ display: grid;
157
+ gap: 3px;
157
158
  }
158
159
 
159
- .inspector h1 {
160
- margin-top: 6px;
160
+ .metric-chip strong {
161
161
  font-size: 26px;
162
- line-height: 1.12;
163
- overflow-wrap: anywhere;
162
+ line-height: 1;
164
163
  }
165
164
 
166
- .inspector h2 {
167
- margin-bottom: 10px;
165
+ .metric-chip small {
168
166
  color: var(--muted);
169
- font-size: 12px;
170
- font-weight: 700;
167
+ font-size: 11px;
168
+ letter-spacing: 0.03em;
171
169
  text-transform: uppercase;
172
170
  }
173
171
 
174
- #path {
175
- margin-top: 10px;
176
- color: var(--muted);
177
- line-height: 1.45;
178
- overflow-wrap: anywhere;
179
- }
180
-
181
- .metrics {
182
- display: grid;
183
- grid-template-columns: repeat(3, 1fr);
184
- border: 1px solid var(--line);
185
- border-radius: 8px;
186
- overflow: hidden;
187
- }
188
-
189
- .metrics div {
190
- display: grid;
191
- gap: 4px;
192
- padding: 14px;
193
- background: var(--panel-strong);
194
- }
195
-
196
- .metrics div + div {
197
- border-left: 1px solid var(--line);
198
- }
199
-
200
- .metrics span {
201
- font-size: 22px;
202
- font-weight: 700;
203
- }
204
-
205
172
  .tags {
206
173
  display: flex;
207
174
  flex-wrap: wrap;
@@ -215,6 +182,7 @@ select {
215
182
  background: var(--accent-weak);
216
183
  color: var(--accent);
217
184
  font-size: 12px;
185
+ word-break: break-word;
218
186
  overflow-wrap: anywhere;
219
187
  }
220
188
 
@@ -230,6 +198,7 @@ li {
230
198
  padding: 10px 0;
231
199
  border-bottom: 1px solid var(--line);
232
200
  color: var(--text);
201
+ word-break: break-word;
233
202
  overflow-wrap: anywhere;
234
203
  }
235
204
 
@@ -253,7 +222,7 @@ li small {
253
222
  }
254
223
 
255
224
  .note-content {
256
- max-height: 32svh;
225
+ max-height: min(68svh, 760px);
257
226
  margin: 0;
258
227
  padding: 12px;
259
228
  border: 1px solid var(--line);
@@ -267,28 +236,150 @@ li small {
267
236
  line-height: 1.5;
268
237
  }
269
238
 
270
- @media (max-width: 860px) {
271
- .shell {
272
- grid-template-columns: 1fr;
273
- grid-template-rows: minmax(0, 1fr) 42svh;
274
- }
239
+ .content-dialog {
240
+ width: min(920px, calc(100vw - 32px));
241
+ max-height: calc(100svh - 32px);
242
+ padding: 0;
243
+ border: 1px solid var(--line);
244
+ border-radius: 8px;
245
+ background: var(--panel);
246
+ color: var(--text);
247
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
248
+ }
275
249
 
276
- .inspector {
277
- border-left: 0;
278
- border-top: 1px solid var(--line);
279
- padding: 18px;
280
- }
250
+ .content-dialog::backdrop {
251
+ background: rgba(4, 7, 10, 0.72);
252
+ backdrop-filter: blur(4px);
253
+ }
254
+
255
+ .content-dialog article {
256
+ display: grid;
257
+ grid-template-rows: auto auto minmax(0, 1fr);
258
+ max-height: calc(100svh - 34px);
259
+ }
260
+
261
+ .content-dialog header {
262
+ display: flex;
263
+ align-items: flex-start;
264
+ justify-content: space-between;
265
+ gap: 18px;
266
+ padding: 22px;
267
+ border-bottom: 1px solid var(--line);
268
+ }
269
+
270
+ .content-dialog h2,
271
+ .content-dialog p {
272
+ margin: 0;
273
+ }
274
+
275
+ .content-dialog h2 {
276
+ margin-top: 6px;
277
+ font-size: 24px;
278
+ line-height: 1.15;
279
+ overflow-wrap: anywhere;
280
+ }
281
281
 
282
- .topbar {
282
+ .content-dialog p {
283
+ margin-top: 8px;
284
+ color: var(--muted);
285
+ overflow-wrap: anywhere;
286
+ }
287
+
288
+ .content-dialog button {
289
+ flex: 0 0 auto;
290
+ height: 38px;
291
+ padding: 0 14px;
292
+ border: 1px solid var(--line);
293
+ border-radius: 8px;
294
+ background: var(--panel-strong);
295
+ color: var(--text);
296
+ cursor: pointer;
297
+ }
298
+
299
+ .content-dialog button:hover,
300
+ .content-dialog button:focus {
301
+ border-color: var(--accent);
302
+ color: var(--accent);
303
+ }
304
+
305
+ .content-meta {
306
+ display: grid;
307
+ grid-template-columns: repeat(3, minmax(0, 1fr));
308
+ gap: 10px;
309
+ padding: 14px 22px;
310
+ border-bottom: 1px solid var(--line);
311
+ }
312
+
313
+ .content-meta-section {
314
+ min-height: 0;
315
+ padding: 10px;
316
+ border: 1px solid var(--line);
317
+ border-radius: 8px;
318
+ background: var(--panel-strong);
319
+ display: grid;
320
+ grid-template-rows: auto minmax(0, 1fr);
321
+ gap: 8px;
322
+ }
323
+
324
+ .content-meta-section h3 {
325
+ margin: 0;
326
+ color: var(--muted);
327
+ font-size: 11px;
328
+ font-weight: 700;
329
+ text-transform: uppercase;
330
+ }
331
+
332
+ .content-meta-section ul,
333
+ .content-meta-section .tags {
334
+ max-height: 140px;
335
+ overflow: auto;
336
+ align-content: flex-start;
337
+ padding-right: 4px;
338
+ }
339
+
340
+ .content-dialog .note-content {
341
+ max-height: none;
342
+ min-height: 0;
343
+ border: 0;
344
+ border-radius: 0;
345
+ padding: 22px;
346
+ }
347
+
348
+ @media (max-width: 860px) {
349
+ .graph-header {
283
350
  align-items: stretch;
284
- flex-direction: column;
351
+ flex-wrap: wrap;
352
+ padding: 10px 12px;
353
+ min-height: 0;
285
354
  }
286
355
 
287
356
  .search {
288
357
  width: 100%;
358
+ flex-basis: 100%;
359
+ order: 3;
289
360
  }
290
361
 
291
362
  .agent-filter {
292
363
  width: 100%;
293
364
  }
365
+
366
+ .header-actions {
367
+ width: 100%;
368
+ margin-left: 0;
369
+ justify-content: space-between;
370
+ order: 4;
371
+ }
372
+
373
+ .content-dialog header {
374
+ align-items: stretch;
375
+ flex-direction: column;
376
+ }
377
+
378
+ .metric-chip {
379
+ min-width: 82px;
380
+ }
381
+
382
+ .content-meta {
383
+ grid-template-columns: 1fr;
384
+ }
294
385
  }`;
@@ -9,58 +9,70 @@ export const createClientHtml = () => `<!doctype html>
9
9
  <body>
10
10
  <main class="shell">
11
11
  <section class="workspace" aria-label="Knowledge graph">
12
- <canvas id="graph" aria-label="Brainlink knowledge graph"></canvas>
13
- <div class="topbar">
14
- <div>
12
+ <header class="graph-header" aria-label="Graph actions">
13
+ <div class="brand-block">
15
14
  <strong>Brainlink</strong>
16
- <span id="stats">Loading graph</span>
15
+ <span class="eyebrow">Knowledge Graph</span>
16
+ </div>
17
+ <div class="floating-metrics" aria-label="Graph totals">
18
+ <div class="metric-chip">
19
+ <strong id="nodeCount">0</strong>
20
+ <small>Notes</small>
21
+ </div>
22
+ <div class="metric-chip">
23
+ <strong id="edgeCount">0</strong>
24
+ <small>Links</small>
25
+ </div>
26
+ <div class="metric-chip">
27
+ <strong id="tagCount">0</strong>
28
+ <small>Tags</small>
29
+ </div>
17
30
  </div>
18
31
  <label class="search">
19
32
  <input id="search" type="search" placeholder="Filter notes, tags or paths" autocomplete="off" />
20
33
  </label>
21
- <label class="agent-filter">
22
- <select id="agent"></select>
23
- </label>
24
- </div>
25
- <div class="toolbar" aria-label="Graph controls">
26
- <button id="zoomIn" type="button" title="Zoom in">+</button>
27
- <button id="zoomOut" type="button" title="Zoom out">-</button>
28
- <button id="reset" type="button" title="Reset view">⌂</button>
29
- </div>
34
+ <div class="header-actions">
35
+ <label class="agent-filter">
36
+ <select id="agent"></select>
37
+ </label>
38
+ <div class="toolbar" aria-label="Graph controls">
39
+ <button id="zoomIn" type="button" title="Zoom in">+</button>
40
+ <button id="zoomOut" type="button" title="Zoom out">-</button>
41
+ <button id="fit" type="button" title="Fit visible nodes">◎</button>
42
+ <button id="reset" type="button" title="Reset view">⌂</button>
43
+ </div>
44
+ </div>
45
+ </header>
46
+ <canvas id="graph" aria-label="Brainlink knowledge graph"></canvas>
30
47
  </section>
31
- <aside class="inspector" aria-label="Selected note">
32
- <div>
33
- <span class="eyebrow">Selected note</span>
34
- <h1 id="title">Graph Overview</h1>
35
- <p id="path">Select a node to inspect links and backlinks.</p>
36
- </div>
37
- <div class="metrics">
38
- <div><span id="nodeCount">0</span><small>Notes</small></div>
39
- <div><span id="edgeCount">0</span><small>Links</small></div>
40
- <div><span id="tagCount">0</span><small>Tags</small></div>
41
- </div>
42
- <section>
43
- <h2>Tags</h2>
44
- <div id="tags" class="tags"></div>
45
- </section>
46
- <section>
47
- <h2>Notes</h2>
48
- <ul id="notes"></ul>
49
- </section>
50
- <section>
51
- <h2>Content</h2>
52
- <pre id="content" class="note-content"></pre>
53
- </section>
54
- <section>
55
- <h2>Outgoing</h2>
56
- <ul id="outgoing"></ul>
57
- </section>
58
- <section>
59
- <h2>Backlinks</h2>
60
- <ul id="incoming"></ul>
61
- </section>
62
- </aside>
63
48
  </main>
49
+ <dialog id="contentDialog" class="content-dialog" aria-labelledby="contentTitle">
50
+ <article>
51
+ <header>
52
+ <div>
53
+ <span class="eyebrow">Markdown content</span>
54
+ <h2 id="contentTitle">Selected note</h2>
55
+ <p id="contentPath"></p>
56
+ </div>
57
+ <button id="contentClose" type="button">Close</button>
58
+ </header>
59
+ <div class="content-meta">
60
+ <section class="content-meta-section">
61
+ <h3>Tags</h3>
62
+ <div id="contentTags" class="tags"></div>
63
+ </section>
64
+ <section class="content-meta-section">
65
+ <h3>Outgoing</h3>
66
+ <ul id="contentOutgoing"></ul>
67
+ </section>
68
+ <section class="content-meta-section">
69
+ <h3>Backlinks</h3>
70
+ <ul id="contentIncoming"></ul>
71
+ </section>
72
+ </div>
73
+ <pre id="contentBody" class="note-content"></pre>
74
+ </article>
75
+ </dialog>
64
76
  <script src="/app.js"></script>
65
77
  </body>
66
78
  </html>`;