Skip to content

Commit 2e00f23

Browse files
selcuxclaude
andcommitted
Add orphan page detection and toolbar
Surfaces pages with zero wikilinks as orphan nodes (purple), toggled via a new toolbar header bar. Preference persists across sessions via clientStore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0dcc14a commit 2e00f23

File tree

7 files changed

+211
-30
lines changed

7 files changed

+211
-30
lines changed

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ An interactive knowledge graph that lives in the right panel. Shows **all pages
1414
- **Zoom & pan** — scroll to zoom, drag background to pan
1515
- **Hover highlighting** — hover a node to highlight its connections, dim the rest
1616
- **Adaptive node sizing** — nodes scale with connection count
17+
- **Orphan page detection** — surfaces pages with zero wikilinks (toggled via toolbar)
18+
- **Toolbar** — header bar with toggle buttons for graph options
1719
- **Dark/light mode** — follows SilverBullet's theme automatically
1820

1921
## Install
@@ -30,6 +32,8 @@ Run the command: **Atlas: Toggle Graph View**
3032

3133
This opens (or closes) the graph panel on the right side. The graph automatically updates as you navigate between pages.
3234

35+
The toolbar at the top of the panel lets you toggle **Orphans** — pages with no incoming or outgoing links. Your preference persists across sessions.
36+
3337
## Known Issues
3438

3539
- **Script-generated links not shown** — Links produced dynamically by Space Lua templates (e.g. `${string.format("[[Journal/%s|Today's Journal]]", os.date("%Y-%m-%d"))}`) are not included in the graph. The SilverBullet index only tracks statically written wikilinks.
@@ -65,10 +69,12 @@ Web Worker (no DOM) Panel iframe (has DOM)
6569
│ atlas.ts │──JSON──▶│ d3.min.js │
6670
│ ├─ toggleAtlas() │ │ atlas-render.js │
6771
│ ├─ updateGraph() │◀─call───│ atlas-style.css │
68-
│ └─ handleNavigate() │ │ │
69-
│ │ │ SVG force graph │
70-
│ graph.ts │ │ drag/zoom/click │
71-
│ └─ buildFullGraph() │ └──────────────────────┘
72+
│ ├─ handleNavigate() │ │ │
73+
│ └─ setOption() │ │ Toolbar + SVG graph │
74+
│ │ │ drag/zoom/click │
75+
│ graph.ts │ └──────────────────────┘
76+
│ ├─ buildFullGraph() │
77+
│ └─ queryAllPages() │
7278
│ ▼ │
7379
│ SB Index syscalls │
7480
└─────────────────────┘
@@ -89,4 +95,8 @@ All colors are defined as CSS custom properties in `atlas-style.css`, keyed by `
8995
| `--atlas-node-neighbor` | Other nodes |
9096
| `--atlas-edge` / `--atlas-edge-highlight` | Edge default / hover |
9197
| `--atlas-label` / `--atlas-label-current` | Label text |
92-
| `--atlas-node-dim` / `--atlas-edge-dim` / `--atlas-label-dim` | Dimmed on hover |
98+
| `--atlas-node-orphan` | Orphan node fill + stroke |
99+
| `--atlas-node-dim` / `--atlas-edge-dim` / `--atlas-label-dim` | Dimmed on hover |
100+
| `--atlas-toolbar-bg` | Toolbar background |
101+
| `--atlas-toolbar-border` | Toolbar bottom border |
102+
| `--atlas-btn-hover` / `--atlas-btn-active` | Button states |

assets/atlas-render.js

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
// Atlas graph renderer — runs inside the panel iframe
22
// Expects: window.__ATLAS_DATA__ = { nodes: [...], edges: [...] }
33
// window.__ATLAS_DARK__ = boolean
4+
// window.__ATLAS_OPTIONS__ = { showOrphans: boolean }
45
(function () {
56
"use strict";
67

78
const data = window.__ATLAS_DATA__;
89
if (!data || !data.nodes.length) return;
910

11+
const options = window.__ATLAS_OPTIONS__ || { showOrphans: false };
12+
1013
const container = document.getElementById("atlas-container");
1114
if (!container) return;
1215

13-
// --- Color palette (reads CSS custom properties from atlas-style.css) ---
14-
// SB's panel.tsx already sets data-theme on <html> via postMessage before our script runs.
15-
// Fall back to __ATLAS_DARK__ (from getUiOption) or prefers-color-scheme if somehow missing.
16+
// --- Theme detection ---
1617
const sbTheme = document.documentElement.getAttribute("data-theme");
1718
const isDark = sbTheme
1819
? sbTheme === "dark"
@@ -29,6 +30,7 @@
2930
bg: v("--atlas-bg"),
3031
currentNode: v("--atlas-node-current"),
3132
neighborNode: v("--atlas-node-neighbor"),
33+
orphanNode: v("--atlas-node-orphan"),
3234
edge: v("--atlas-edge"),
3335
edgeHighlight: v("--atlas-edge-highlight"),
3436
label: v("--atlas-label"),
@@ -38,6 +40,24 @@
3840
dimLabel: v("--atlas-label-dim"),
3941
};
4042

43+
// --- Toolbar ---
44+
const toolbar = document.getElementById("atlas-toolbar");
45+
if (toolbar) {
46+
const orphanBtn = document.createElement("button");
47+
orphanBtn.className = "atlas-btn" + (options.showOrphans ? " active" : "");
48+
orphanBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
49+
<circle cx="8" cy="8" r="6" stroke-dasharray="3,2"/>
50+
</svg><span>Orphans</span>`;
51+
orphanBtn.addEventListener("click", () => {
52+
options.showOrphans = !options.showOrphans;
53+
orphanBtn.classList.toggle("active", options.showOrphans);
54+
toggleOrphanVisibility(options.showOrphans);
55+
// Fire-and-forget persist to worker
56+
syscall("system.invokeFunction", "atlas.setOption", "showOrphans", options.showOrphans);
57+
});
58+
toolbar.appendChild(orphanBtn);
59+
}
60+
4161
// --- Dimensions ---
4262
const width = container.clientWidth || 300;
4363
const height = container.clientHeight || 400;
@@ -118,18 +138,26 @@
118138
.selectAll("g")
119139
.data(data.nodes)
120140
.join("g")
121-
.attr("class", "node-group")
141+
.attr("class", (d) => "node-group" + (d.isOrphan ? " orphan" : ""))
122142
.style("cursor", "pointer");
123143

124-
// Circles — radius scales with connection count
144+
// Circles — radius scales with connection count; orphans are smaller
125145
node
126146
.append("circle")
127147
.attr("r", (d) => {
148+
if (d.isOrphan) return 3;
128149
const deg = degree.get(d.id) || 0;
129150
const base = d.isCurrent ? 7 : 4;
130151
return base + Math.min(deg, 10) * 0.4;
131152
})
132-
.attr("fill", (d) => (d.isCurrent ? palette.currentNode : palette.neighborNode));
153+
.attr("fill", (d) => {
154+
if (d.isOrphan) return palette.orphanNode;
155+
return d.isCurrent ? palette.currentNode : palette.neighborNode;
156+
})
157+
.attr("fill-opacity", (d) => d.isOrphan ? 0.4 : 1)
158+
.attr("stroke", (d) => d.isOrphan ? palette.orphanNode : "none")
159+
.attr("stroke-width", (d) => d.isOrphan ? 1.5 : 0)
160+
.attr("stroke-dasharray", (d) => d.isOrphan ? "2,2" : "none");
133161

134162
// Labels — always outside the circle
135163
node
@@ -138,14 +166,37 @@
138166
.attr("dx", 10)
139167
.attr("dy", 4)
140168
.attr("text-anchor", "start")
141-
.attr("fill", (d) => (d.isCurrent ? palette.currentNode : palette.label))
169+
.attr("fill", (d) => {
170+
if (d.isOrphan) return palette.orphanNode;
171+
return d.isCurrent ? palette.currentNode : palette.label;
172+
})
173+
.attr("fill-opacity", (d) => d.isOrphan ? 0.6 : 1)
142174
.attr("font-size", "10px")
143175
.attr("font-weight", (d) => (d.isCurrent ? "600" : "400"))
144176
.attr("font-family", "system-ui, -apple-system, sans-serif")
145177
.attr("paint-order", "stroke")
146178
.attr("stroke", palette.bg)
147179
.attr("stroke-width", 3);
148180

181+
// --- Orphan visibility ---
182+
function toggleOrphanVisibility(show) {
183+
const orphans = node.filter((d) => d.isOrphan);
184+
orphans
185+
.transition()
186+
.duration(300)
187+
.style("opacity", show ? 1 : 0)
188+
.on("end", function () {
189+
d3.select(this).style("pointer-events", show ? "auto" : "none");
190+
});
191+
// Reheat simulation so orphans settle naturally when shown
192+
if (show) {
193+
simulation.alpha(0.3).restart();
194+
}
195+
}
196+
197+
// Apply initial orphan visibility
198+
toggleOrphanVisibility(options.showOrphans);
199+
149200
// --- Drag behavior ---
150201
const drag = d3
151202
.drag()
@@ -177,15 +228,15 @@
177228
const neighbors = adjacency.get(d.id) || new Set();
178229

179230
node.select("circle").attr("fill", (n) => {
180-
if (n.id === d.id) return palette.currentNode;
231+
if (n.id === d.id) return n.isOrphan ? palette.orphanNode : palette.currentNode;
181232
if (neighbors.has(n.id)) {
182233
return n.isCurrent ? palette.currentNode : palette.neighborNode;
183234
}
184235
return palette.dimNode;
185236
});
186237

187238
node.select("text").attr("fill", (n) => {
188-
if (n.id === d.id) return n.isCurrent ? palette.labelCurrent : palette.label;
239+
if (n.id === d.id) return n.isCurrent ? palette.labelCurrent : (n.isOrphan ? palette.orphanNode : palette.label);
189240
if (neighbors.has(n.id)) {
190241
return n.isCurrent ? palette.labelCurrent : palette.label;
191242
}
@@ -209,15 +260,17 @@
209260
.on("mouseleave", () => {
210261
node
211262
.select("circle")
212-
.attr("fill", (n) =>
213-
n.isCurrent ? palette.currentNode : palette.neighborNode
214-
);
263+
.attr("fill", (n) => {
264+
if (n.isOrphan) return palette.orphanNode;
265+
return n.isCurrent ? palette.currentNode : palette.neighborNode;
266+
});
215267

216268
node
217269
.select("text")
218-
.attr("fill", (n) =>
219-
n.isCurrent ? palette.labelCurrent : palette.label
220-
);
270+
.attr("fill", (n) => {
271+
if (n.isOrphan) return palette.orphanNode;
272+
return n.isCurrent ? palette.labelCurrent : palette.label;
273+
});
221274

222275
link.attr("stroke", palette.edge).attr("stroke-width", 1.5);
223276
});

assets/atlas-style.css

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,40 @@
33
--atlas-bg: #ffffff;
44
--atlas-node-current: #1e66f5;
55
--atlas-node-neighbor: #6c6f85;
6+
--atlas-node-orphan: #8839ef;
67
--atlas-edge: #bcc0cc;
78
--atlas-edge-highlight: #1e66f5;
89
--atlas-label: #4c4f69;
910
--atlas-label-current: #ffffff;
1011
--atlas-node-dim: #dce0e8;
1112
--atlas-edge-dim: #dce0e8;
1213
--atlas-label-dim: #bcc0cc;
14+
--atlas-toolbar-bg: #f2f4f7;
15+
--atlas-toolbar-border: #dce0e8;
16+
--atlas-btn-hover: #e6e9ef;
17+
--atlas-btn-active: #1e66f5;
18+
--atlas-btn-active-text: #ffffff;
19+
--atlas-btn-text: #4c4f69;
1320
}
1421

1522
:root[data-theme="dark"] {
1623
--atlas-bg: #1e1e2e;
1724
--atlas-node-current: #89b4fa;
1825
--atlas-node-neighbor: #a6adc8;
26+
--atlas-node-orphan: #f5c2e7;
1927
--atlas-edge: #45475a;
2028
--atlas-edge-highlight: #89b4fa;
2129
--atlas-label: #cdd6f4;
2230
--atlas-label-current: #1e1e2e;
2331
--atlas-node-dim: #313244;
2432
--atlas-edge-dim: #313244;
2533
--atlas-label-dim: #585b70;
34+
--atlas-toolbar-bg: #181825;
35+
--atlas-toolbar-border: #313244;
36+
--atlas-btn-hover: #313244;
37+
--atlas-btn-active: #89b4fa;
38+
--atlas-btn-active-text: #1e1e2e;
39+
--atlas-btn-text: #cdd6f4;
2640
}
2741

2842
* {
@@ -36,11 +50,51 @@ html, body {
3650
height: 100%;
3751
overflow: hidden;
3852
font-family: system-ui, -apple-system, sans-serif;
53+
display: flex;
54+
flex-direction: column;
55+
}
56+
57+
#atlas-toolbar {
58+
display: flex;
59+
gap: 4px;
60+
padding: 4px 8px;
61+
background-color: var(--atlas-toolbar-bg);
62+
border-bottom: 1px solid var(--atlas-toolbar-border);
63+
flex-shrink: 0;
64+
}
65+
66+
.atlas-btn {
67+
display: inline-flex;
68+
align-items: center;
69+
gap: 4px;
70+
padding: 3px 8px;
71+
border: none;
72+
border-radius: 4px;
73+
background: transparent;
74+
color: var(--atlas-btn-text);
75+
font-size: 11px;
76+
font-family: inherit;
77+
cursor: pointer;
78+
transition: background-color 0.15s ease, color 0.15s ease;
79+
}
80+
81+
.atlas-btn:hover {
82+
background-color: var(--atlas-btn-hover);
83+
}
84+
85+
.atlas-btn.active {
86+
background-color: var(--atlas-btn-active);
87+
color: var(--atlas-btn-active-text);
88+
}
89+
90+
.atlas-btn svg {
91+
flex-shrink: 0;
3992
}
4093

4194
#atlas-container {
4295
width: 100%;
43-
height: 100vh;
96+
flex: 1;
97+
min-height: 0;
4498
position: relative;
4599
background-color: var(--atlas-bg);
46100
}

0 commit comments

Comments
 (0)