@andespindola/brainlink 0.1.0-beta.161 → 0.1.0-beta.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,14 @@
1
1
  export const createClientCss = () => `:root {
2
- color-scheme: dark;
3
- --bg: #0d0f12;
4
- --panel: #15191f;
5
- --panel-strong: #1c222b;
6
- --line: #29313c;
7
- --text: #edf2f7;
8
- --muted: #99a5b5;
9
- --accent: #35d0a2;
10
- --accent-weak: rgba(53, 208, 162, 0.14);
11
- --danger: #ff6b6b;
2
+ color-scheme: light;
3
+ --bg: #eef2f7;
4
+ --panel: #ffffff;
5
+ --panel-strong: #f7f9fc;
6
+ --line: #d9e1ec;
7
+ --text: #172033;
8
+ --muted: #657386;
9
+ --accent: #0b6fcb;
10
+ --accent-weak: rgba(11, 111, 203, 0.12);
11
+ --danger: #d84949;
12
12
  }
13
13
 
14
14
  * {
@@ -63,7 +63,8 @@ select {
63
63
  min-height: 72px;
64
64
  padding: 10px 16px;
65
65
  border-bottom: 1px solid var(--line);
66
- background: linear-gradient(180deg, rgba(17, 21, 27, 0.96) 0%, rgba(17, 21, 27, 0.86) 100%);
66
+ background: rgba(255, 255, 255, 0.96);
67
+ box-shadow: 0 1px 8px rgba(23, 32, 51, 0.08);
67
68
  backdrop-filter: blur(8px);
68
69
  }
69
70
 
@@ -82,8 +83,10 @@ select {
82
83
  width: 100%;
83
84
  height: 100%;
84
85
  background:
85
- radial-gradient(circle at 18% 20%, rgba(53, 208, 162, 0.12), transparent 28rem),
86
- linear-gradient(135deg, #0d0f12 0%, #12161c 55%, #0a0d10 100%);
86
+ linear-gradient(rgba(23, 32, 51, 0.035) 1px, transparent 1px),
87
+ linear-gradient(90deg, rgba(23, 32, 51, 0.035) 1px, transparent 1px),
88
+ #f6f8fb;
89
+ background-size: 28px 28px, 28px 28px, auto;
87
90
  overflow: hidden;
88
91
  }
89
92
 
@@ -126,23 +129,23 @@ select {
126
129
  .graph-label {
127
130
  position: absolute;
128
131
  max-width: 220px;
129
- transform: translate(-50%, calc(-100% - 10px));
130
- padding: 4px 7px;
131
- border: 1px solid rgba(129, 146, 170, 0.28);
132
+ transform: translate(-50%, calc(-100% - 12px));
133
+ padding: 4px 8px;
134
+ border: 1px solid rgba(101, 115, 134, 0.24);
132
135
  border-radius: 6px;
133
- background: rgba(13, 16, 20, 0.78);
136
+ background: rgba(255, 255, 255, 0.92);
134
137
  color: var(--text);
135
138
  font-size: 11px;
136
139
  line-height: 1.25;
137
140
  white-space: nowrap;
138
141
  overflow: hidden;
139
142
  text-overflow: ellipsis;
140
- box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
143
+ box-shadow: 0 8px 22px rgba(23, 32, 51, 0.12);
141
144
  }
142
145
 
143
146
  .graph-label.is-focused {
144
- border-color: rgba(53, 208, 162, 0.72);
145
- color: #dffbf3;
147
+ border-color: rgba(11, 111, 203, 0.56);
148
+ color: #0b4f92;
146
149
  }
147
150
 
148
151
  .graph-tooltip {
@@ -152,12 +155,12 @@ select {
152
155
  padding: 8px 10px;
153
156
  border: 1px solid var(--line);
154
157
  border-radius: 6px;
155
- background: rgba(13, 16, 20, 0.94);
158
+ background: rgba(255, 255, 255, 0.96);
156
159
  color: var(--text);
157
160
  font-size: 12px;
158
161
  line-height: 1.35;
159
162
  pointer-events: none;
160
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38);
163
+ box-shadow: 0 16px 40px rgba(23, 32, 51, 0.18);
161
164
  }
162
165
 
163
166
  .graph-tooltip strong,
@@ -181,8 +184,8 @@ select {
181
184
  height: 120px;
182
185
  border: 1px solid rgba(129, 146, 170, 0.28);
183
186
  border-radius: 8px;
184
- background: rgba(13, 16, 20, 0.78);
185
- box-shadow: 0 16px 42px rgba(0, 0, 0, 0.38);
187
+ background: rgba(255, 255, 255, 0.88);
188
+ box-shadow: 0 16px 42px rgba(23, 32, 51, 0.16);
186
189
  }
187
190
 
188
191
  .eyebrow {
@@ -215,7 +218,7 @@ select {
215
218
  border: 1px solid var(--line);
216
219
  border-radius: 8px;
217
220
  outline: none;
218
- background: rgba(21, 25, 31, 0.88);
221
+ background: rgba(255, 255, 255, 0.94);
219
222
  color: var(--text);
220
223
  padding: 0 14px;
221
224
  }
@@ -236,7 +239,7 @@ select {
236
239
  height: 38px;
237
240
  border: 1px solid var(--line);
238
241
  border-radius: 8px;
239
- background: rgba(21, 25, 31, 0.88);
242
+ background: rgba(255, 255, 255, 0.94);
240
243
  color: var(--text);
241
244
  cursor: pointer;
242
245
  }
@@ -257,7 +260,7 @@ select {
257
260
  padding: 10px 12px;
258
261
  border: 1px solid var(--line);
259
262
  border-radius: 10px;
260
- background: rgba(21, 25, 31, 0.88);
263
+ background: rgba(255, 255, 255, 0.94);
261
264
  display: grid;
262
265
  gap: 3px;
263
266
  }
@@ -332,7 +335,7 @@ li small {
332
335
  padding: 12px;
333
336
  border: 1px solid var(--line);
334
337
  border-radius: 8px;
335
- background: #101419;
338
+ background: #f8fafc;
336
339
  color: var(--text);
337
340
  white-space: pre-wrap;
338
341
  overflow: auto;
@@ -342,7 +345,8 @@ li small {
342
345
  }
343
346
 
344
347
  .content-dialog {
345
- position: fixed;
348
+ position: absolute;
349
+ z-index: 6;
346
350
  top: 12px;
347
351
  right: 12px;
348
352
  bottom: 12px;
@@ -350,7 +354,7 @@ li small {
350
354
  top: max(12px, env(safe-area-inset-top));
351
355
  right: max(12px, env(safe-area-inset-right));
352
356
  bottom: max(12px, env(safe-area-inset-bottom));
353
- left: max(12px, calc(100vw - 780px));
357
+ left: max(12px, calc(100% - 780px));
354
358
  margin: 0;
355
359
  width: auto;
356
360
  height: auto;
@@ -361,13 +365,12 @@ li small {
361
365
  border-radius: 8px;
362
366
  background: var(--panel);
363
367
  color: var(--text);
364
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
368
+ box-shadow: 0 24px 80px rgba(23, 32, 51, 0.22);
365
369
  overflow: hidden;
366
370
  }
367
371
 
368
- .content-dialog::backdrop {
369
- background: rgba(4, 7, 10, 0.72);
370
- backdrop-filter: blur(4px);
372
+ .content-dialog[hidden] {
373
+ display: none;
371
374
  }
372
375
 
373
376
  .content-dialog article {
@@ -466,7 +469,7 @@ li small {
466
469
  padding: 10px;
467
470
  border: 1px solid var(--line);
468
471
  border-radius: 8px;
469
- background: var(--panel-strong);
472
+ background: #f8fafc;
470
473
  display: grid;
471
474
  grid-template-rows: auto minmax(0, 1fr);
472
475
  gap: 8px;
@@ -52,47 +52,47 @@ export const createClientHtml = () => `<!doctype html>
52
52
  <div id="graphLabels" class="graph-labels" aria-hidden="true"></div>
53
53
  <div id="graphTooltip" class="graph-tooltip" role="tooltip" hidden></div>
54
54
  <canvas id="miniMap" class="mini-map" aria-label="Graph overview"></canvas>
55
+ <aside id="contentDialog" class="content-dialog" role="dialog" aria-labelledby="contentTitle" hidden>
56
+ <article>
57
+ <header>
58
+ <div>
59
+ <span class="eyebrow">Node details</span>
60
+ <h2 id="contentTitle">Selected note</h2>
61
+ <p id="contentPath"></p>
62
+ </div>
63
+ <button id="contentClose" type="button" aria-label="Close node details" title="Close node details">&times;</button>
64
+ </header>
65
+ <div class="content-meta">
66
+ <section class="content-meta-section">
67
+ <h3>Facts</h3>
68
+ <ul id="contentFacts"></ul>
69
+ </section>
70
+ <section class="content-meta-section">
71
+ <h3>Context Links</h3>
72
+ <ul id="contentContextLinks"></ul>
73
+ </section>
74
+ <section class="content-meta-section">
75
+ <h3>Tags</h3>
76
+ <div id="contentTags" class="tags"></div>
77
+ </section>
78
+ <section class="content-meta-section">
79
+ <h3>Outgoing</h3>
80
+ <ul id="contentOutgoing"></ul>
81
+ </section>
82
+ <section class="content-meta-section">
83
+ <h3>Backlinks</h3>
84
+ <ul id="contentIncoming"></ul>
85
+ </section>
86
+ </div>
87
+ <pre id="contentBody" class="note-content"></pre>
88
+ </article>
89
+ </aside>
55
90
  </div>
56
91
  </section>
57
92
  </main>
58
93
  <footer class="app-footer" aria-label="Copyright notice">
59
94
  <small>Copyright © 2026 Substructa</small>
60
95
  </footer>
61
- <dialog id="contentDialog" class="content-dialog" aria-labelledby="contentTitle">
62
- <article>
63
- <header>
64
- <div>
65
- <span class="eyebrow">Node details</span>
66
- <h2 id="contentTitle">Selected note</h2>
67
- <p id="contentPath"></p>
68
- </div>
69
- <button id="contentClose" type="button" aria-label="Close node details" title="Close node details">&times;</button>
70
- </header>
71
- <div class="content-meta">
72
- <section class="content-meta-section">
73
- <h3>Facts</h3>
74
- <ul id="contentFacts"></ul>
75
- </section>
76
- <section class="content-meta-section">
77
- <h3>Context Links</h3>
78
- <ul id="contentContextLinks"></ul>
79
- </section>
80
- <section class="content-meta-section">
81
- <h3>Tags</h3>
82
- <div id="contentTags" class="tags"></div>
83
- </section>
84
- <section class="content-meta-section">
85
- <h3>Outgoing</h3>
86
- <ul id="contentOutgoing"></ul>
87
- </section>
88
- <section class="content-meta-section">
89
- <h3>Backlinks</h3>
90
- <ul id="contentIncoming"></ul>
91
- </section>
92
- </div>
93
- <pre id="contentBody" class="note-content"></pre>
94
- </article>
95
- </dialog>
96
96
  <script src="/app.js"></script>
97
97
  </body>
98
98
  </html>`;
@@ -369,14 +369,39 @@ const parseColor = (hex) => {
369
369
  }
370
370
 
371
371
  const graphTheme = {
372
- node: parseColor('#aeb8c5'),
373
- nodeCluster: parseColor('#6bb7e8'),
374
- nodeHighlight: parseColor('#f5c24a'),
375
- nodeSelected: parseColor('#ffffff'),
376
- edge: [0.58, 0.64, 0.74, 0.24],
377
- edgeHeavy: [0.78, 0.84, 0.92, 0.44],
378
- clear: parseColor('#0d0f12')
379
- }
372
+ node: parseColor('#4c8eda'),
373
+ nodeCluster: parseColor('#2f6fb4'),
374
+ nodeHighlight: parseColor('#f2b441'),
375
+ nodeSelected: parseColor('#172033'),
376
+ nodePalette: [
377
+ parseColor('#4c8eda'),
378
+ parseColor('#65b96e'),
379
+ parseColor('#f0a33a'),
380
+ parseColor('#d95f8d'),
381
+ parseColor('#8d72d9'),
382
+ parseColor('#55bfc4'),
383
+ parseColor('#ec6b56'),
384
+ parseColor('#9aa6b2'),
385
+ parseColor('#b78255'),
386
+ parseColor('#6f9fd8')
387
+ ],
388
+ edge: [0.23, 0.31, 0.42, 0.18],
389
+ edgeHeavy: [0.23, 0.31, 0.42, 0.34],
390
+ clear: parseColor('#f6f8fb')
391
+ }
392
+
393
+ const segmentPalette = ['#4c8eda', '#65b96e', '#f0a33a', '#d95f8d', '#8d72d9', '#55bfc4', '#ec6b56', '#9aa6b2', '#b78255', '#6f9fd8']
394
+
395
+ const segmentColorIndex = (segment) => {
396
+ const value = String(segment || '')
397
+ let hash = 0
398
+ for (let index = 0; index < value.length; index += 1) {
399
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
400
+ }
401
+ return Math.abs(hash) % segmentPalette.length
402
+ }
403
+
404
+ const segmentColor = (segment) => segmentPalette[segmentColorIndex(segment)] || segmentPalette[0]
380
405
 
381
406
  const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
382
407
 
@@ -518,7 +543,7 @@ const drawFallback = () => {
518
543
  canvas.width = Math.floor(width * ratio)
519
544
  canvas.height = Math.floor(height * ratio)
520
545
  ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
521
- ctx2dFallback.fillStyle = '#0d0f12'
546
+ ctx2dFallback.fillStyle = '#f6f8fb'
522
547
  ctx2dFallback.fillRect(0, 0, width, height)
523
548
 
524
549
  const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
@@ -528,7 +553,7 @@ const drawFallback = () => {
528
553
  nodeById.set(nodes[i][0], nodes[i])
529
554
  }
530
555
 
531
- ctx2dFallback.strokeStyle = 'rgba(150,165,190,0.2)'
556
+ ctx2dFallback.strokeStyle = 'rgba(59,79,108,0.18)'
532
557
  ctx2dFallback.lineWidth = 1
533
558
  for (let i = 0; i < edges.length; i += 1) {
534
559
  const edge = edges[i]
@@ -547,7 +572,7 @@ const drawFallback = () => {
547
572
  const node = nodes[i]
548
573
  const p = worldToScreen(node[2], node[3])
549
574
  const selected = state.selectedNodeId === node[0]
550
- const color = node[6] === 'cluster' ? '#6bb7e8' : '#aeb8c5'
575
+ const color = segmentColor(node[5] || node[4] || node[1])
551
576
  const radius = Math.max(2.4, Math.min(14, 4 + node[7] * 0.55))
552
577
 
553
578
  ctx2dFallback.beginPath()
@@ -556,7 +581,7 @@ const drawFallback = () => {
556
581
  ctx2dFallback.fill()
557
582
  }
558
583
 
559
- ctx2dFallback.fillStyle = '#edf2f7'
584
+ ctx2dFallback.fillStyle = '#172033'
560
585
  ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
561
586
  ctx2dFallback.textAlign = 'center'
562
587
  ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
@@ -718,7 +743,7 @@ const drawMiniMap = () => {
718
743
  miniMap.height = Math.floor(height * ratio)
719
744
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
720
745
  ctx.clearRect(0, 0, width, height)
721
- ctx.fillStyle = 'rgba(13, 16, 20, 0.86)'
746
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.88)'
722
747
  ctx.fillRect(0, 0, width, height)
723
748
 
724
749
  const xs = nodes.map((node) => Number(node[2])).filter(Number.isFinite)
@@ -738,7 +763,7 @@ const drawMiniMap = () => {
738
763
  })
739
764
  state.miniMapView = { minX, minY, scale, offsetX, offsetY, width, height }
740
765
 
741
- ctx.fillStyle = 'rgba(174, 184, 197, 0.62)'
766
+ ctx.fillStyle = 'rgba(76, 142, 218, 0.62)'
742
767
  nodes.forEach((node) => {
743
768
  const point = toMini(Number(node[2]), Number(node[3]))
744
769
  ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
@@ -748,7 +773,7 @@ const drawMiniMap = () => {
748
773
  const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
749
774
  const topLeft = toMini(Math.min(worldTopLeft.x, worldBottomRight.x), Math.min(worldTopLeft.y, worldBottomRight.y))
750
775
  const bottomRight = toMini(Math.max(worldTopLeft.x, worldBottomRight.x), Math.max(worldTopLeft.y, worldBottomRight.y))
751
- ctx.strokeStyle = 'rgba(53, 208, 162, 0.86)'
776
+ ctx.strokeStyle = 'rgba(11, 111, 203, 0.86)'
752
777
  ctx.lineWidth = 1
753
778
  ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
754
779
  }
@@ -866,10 +891,11 @@ const linkedNodes = (node) => {
866
891
  }
867
892
 
868
893
  const openContentDialog = () => {
869
- const dialog = elements.contentDialog
870
- if (!dialog.open) {
871
- dialog.show()
872
- }
894
+ elements.contentDialog.hidden = false
895
+ }
896
+
897
+ const closeContentDialog = () => {
898
+ elements.contentDialog.hidden = true
873
899
  }
874
900
 
875
901
  const loadNodeDetails = async (nodeId) => {
@@ -1282,6 +1308,10 @@ const setupInput = () => {
1282
1308
  })
1283
1309
 
1284
1310
  window.addEventListener('keydown', (event) => {
1311
+ if (event.key === 'Escape' && !elements.contentDialog.hidden) {
1312
+ closeContentDialog()
1313
+ return
1314
+ }
1285
1315
  if (event.key === '+') {
1286
1316
  zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 1.06)
1287
1317
  return
@@ -1323,12 +1353,12 @@ const setupControls = () => {
1323
1353
  })
1324
1354
 
1325
1355
  elements.contentClose.addEventListener('click', () => {
1326
- elements.contentDialog.close()
1356
+ closeContentDialog()
1327
1357
  })
1328
1358
 
1329
1359
  elements.contentDialog.addEventListener('click', (event) => {
1330
1360
  if (event.target === elements.contentDialog) {
1331
- elements.contentDialog.close()
1361
+ closeContentDialog()
1332
1362
  }
1333
1363
  })
1334
1364
 
@@ -14,6 +14,7 @@ const state = {
14
14
  y: new Float32Array(0),
15
15
  relevance: new Float32Array(0),
16
16
  radius: new Float32Array(0),
17
+ colorIndex: new Uint8Array(0),
17
18
  visible: new Uint8Array(0),
18
19
  highlighted: new Uint8Array(0),
19
20
  focused: new Uint8Array(0),
@@ -39,13 +40,25 @@ let pointPositionsBuffer = new Float32Array(0)
39
40
  let pointSizesBuffer = new Float32Array(0)
40
41
 
41
42
  const defaultTheme = {
42
- node: [0.68, 0.72, 0.78, 1],
43
- nodeCluster: [0.42, 0.76, 0.92, 1],
44
- nodeHighlight: [0.95, 0.76, 0.22, 1],
45
- nodeSelected: [0.99, 0.99, 1, 1],
46
- edge: [0.58, 0.64, 0.74, 0.24],
47
- edgeHeavy: [0.78, 0.84, 0.92, 0.44],
48
- clear: [0.05, 0.06, 0.08, 1]
43
+ node: [0.30, 0.56, 0.85, 1],
44
+ nodeCluster: [0.18, 0.44, 0.71, 1],
45
+ nodeHighlight: [0.95, 0.70, 0.25, 1],
46
+ nodeSelected: [0.09, 0.13, 0.20, 1],
47
+ nodePalette: [
48
+ [0.30, 0.56, 0.85, 1],
49
+ [0.40, 0.73, 0.43, 1],
50
+ [0.94, 0.64, 0.23, 1],
51
+ [0.85, 0.37, 0.55, 1],
52
+ [0.55, 0.45, 0.85, 1],
53
+ [0.33, 0.75, 0.77, 1],
54
+ [0.93, 0.42, 0.34, 1],
55
+ [0.60, 0.65, 0.70, 1],
56
+ [0.72, 0.51, 0.33, 1],
57
+ [0.44, 0.62, 0.85, 1]
58
+ ],
59
+ edge: [0.23, 0.31, 0.42, 0.18],
60
+ edgeHeavy: [0.23, 0.31, 0.42, 0.34],
61
+ clear: [0.96, 0.97, 0.98, 1]
49
62
  }
50
63
 
51
64
  const theme = { ...defaultTheme }
@@ -181,6 +194,7 @@ const ensureNodeCapacity = (count) => {
181
194
  state.y = new Float32Array(nextCapacity)
182
195
  state.relevance = new Float32Array(nextCapacity)
183
196
  state.radius = new Float32Array(nextCapacity)
197
+ state.colorIndex = new Uint8Array(nextCapacity)
184
198
  state.visible = new Uint8Array(nextCapacity)
185
199
  state.highlighted = new Uint8Array(nextCapacity)
186
200
  state.focused = new Uint8Array(nextCapacity)
@@ -199,11 +213,21 @@ const ensureEdgeCapacity = (count) => {
199
213
  }
200
214
 
201
215
  const nodeRadius = (relevance, kind) => {
202
- const base = kind === 'cluster' ? 7.8 : 4.6
203
- const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
216
+ const base = kind === 'cluster' ? 8.8 : 5.4
217
+ const modifier = Math.min(5.6, Math.max(0, relevance * 0.62))
204
218
  return base + modifier
205
219
  }
206
220
 
221
+ const segmentColorIndex = (segment) => {
222
+ const value = String(segment || '')
223
+ let hash = 0
224
+ for (let index = 0; index < value.length; index += 1) {
225
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
226
+ }
227
+ const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
228
+ return Math.abs(hash) % palette.length
229
+ }
230
+
207
231
  const loadChunk = (chunk) => {
208
232
  const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
209
233
  const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
@@ -223,6 +247,7 @@ const loadChunk = (chunk) => {
223
247
  const title = typeof row?.[1] === 'string' ? row[1] : id
224
248
  const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
225
249
  const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
250
+ const segment = typeof row?.[5] === 'string' ? row[5] : ''
226
251
  const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
227
252
  const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
228
253
 
@@ -233,6 +258,7 @@ const loadChunk = (chunk) => {
233
258
  state.y[index] = y
234
259
  state.relevance[index] = relevance
235
260
  state.radius[index] = nodeRadius(relevance, kind)
261
+ state.colorIndex[index] = segmentColorIndex(segment || title)
236
262
  state.visible[index] = 0
237
263
  state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
238
264
  state.focused[index] = focusedIds.has(id) ? 1 : 0
@@ -349,6 +375,13 @@ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
349
375
  gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
350
376
  }
351
377
 
378
+ const drawColoredNodeLayer = (predicate, radiusBoost = 1) => {
379
+ const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
380
+ for (let colorIndex = 0; colorIndex < palette.length; colorIndex += 1) {
381
+ drawNodeLayer((index) => predicate(index) && state.colorIndex[index] === colorIndex, palette[colorIndex], radiusBoost)
382
+ }
383
+ }
384
+
352
385
  const clear = () => {
353
386
  if (!gl || !canvas) return
354
387
  gl.viewport(0, 0, canvas.width, canvas.height)
@@ -398,15 +431,13 @@ const renderFrame = (now) => {
398
431
  scheduleSettledRender(now)
399
432
  }
400
433
 
401
- drawNodeLayer(
402
- (index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
403
- theme.node,
434
+ drawColoredNodeLayer(
435
+ (index) => state.visible[index] === 1 && state.kinds[index] !== 'cluster' && state.selected[index] === 0 && state.highlighted[index] === 0 && state.focused[index] === 0,
404
436
  1
405
437
  )
406
438
 
407
- drawNodeLayer(
439
+ drawColoredNodeLayer(
408
440
  (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
409
- theme.nodeCluster,
410
441
  1.15
411
442
  )
412
443
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.161",
3
+ "version": "0.1.0-beta.163",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",