backpack-viewer 0.2.15 → 0.2.17

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/README.md CHANGED
@@ -34,13 +34,85 @@ backpack-ontology (MCP) ──writes──> ~/.local/share/backpack/ontologies/
34
34
  backpack-viewer ──reads──────────────────┘
35
35
  ```
36
36
 
37
+ ## Configuration
38
+
39
+ The viewer reads an optional config file for customizing keybindings and other settings. The config file follows the [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/latest/) convention:
40
+
41
+ ```
42
+ ~/.config/backpack/viewer.json
43
+ ```
44
+
45
+ Override with environment variables:
46
+ - `$XDG_CONFIG_HOME/backpack/viewer.json`
47
+ - `$BACKPACK_DIR/config/viewer.json`
48
+
49
+ ### Keybindings
50
+
51
+ Create `~/.config/backpack/viewer.json` and override any binding. Unspecified keys keep their defaults.
52
+
53
+ ```json
54
+ {
55
+ "keybindings": {
56
+ "search": "s",
57
+ "focus": "g",
58
+ "panLeft": "a",
59
+ "panDown": "s",
60
+ "panUp": "w",
61
+ "panRight": "d"
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### Available actions
67
+
68
+ | Action | Default | Description |
69
+ |---|---|---|
70
+ | `search` | `/` | Focus the search bar |
71
+ | `searchAlt` | `ctrl+k` | Focus the search bar (alternate) |
72
+ | `undo` | `ctrl+z` | Undo last edit |
73
+ | `redo` | `ctrl+shift+z` | Redo last edit |
74
+ | `help` | `?` | Toggle keyboard shortcuts help |
75
+ | `escape` | `Escape` | Exit focus mode or close panel |
76
+ | `focus` | `f` | Focus on selected nodes / exit focus |
77
+ | `toggleEdges` | `e` | Toggle edge visibility |
78
+ | `center` | `c` | Center view on the graph |
79
+ | `nextNode` | `.` | Cycle to next node in view |
80
+ | `prevNode` | `,` | Cycle to previous node in view |
81
+ | `nextConnection` | `>` | Cycle to next connection in info panel |
82
+ | `prevConnection` | `<` | Cycle to previous connection in info panel |
83
+ | `historyBack` | `(` | Go back in node inspection history |
84
+ | `historyForward` | `)` | Go forward in node inspection history |
85
+ | `hopsIncrease` | `=` | Increase hops in focus mode |
86
+ | `hopsDecrease` | `-` | Decrease hops in focus mode |
87
+ | `panLeft` | `h` | Pan camera left |
88
+ | `panDown` | `j` | Pan camera down |
89
+ | `panUp` | `k` | Pan camera up |
90
+ | `panRight` | `l` | Pan camera right |
91
+ | `panFastLeft` | `H` | Pan camera left (fast) |
92
+ | `panFastRight` | `L` | Pan camera right (fast) |
93
+ | `zoomIn` | `K` | Zoom in |
94
+ | `zoomOut` | `J` | Zoom out |
95
+ | `spacingDecrease` | `[` | Decrease node spacing |
96
+ | `spacingIncrease` | `]` | Increase node spacing |
97
+ | `clusteringDecrease` | `{` | Decrease type clustering |
98
+ | `clusteringIncrease` | `}` | Increase type clustering |
99
+
100
+ ### Binding format
101
+
102
+ Bindings are strings with optional modifier prefixes separated by `+`:
103
+
104
+ - Single keys: `"f"`, `"/"`, `"?"`, `"Escape"`
105
+ - With modifiers: `"ctrl+z"`, `"ctrl+shift+z"`, `"alt+s"`
106
+ - `ctrl` and `cmd`/`meta` are treated as equivalent (works on both Mac and Linux)
107
+
37
108
  ## Reference
38
109
 
39
110
  | Variable | Effect |
40
111
  |---|---|
41
112
  | `PORT` | Override the default port (default: `5173`) |
113
+ | `XDG_CONFIG_HOME` | Override config location (default: `~/.config`) |
42
114
  | `XDG_DATA_HOME` | Override data location (default: `~/.local/share`) |
43
- | `BACKPACK_DIR` | Override data directory |
115
+ | `BACKPACK_DIR` | Override both config and data directories |
44
116
 
45
117
  ## Support
46
118
 
package/bin/serve.js CHANGED
@@ -15,9 +15,11 @@ const hasDistBuild = fs.existsSync(path.join(distDir, "index.html"));
15
15
  if (hasDistBuild) {
16
16
  // --- Production: static file server + API (zero native deps) ---
17
17
  const { JsonFileBackend, dataDir } = await import("backpack-ontology");
18
+ const { loadViewerConfig } = await import("../dist/config.js");
18
19
 
19
20
  const storage = new JsonFileBackend();
20
21
  await storage.initialize();
22
+ const viewerConfig = loadViewerConfig();
21
23
 
22
24
  const MIME_TYPES = {
23
25
  ".html": "text/html",
@@ -33,6 +35,12 @@ if (hasDistBuild) {
33
35
  const url = req.url?.replace(/\?.*$/, "") || "/";
34
36
 
35
37
  // --- API routes ---
38
+ if (url === "/api/config") {
39
+ res.writeHead(200, { "Content-Type": "application/json" });
40
+ res.end(JSON.stringify(viewerConfig));
41
+ return;
42
+ }
43
+
36
44
  if (url === "/api/ontologies") {
37
45
  try {
38
46
  const summaries = await storage.listOntologies();
@@ -0,0 +1 @@
1
+ *{margin:0;padding:0;box-sizing:border-box}:root{--bg: #141414;--bg-surface: #1a1a1a;--bg-hover: #222222;--bg-active: #2a2a2a;--bg-elevated: #1e1e1e;--bg-inset: #111111;--border: #2a2a2a;--text: #d4d4d4;--text-strong: #e5e5e5;--text-muted: #737373;--text-dim: #525252;--accent: #d4a27f;--accent-hover: #e8b898;--badge-text: #141414;--glass-bg: rgba(20, 20, 20, .85);--glass-border: rgba(255, 255, 255, .08);--chip-bg: rgba(42, 42, 42, .7);--chip-bg-active: rgba(42, 42, 42, .9);--chip-bg-hover: rgba(50, 50, 50, .9);--chip-border-active: rgba(255, 255, 255, .06);--shadow: rgba(0, 0, 0, .6);--shadow-strong: rgba(0, 0, 0, .5);--canvas-edge: rgba(255, 255, 255, .08);--canvas-edge-highlight: rgba(212, 162, 127, .5);--canvas-edge-dim: rgba(255, 255, 255, .03);--canvas-edge-label: rgba(255, 255, 255, .2);--canvas-edge-label-highlight: rgba(212, 162, 127, .7);--canvas-edge-label-dim: rgba(255, 255, 255, .05);--canvas-arrow: rgba(255, 255, 255, .12);--canvas-arrow-highlight: rgba(212, 162, 127, .5);--canvas-node-label: #a3a3a3;--canvas-node-label-dim: rgba(212, 212, 212, .2);--canvas-type-badge: rgba(115, 115, 115, .5);--canvas-type-badge-dim: rgba(115, 115, 115, .15);--canvas-selection-border: #d4d4d4;--canvas-node-border: rgba(255, 255, 255, .15)}[data-theme=light]{--bg: #f5f5f4;--bg-surface: #fafaf9;--bg-hover: #f0efee;--bg-active: #e7e5e4;--bg-elevated: #f0efee;--bg-inset: #e7e5e4;--border: #d6d3d1;--text: #292524;--text-strong: #1c1917;--text-muted: #78716c;--text-dim: #a8a29e;--accent: #c17856;--accent-hover: #b07a5e;--badge-text: #fafaf9;--glass-bg: rgba(250, 250, 249, .85);--glass-border: rgba(0, 0, 0, .08);--chip-bg: rgba(214, 211, 209, .5);--chip-bg-active: rgba(214, 211, 209, .8);--chip-bg-hover: rgba(200, 197, 195, .8);--chip-border-active: rgba(0, 0, 0, .08);--shadow: rgba(0, 0, 0, .1);--shadow-strong: rgba(0, 0, 0, .15);--canvas-edge: rgba(0, 0, 0, .1);--canvas-edge-highlight: rgba(193, 120, 86, .6);--canvas-edge-dim: rgba(0, 0, 0, .03);--canvas-edge-label: rgba(0, 0, 0, .25);--canvas-edge-label-highlight: rgba(193, 120, 86, .8);--canvas-edge-label-dim: rgba(0, 0, 0, .06);--canvas-arrow: rgba(0, 0, 0, .15);--canvas-arrow-highlight: rgba(193, 120, 86, .6);--canvas-node-label: #57534e;--canvas-node-label-dim: rgba(87, 83, 78, .2);--canvas-type-badge: rgba(87, 83, 78, .5);--canvas-type-badge-dim: rgba(87, 83, 78, .15);--canvas-selection-border: #292524;--canvas-node-border: rgba(0, 0, 0, .1)}body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);overflow:hidden}#app{display:flex;height:100vh;width:100vw}#sidebar{width:280px;min-width:280px;background:var(--bg-surface);border-right:1px solid var(--border);display:flex;flex-direction:column;padding:16px;overflow-y:auto}#sidebar h2{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:14px}#sidebar input{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px;outline:none;margin-bottom:12px}#sidebar input:focus{border-color:var(--accent)}#sidebar input::placeholder{color:var(--text-dim)}#ontology-list{list-style:none;display:flex;flex-direction:column;gap:2px}.ontology-item{padding:10px 12px;border-radius:6px;cursor:pointer;transition:background .15s}.ontology-item:hover{background:var(--bg-hover)}.ontology-item.active{background:var(--bg-active)}.ontology-item .name{display:block;font-size:13px;font-weight:500;color:var(--text)}.ontology-item .stats{display:block;font-size:11px;color:var(--text-dim);margin-top:2px}.sidebar-edit-btn{position:absolute;right:8px;top:10px;background:none;border:none;color:var(--text-dim);font-size:11px;cursor:pointer;opacity:0;transition:opacity .1s}.ontology-item{position:relative}.ontology-item:hover .sidebar-edit-btn{opacity:.7}.sidebar-edit-btn:hover{opacity:1!important;color:var(--text)}.sidebar-rename-input{background:transparent;border:none;border-bottom:1px solid var(--accent);color:var(--text);font:inherit;font-size:13px;font-weight:500;outline:none;width:100%;padding:0}.sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);text-align:center}.sidebar-footer a{display:block;font-size:12px;font-weight:500;color:var(--accent);text-decoration:none;margin-bottom:4px}.sidebar-footer a:hover{color:var(--accent-hover)}.sidebar-footer span{display:block;font-size:10px;color:var(--text-dim)}.canvas-top-bar{position:absolute;top:16px;left:16px;right:16px;z-index:30;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none}.canvas-top-left,.canvas-top-center,.canvas-top-right{pointer-events:auto;display:flex;align-items:center;gap:4px}.canvas-top-left{flex-shrink:0}.canvas-top-center{flex:1;justify-content:center}.focus-indicator{display:flex;align-items:center;gap:2px;background:var(--bg-surface);border:1px solid rgba(212,162,127,.4);border-radius:8px;padding:4px 6px 4px 10px;box-shadow:0 2px 8px var(--shadow)}.focus-indicator-label{font-size:11px;color:var(--accent);font-weight:500;white-space:nowrap;margin-right:4px}.focus-indicator-hops{font-size:11px;color:var(--text-muted);font-family:monospace;min-width:12px;text-align:center}.focus-indicator-btn{background:none;border:none;color:var(--text-muted);font-size:14px;cursor:pointer;padding:2px 4px;line-height:1;border-radius:4px;transition:color .15s,background .15s}.focus-indicator-btn:hover:not(:disabled){color:var(--text);background:var(--bg-hover)}.focus-indicator-btn:disabled{color:var(--text-dim);cursor:default;opacity:.3}.focus-indicator-exit{font-size:16px;margin-left:2px}.focus-indicator-exit:hover{color:#ef4444!important}.info-focus-btn{font-size:14px}.theme-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.theme-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.zoom-controls{display:flex;gap:4px}.zoom-btn{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.zoom-btn:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}#canvas-container{flex:1;position:relative;overflow:hidden;touch-action:none}#graph-canvas{position:absolute;top:0;left:0;touch-action:none;width:100%;height:100%;cursor:grab}#graph-canvas:active{cursor:grabbing}.search-overlay{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;max-height:calc(100vh - 48px);pointer-events:none}.search-overlay>*{pointer-events:auto}.search-overlay.hidden{display:none}.search-input-wrap{position:relative;display:flex;align-items:center;gap:6px;width:380px;max-width:calc(100vw - 340px)}.search-input{flex:1;min-width:0;padding:10px 36px 10px 16px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text);font-size:14px;outline:none;transition:border-color .15s,box-shadow .15s}.search-input:focus{border-color:#d4a27f66;box-shadow:0 0 0 3px #d4a27f1a}.search-input::placeholder{color:var(--text-dim)}.search-kbd{position:absolute;right:10px;top:50%;transform:translateY(-50%);padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg-surface);color:var(--text-dim);font-size:11px;font-family:monospace;pointer-events:none}.search-kbd.hidden{display:none}.search-results{list-style:none;width:380px;max-width:calc(100vw - 340px);background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:10px;overflow:hidden;box-shadow:0 8px 32px var(--shadow-strong)}.search-results.hidden{display:none}.search-result-item{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;transition:background .1s}.search-result-item:hover,.search-result-active{background:var(--bg-hover)}.search-result-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.search-result-label{font-size:13px;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-result-type{font-size:11px;color:var(--text-dim);flex-shrink:0}.chip-toggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text-dim);cursor:pointer;transition:border-color .15s,color .15s;pointer-events:auto}.chip-toggle:hover{border-color:#d4a27f4d;color:var(--text)}.chip-toggle.active{border-color:#d4a27f66;color:var(--accent)}.type-chips{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;max-width:500px;max-height:200px;overflow-y:auto;padding:4px;border-radius:10px}.type-chips.hidden{display:none}.type-chip{display:flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid transparent;border-radius:12px;background:var(--chip-bg);color:var(--text-dim);font-size:11px;cursor:pointer;transition:all .15s;white-space:nowrap}.type-chip.active{background:var(--chip-bg-active);color:var(--text-muted);border-color:var(--chip-border-active)}.type-chip:hover{background:var(--chip-bg-hover)}.type-chip-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.type-chip:not(.active) .type-chip-dot{opacity:.3}.info-panel{position:absolute;top:56px;right:16px;width:360px;max-height:calc(100vh - 72px);background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;overflow-y:auto;padding:20px;z-index:10;box-shadow:0 8px 32px var(--shadow);transition:top .25s ease,right .25s ease,bottom .25s ease,left .25s ease,width .25s ease,max-height .25s ease,border-radius .25s ease}.info-panel.hidden{display:none}.info-panel.info-panel-maximized{top:0;right:0;bottom:0;left:0;width:auto;max-height:none;border-radius:0;z-index:40}.info-panel-toolbar{position:absolute;top:12px;right:14px;display:flex;align-items:center;gap:2px;z-index:1}.info-toolbar-btn{background:none;border:none;color:var(--text-muted);font-size:16px;cursor:pointer;padding:4px 6px;line-height:1;border-radius:4px;transition:color .15s,background .15s}.info-toolbar-btn:hover:not(:disabled){color:var(--text);background:var(--bg-hover)}.info-toolbar-btn:disabled{color:var(--text-dim);cursor:default;opacity:.3}.info-close-btn{font-size:20px}.info-connection-link{cursor:pointer;transition:background .15s}.info-connection-link:hover{background:var(--bg-active)}.info-connection-link .info-target{color:var(--accent);text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color .15s}.info-connection-link:hover .info-target{text-decoration-color:var(--accent)}.info-header{margin-bottom:16px}.info-type-badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;color:var(--badge-text);margin-bottom:8px}.info-label{font-size:18px;font-weight:600;color:var(--text-strong);margin-bottom:4px;word-break:break-word}.info-id{display:block;font-size:11px;color:var(--text-dim);font-family:monospace}.info-section{margin-bottom:16px}.info-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border)}.info-props{display:grid;grid-template-columns:auto 1fr;gap:4px 12px}.info-props dt{font-size:12px;color:var(--text-muted);padding-top:2px}.info-props dd{font-size:12px;color:var(--text);word-break:break-word;display:flex;align-items:center;gap:4px}.info-value{white-space:pre-wrap}.info-array{display:flex;flex-wrap:wrap;gap:4px}.info-tag{display:inline-block;padding:2px 8px;background:var(--bg-hover);border-radius:4px;font-size:11px;color:var(--text-muted)}.info-json{font-size:11px;font-family:monospace;color:var(--text-muted);background:var(--bg-inset);padding:6px 8px;border-radius:4px;overflow-x:auto;white-space:pre}.info-connections{list-style:none;display:flex;flex-direction:column;gap:6px}.info-connection{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--bg-elevated);border-radius:6px;font-size:12px;flex-wrap:wrap}.info-connection-active{outline:1.5px solid var(--accent);background:var(--bg-hover)}.info-target-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.info-arrow{color:var(--text-dim);font-size:14px;flex-shrink:0}.info-edge-type{color:var(--text-muted);font-size:11px;font-weight:500}.info-target{color:var(--text);font-weight:500}.info-edge-props{width:100%;padding-top:4px;padding-left:20px}.info-edge-prop{display:block;font-size:11px;color:var(--text-dim)}.info-editable{cursor:default;position:relative}.info-inline-edit{background:none;border:none;color:var(--badge-text);opacity:0;font-size:10px;cursor:pointer;margin-left:4px;transition:opacity .15s}.info-editable:hover .info-inline-edit{opacity:.8}.info-inline-edit:hover{opacity:1!important}.info-edit-inline-input{background:transparent;border:none;border-bottom:1px solid var(--accent);color:var(--badge-text);font:inherit;font-size:inherit;outline:none;width:100%;padding:0}.info-edit-input{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:3px 6px;font-size:12px;font-family:inherit;color:var(--text);flex:1;min-width:0;resize:vertical;overflow:hidden;line-height:1.4;max-height:300px}.info-edit-input:focus{outline:none;border-color:var(--accent)}.info-delete-prop{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;padding:0 2px;flex-shrink:0;opacity:0;transition:opacity .1s,color .1s}.info-props dd:hover .info-delete-prop{opacity:1}.info-delete-prop:hover{color:#ef4444}.info-add-btn{background:none;border:1px dashed var(--border);border-radius:4px;padding:6px 10px;font-size:12px;color:var(--text-dim);cursor:pointer;width:100%;margin-top:8px;transition:border-color .15s,color .15s}.info-add-btn:hover{border-color:var(--accent);color:var(--text)}.info-add-row{display:flex;gap:4px;margin-top:6px}.info-add-save{background:var(--accent);border:none;border-radius:4px;padding:3px 10px;font-size:12px;color:var(--badge-text);cursor:pointer;flex-shrink:0}.info-add-save:hover{background:var(--accent-hover)}.info-delete-edge{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;margin-left:auto;padding:0 2px;opacity:0;transition:opacity .1s,color .1s}.info-connection:hover .info-delete-edge{opacity:1}.info-delete-edge:hover{color:#ef4444}.info-danger{margin-top:8px;padding-top:12px;border-top:1px solid var(--border)}.info-delete-node{background:none;border:1px solid rgba(239,68,68,.3);border-radius:6px;padding:6px 12px;font-size:12px;color:#ef4444;cursor:pointer;width:100%;transition:background .15s}.info-delete-node:hover{background:#ef44441a}.tools-pane-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.tools-pane-toggle.hidden{display:none}.tools-pane-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.tools-pane-toggle.active{color:var(--accent);border-color:#d4a27f66}.tools-pane-content{position:absolute;top:56px;left:16px;bottom:16px;z-index:20;width:200px;overflow-y:auto;overflow-x:hidden;background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:12px;box-shadow:0 8px 32px var(--shadow)}.tools-pane-content.hidden{display:none}.tools-pane-section{margin-bottom:12px}.tools-pane-section:last-child{margin-bottom:0}.tools-pane-heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);margin-bottom:6px}.tools-pane-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:12px}.tools-pane-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.tools-pane-name{flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tools-pane-count{color:var(--text-dim);font-size:11px;flex-shrink:0}.tools-pane-summary{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-muted);padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--border)}.tools-pane-sep{color:var(--text-dim)}.tools-pane-clickable{cursor:pointer;border-radius:4px;padding:3px 4px;margin:0 -4px;transition:background .1s}.tools-pane-clickable:hover{background:var(--bg-hover)}.tools-pane-clickable.active{background:var(--bg-hover);outline:1px solid var(--border)}.tools-pane-badge{font-size:9px;color:var(--accent);flex-shrink:0;opacity:.8}.tools-pane-issue .tools-pane-name{color:var(--text-muted)}.tools-pane-more{font-size:10px;color:var(--text-dim);padding:4px 0 0}.tools-pane-edit{background:none;border:none;color:var(--text-dim);font-size:11px;cursor:pointer;padding:0 2px;opacity:0;transition:opacity .1s,color .1s;flex-shrink:0}.tools-pane-row:hover .tools-pane-edit{opacity:1}.tools-pane-edit:hover{color:var(--accent)}.tools-pane-focus-toggle{opacity:.4;font-size:11px}.tools-pane-focus-active{opacity:1!important;color:var(--accent)!important}.tools-pane-focus-clear{margin-top:4px;border-top:1px solid var(--border);padding-top:6px}.tools-pane-editing{background:none!important}.tools-pane-inline-input{width:100%;background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:12px;padding:2px 6px;outline:none}.tools-pane-slider-row{display:flex;align-items:center;gap:6px;padding:4px 0}.tools-pane-slider-label{font-size:11px;color:var(--text-muted);white-space:nowrap;min-width:56px}.tools-pane-slider{flex:1;min-width:0;height:4px;accent-color:var(--accent);cursor:pointer}.tools-pane-slider-value{font-size:10px;color:var(--text-dim);min-width:28px;text-align:right;font-family:monospace}.tools-pane-checkbox{width:14px;height:14px;accent-color:var(--accent);cursor:pointer;flex-shrink:0}.tools-pane-export-row{display:flex;gap:4px;margin-top:6px}.tools-pane-export-btn{flex:1;padding:4px 8px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s}.tools-pane-export-btn:hover{color:var(--text);border-color:var(--text-muted)}.tools-pane-empty{font-size:11px;color:var(--text-dim);text-align:center;padding:8px 0}.tools-pane-tabs{display:flex;gap:2px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}.tools-pane-tab{flex:1;padding:4px 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;background:none;border:1px solid transparent;border-radius:5px;color:var(--text-dim);cursor:pointer;transition:color .15s,background .15s,border-color .15s}.tools-pane-tab:hover{color:var(--text-muted);background:var(--bg-hover)}.tools-pane-tab-active{color:var(--text);background:var(--bg-hover);border-color:var(--border)}.tools-pane-search{width:100%;padding:4px 8px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);outline:none;margin-bottom:8px;box-sizing:border-box}.tools-pane-search:focus{border-color:var(--accent)}.tools-pane-search::placeholder{color:var(--text-dim)}.tools-pane-empty-msg{font-size:11px;color:var(--text-dim);text-align:center;padding:16px 0}.empty-state{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;z-index:5;pointer-events:none}.empty-state.hidden{display:none}.empty-state-content{text-align:center;max-width:420px;padding:40px 24px}.empty-state-icon{color:var(--text-dim);margin-bottom:16px}.empty-state-title{font-size:18px;font-weight:600;color:var(--text);margin-bottom:8px}.empty-state-desc{font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:20px}.empty-state-setup{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:16px}.empty-state-label{font-size:11px;color:var(--text-dim);margin-bottom:8px}.empty-state-code{display:block;font-size:12px;color:var(--accent);font-family:monospace;word-break:break-all}.empty-state-hint{font-size:11px;color:var(--text-dim)}.empty-state-hint kbd{padding:1px 5px;border:1px solid var(--border);border-radius:3px;background:var(--bg-surface);font-family:monospace;font-size:11px}.shortcuts-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.shortcuts-overlay.hidden{display:none}.shortcuts-modal{background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;padding:24px;min-width:300px;max-width:400px;box-shadow:0 16px 48px var(--shadow);position:relative}.shortcuts-close{position:absolute;top:12px;right:14px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;padding:0 4px;line-height:1}.shortcuts-close:hover{color:var(--text)}.shortcuts-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:16px}.shortcuts-list{display:flex;flex-direction:column;gap:8px}.shortcuts-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.shortcuts-keys{display:flex;align-items:center;gap:4px}.shortcuts-keys kbd{padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text);font-size:11px;font-family:monospace}.shortcuts-or{font-size:10px;color:var(--text-dim)}.shortcuts-desc{font-size:12px;color:var(--text-muted)}@media(max-width:768px){#app{flex-direction:column}#sidebar{width:100%;min-width:0;max-height:35vh;border-right:none;border-bottom:1px solid var(--border)}.info-panel{top:auto;bottom:72px;right:8px;left:8px;width:auto;max-height:calc(100% - 200px);overflow-y:auto}.info-panel.info-panel-maximized{bottom:0;left:0;right:0}.canvas-top-bar{top:8px;left:8px;right:8px}.tools-pane-content{top:48px;left:8px;bottom:80px;width:160px;max-width:calc(100vw - 24px)}.tools-pane-edit{opacity:.6}}
@@ -0,0 +1,21 @@
1
+ (function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))d(t);new MutationObserver(t=>{for(const E of t)if(E.type==="childList")for(const n of E.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&d(n)}).observe(document,{childList:!0,subtree:!0});function m(t){const E={};return t.integrity&&(E.integrity=t.integrity),t.referrerPolicy&&(E.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?E.credentials="include":t.crossOrigin==="anonymous"?E.credentials="omit":E.credentials="same-origin",E}function d(t){if(t.ep)return;t.ep=!0;const E=m(t);fetch(t.href,E)}})();async function Ae(){const l=await fetch("/api/ontologies");return l.ok?l.json():[]}async function Be(l){const s=await fetch(`/api/ontologies/${encodeURIComponent(l)}`);if(!s.ok)throw new Error(`Failed to load ontology: ${l}`);return s.json()}async function Oe(l,s){if(!(await fetch(`/api/ontologies/${encodeURIComponent(l)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).ok)throw new Error(`Failed to save ontology: ${l}`)}async function nt(l,s){if(!(await fetch(`/api/ontologies/${encodeURIComponent(l)}/rename`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:s})})).ok)throw new Error(`Failed to rename ontology: ${l}`)}function ot(l,s){const m=typeof s=="function"?{onSelect:s}:s,d=document.createElement("h2");d.textContent="Backpack Viewer";const t=document.createElement("input");t.type="text",t.placeholder="Filter...",t.id="filter";const E=document.createElement("ul");E.id="ontology-list";const n=document.createElement("div");n.className="sidebar-footer",n.innerHTML='<a href="mailto:support@backpackontology.com">support@backpackontology.com</a><span>Feedback & support</span>',l.appendChild(d),l.appendChild(t),l.appendChild(E),l.appendChild(n);let c=[],k="";return t.addEventListener("input",()=>{const p=t.value.toLowerCase();for(const C of c){const A=C.dataset.name??"";C.style.display=A.includes(p)?"":"none"}}),{setSummaries(p){E.innerHTML="",c=p.map(C=>{const A=document.createElement("li");A.className="ontology-item",A.dataset.name=C.name;const S=document.createElement("span");S.className="name",S.textContent=C.name;const F=document.createElement("span");if(F.className="stats",F.textContent=`${C.nodeCount} nodes, ${C.edgeCount} edges`,A.appendChild(S),A.appendChild(F),m.onRename){const U=document.createElement("button");U.className="sidebar-edit-btn",U.textContent="✎",U.title="Rename";const M=m.onRename;U.addEventListener("click",D=>{D.stopPropagation();const R=document.createElement("input");R.type="text",R.className="sidebar-rename-input",R.value=C.name,S.textContent="",S.appendChild(R),U.style.display="none",R.focus(),R.select();const V=()=>{const X=R.value.trim();X&&X!==C.name?M(C.name,X):(S.textContent=C.name,U.style.display="")};R.addEventListener("blur",V),R.addEventListener("keydown",X=>{X.key==="Enter"&&R.blur(),X.key==="Escape"&&(R.value=C.name,R.blur())})}),A.appendChild(U)}return A.addEventListener("click",()=>m.onSelect(C.name)),E.appendChild(A),A}),k&&this.setActive(k)},setActive(p){k=p;for(const C of c)C.classList.toggle("active",C.dataset.name===p)}}}const Ke={clusterStrength:.08,spacing:1.5},st=6e3,at=12e3,ct=.004,Je=140,Ze=350,He=.9,$e=.01,ze=30,Fe=50;let we={...Ke};function ke(l){l.clusterStrength!==void 0&&(we.clusterStrength=l.clusterStrength),l.spacing!==void 0&&(we.spacing=l.spacing)}function De(){return{...we}}function it(l){if(l<=30)return{...Ke};const s=Math.log2(l/30);return{clusterStrength:Math.min(.5,.08+.06*s),spacing:Math.min(15,1.5+1.2*s)}}function lt(l,s){for(const m of Object.values(l))if(typeof m=="string")return m;return s}function dt(l,s,m){const d=new Set(s);let t=new Set(s);for(let E=0;E<m;E++){const n=new Set;for(const c of l.edges)t.has(c.sourceId)&&!d.has(c.targetId)&&n.add(c.targetId),t.has(c.targetId)&&!d.has(c.sourceId)&&n.add(c.sourceId);for(const c of n)d.add(c);if(t=n,n.size===0)break}return{nodes:l.nodes.filter(E=>d.has(E.id)),edges:l.edges.filter(E=>d.has(E.sourceId)&&d.has(E.targetId)),metadata:l.metadata}}function Ye(l){const s=new Map,m=[...new Set(l.nodes.map(k=>k.type))],d=Math.sqrt(m.length)*Ze*.6*Math.max(1,we.spacing),t=new Map,E=new Map;for(const k of l.nodes)E.set(k.type,(E.get(k.type)??0)+1);const n=l.nodes.map(k=>{const p=m.indexOf(k.type),C=2*Math.PI*p/Math.max(m.length,1),A=Math.cos(C)*d,S=Math.sin(C)*d,F=t.get(k.type)??0;t.set(k.type,F+1);const U=E.get(k.type)??1,M=2*Math.PI*F/U,D=Je*.6,R={id:k.id,x:A+Math.cos(M)*D,y:S+Math.sin(M)*D,vx:0,vy:0,label:lt(k.properties,k.id),type:k.type};return s.set(k.id,R),R}),c=l.edges.map(k=>({sourceId:k.sourceId,targetId:k.targetId,type:k.type}));return{nodes:n,edges:c,nodeMap:s}}function rt(l,s){const{nodes:m,edges:d,nodeMap:t}=l;for(let n=0;n<m.length;n++)for(let c=n+1;c<m.length;c++){const k=m[n],p=m[c];let C=p.x-k.x,A=p.y-k.y,S=Math.sqrt(C*C+A*A);S<ze&&(S=ze);const U=(k.type===p.type?st:at*we.spacing)*s/(S*S),M=C/S*U,D=A/S*U;k.vx-=M,k.vy-=D,p.vx+=M,p.vy+=D}for(const n of d){const c=t.get(n.sourceId),k=t.get(n.targetId);if(!c||!k)continue;const p=k.x-c.x,C=k.y-c.y,A=Math.sqrt(p*p+C*C);if(A===0)continue;const S=c.type===k.type?Je*we.spacing:Ze*we.spacing,F=ct*(A-S)*s,U=p/A*F,M=C/A*F;c.vx+=U,c.vy+=M,k.vx-=U,k.vy-=M}for(const n of m)n.vx-=n.x*$e*s,n.vy-=n.y*$e*s;const E=new Map;for(const n of m){const c=E.get(n.type)??{x:0,y:0,count:0};c.x+=n.x,c.y+=n.y,c.count++,E.set(n.type,c)}for(const n of E.values())n.x/=n.count,n.y/=n.count;for(const n of m){const c=E.get(n.type);n.vx+=(c.x-n.x)*we.clusterStrength*s,n.vy+=(c.y-n.y)*we.clusterStrength*s}for(const n of m){n.vx*=He,n.vy*=He;const c=Math.sqrt(n.vx*n.vx+n.vy*n.vy);c>Fe&&(n.vx=n.vx/c*Fe,n.vy=n.vy/c*Fe),n.x+=n.vx,n.y+=n.vy}return s*.995}const Ue=["#d4a27f","#c17856","#b07a5e","#d4956b","#a67c5a","#cc9e7c","#c4866a","#cb8e6c","#b8956e","#a88a70","#d9b08c","#c4a882","#e8b898","#b5927a","#a8886e","#d1a990"],Xe=new Map;function de(l){const s=Xe.get(l);if(s)return s;let m=0;for(let t=0;t<l.length;t++)m=(m<<5)-m+l.charCodeAt(t)|0;const d=Ue[Math.abs(m)%Ue.length];return Xe.set(l,d),d}function ie(l){return getComputedStyle(document.documentElement).getPropertyValue(l).trim()}const ve=20,pt=.001,ut=.4,mt=.25,ft=.35,_e=.2,qe=.15;function Pe(l,s,m,d,t,E=100){const n=(l-m.x)*m.scale,c=(s-m.y)*m.scale;return n>=-E&&n<=d+E&&c>=-E&&c<=t+E}function ht(l,s,m){const d=l.querySelector("canvas"),t=d.getContext("2d"),E=window.devicePixelRatio||1;let n={x:0,y:0,scale:1},c=null,k=1,p=0,C=new Set,A=null,S=!0,F=!0,U=!0,M=!0,D=null,R=null,V=1,X=null,Z=null,T=null,$=null;const _=300;function K(){d.width=d.clientWidth*E,d.height=d.clientHeight*E,z()}const ee=new ResizeObserver(K);ee.observe(l),K();function W(r,h){return[r/n.scale+n.x,h/n.scale+n.y]}function le(r,h){if(!c)return null;const[N,I]=W(r,h);for(let P=c.nodes.length-1;P>=0;P--){const q=c.nodes[P],H=N-q.x,G=I-q.y;if(H*H+G*G<=ve*ve)return q}return null}function z(){if(!c){t.clearRect(0,0,d.width,d.height);return}const r=ie("--canvas-edge"),h=ie("--canvas-edge-highlight"),N=ie("--canvas-edge-dim"),I=ie("--canvas-edge-label"),P=ie("--canvas-edge-label-highlight"),q=ie("--canvas-edge-label-dim"),H=ie("--canvas-arrow"),G=ie("--canvas-arrow-highlight"),ae=ie("--canvas-node-label"),J=ie("--canvas-node-label-dim"),Ce=ie("--canvas-type-badge"),re=ie("--canvas-type-badge-dim"),Ne=ie("--canvas-selection-border"),Se=ie("--canvas-node-border");if(t.save(),t.setTransform(E,0,0,E,0,0),t.clearRect(0,0,d.clientWidth,d.clientHeight),t.save(),t.translate(-n.x*n.scale,-n.y*n.scale),t.scale(n.scale,n.scale),U&&n.scale>=_e){const Y=new Map;for(const te of c.nodes){if(A!==null&&!A.has(te.id))continue;const oe=Y.get(te.type)??[];oe.push(te),Y.set(te.type,oe)}for(const[te,oe]of Y){if(oe.length<2)continue;const Ee=de(te),xe=ve*2.5;let ge=1/0,ye=1/0,ce=-1/0,Q=-1/0;for(const Ie of oe)Ie.x<ge&&(ge=Ie.x),Ie.y<ye&&(ye=Ie.y),Ie.x>ce&&(ce=Ie.x),Ie.y>Q&&(Q=Ie.y);t.beginPath();const be=(ce-ge)/2+xe,Le=(Q-ye)/2+xe,et=(ge+ce)/2,tt=(ye+Q)/2;t.ellipse(et,tt,be,Le,0,0,Math.PI*2),t.fillStyle=Ee,t.globalAlpha=.05,t.fill(),t.strokeStyle=Ee,t.globalAlpha=.12,t.lineWidth=1,t.setLineDash([4,4]),t.stroke(),t.setLineDash([]),t.globalAlpha=1}}if(S)for(const Y of c.edges){const te=c.nodeMap.get(Y.sourceId),oe=c.nodeMap.get(Y.targetId);if(!te||!oe||!Pe(te.x,te.y,n,d.clientWidth,d.clientHeight,200)&&!Pe(oe.x,oe.y,n,d.clientWidth,d.clientHeight,200))continue;const Ee=A===null||A.has(Y.sourceId),xe=A===null||A.has(Y.targetId),ge=Ee&&xe;if(A!==null&&!Ee&&!xe)continue;const ce=C.size>0&&(C.has(Y.sourceId)||C.has(Y.targetId))||A!==null&&ge,Q=A!==null&&!ge;if(Y.sourceId===Y.targetId){f(te,Y.type,ce,r,h,I,P);continue}if(t.beginPath(),t.moveTo(te.x,te.y),t.lineTo(oe.x,oe.y),t.strokeStyle=ce?h:Q?N:r,t.lineWidth=n.scale<qe?1:ce?2.5:1.5,t.stroke(),n.scale>=qe&&he(te.x,te.y,oe.x,oe.y,ce,H,G),F&&n.scale>=ft){const be=(te.x+oe.x)/2,Le=(te.y+oe.y)/2;t.fillStyle=ce?P:Q?q:I,t.font="9px system-ui, sans-serif",t.textAlign="center",t.textBaseline="bottom",t.fillText(Y.type,be,Le-4)}}for(const Y of c.nodes){if(!Pe(Y.x,Y.y,n,d.clientWidth,d.clientHeight))continue;const te=de(Y.type),oe=C.has(Y.id),Ee=C.size>0&&c.edges.some(ce=>C.has(ce.sourceId)&&ce.targetId===Y.id||C.has(ce.targetId)&&ce.sourceId===Y.id),xe=A!==null&&!A.has(Y.id),ge=xe||C.size>0&&!oe&&!Ee,ye=n.scale<_e?ve*.5:ve;if(oe&&(t.save(),t.shadowColor=te,t.shadowBlur=20,t.beginPath(),t.arc(Y.x,Y.y,ye+3,0,Math.PI*2),t.fillStyle=te,t.globalAlpha=.3,t.fill(),t.restore()),t.beginPath(),t.arc(Y.x,Y.y,ye,0,Math.PI*2),t.fillStyle=te,t.globalAlpha=xe?.1:ge?.3:1,t.fill(),t.strokeStyle=oe?Ne:Se,t.lineWidth=oe?3:1.5,t.stroke(),n.scale>=mt){const ce=Y.label.length>24?Y.label.slice(0,22)+"...":Y.label;t.fillStyle=ge?J:ae,t.font="11px system-ui, sans-serif",t.textAlign="center",t.textBaseline="top",t.fillText(ce,Y.x,Y.y+ye+4)}n.scale>=ut&&(t.fillStyle=ge?re:Ce,t.font="9px system-ui, sans-serif",t.textBaseline="bottom",t.fillText(Y.type,Y.x,Y.y-ye-3)),t.globalAlpha=1}t.restore(),t.restore(),M&&c.nodes.length>1&&fe()}function fe(){if(!c)return;const r=140,h=100,N=8,I=d.clientWidth-r-16,P=d.clientHeight-h-16;let q=1/0,H=1/0,G=-1/0,ae=-1/0;for(const Q of c.nodes)Q.x<q&&(q=Q.x),Q.y<H&&(H=Q.y),Q.x>G&&(G=Q.x),Q.y>ae&&(ae=Q.y);const J=G-q||1,Ce=ae-H||1,re=Math.min((r-N*2)/J,(h-N*2)/Ce),Ne=I+N+(r-N*2-J*re)/2,Se=P+N+(h-N*2-Ce*re)/2;t.save(),t.setTransform(E,0,0,E,0,0),t.fillStyle=ie("--bg-surface")||"#1a1a1a",t.globalAlpha=.85,t.beginPath(),t.roundRect(I,P,r,h,8),t.fill(),t.strokeStyle=ie("--border")||"#2a2a2a",t.globalAlpha=1,t.lineWidth=1,t.stroke(),t.globalAlpha=.15,t.strokeStyle=ie("--canvas-edge")||"#555",t.lineWidth=.5;for(const Q of c.edges){const be=c.nodeMap.get(Q.sourceId),Le=c.nodeMap.get(Q.targetId);!be||!Le||Q.sourceId===Q.targetId||(t.beginPath(),t.moveTo(Ne+(be.x-q)*re,Se+(be.y-H)*re),t.lineTo(Ne+(Le.x-q)*re,Se+(Le.y-H)*re),t.stroke())}t.globalAlpha=.8;for(const Q of c.nodes){const be=Ne+(Q.x-q)*re,Le=Se+(Q.y-H)*re;t.beginPath(),t.arc(be,Le,2,0,Math.PI*2),t.fillStyle=de(Q.type),t.fill()}const Y=n.x,te=n.y,oe=n.x+d.clientWidth/n.scale,Ee=n.y+d.clientHeight/n.scale,xe=Ne+(Y-q)*re,ge=Se+(te-H)*re,ye=(oe-Y)*re,ce=(Ee-te)*re;t.globalAlpha=.3,t.strokeStyle=ie("--accent")||"#d4a27f",t.lineWidth=1.5,t.strokeRect(Math.max(I,Math.min(xe,I+r)),Math.max(P,Math.min(ge,P+h)),Math.min(ye,r),Math.min(ce,h)),t.globalAlpha=1,t.restore()}function he(r,h,N,I,P,q,H){const G=Math.atan2(I-h,N-r),ae=N-Math.cos(G)*ve,J=I-Math.sin(G)*ve,Ce=8;t.beginPath(),t.moveTo(ae,J),t.lineTo(ae-Ce*Math.cos(G-.4),J-Ce*Math.sin(G-.4)),t.lineTo(ae-Ce*Math.cos(G+.4),J-Ce*Math.sin(G+.4)),t.closePath(),t.fillStyle=P?H:q,t.fill()}function f(r,h,N,I,P,q,H){const G=r.x+ve+15,ae=r.y-ve-15;t.beginPath(),t.arc(G,ae,15,0,Math.PI*2),t.strokeStyle=N?P:I,t.lineWidth=N?2.5:1.5,t.stroke(),F&&(t.fillStyle=N?H:q,t.font="9px system-ui, sans-serif",t.textAlign="center",t.fillText(h,G,ae-18))}function v(){if(!T||!$)return;const r=performance.now()-$.time,h=Math.min(r/_,1),N=1-Math.pow(1-h,3);n.x=$.x+(T.x-$.x)*N,n.y=$.y+(T.y-$.y)*N,z(),h<1?requestAnimationFrame(v):(T=null,$=null)}function x(){!c||k<pt||(k=rt(c,k),z(),p=requestAnimationFrame(x))}let y=!1,w=!1,b=0,u=0;d.addEventListener("mousedown",r=>{y=!0,w=!1,b=r.clientX,u=r.clientY}),d.addEventListener("mousemove",r=>{if(!y)return;const h=r.clientX-b,N=r.clientY-u;(Math.abs(h)>2||Math.abs(N)>2)&&(w=!0),n.x-=h/n.scale,n.y-=N/n.scale,b=r.clientX,u=r.clientY,z()}),d.addEventListener("mouseup",r=>{if(y=!1,w)return;const h=d.getBoundingClientRect(),N=r.clientX-h.left,I=r.clientY-h.top,P=le(N,I),q=r.ctrlKey||r.metaKey;if(P){q?C.has(P.id)?C.delete(P.id):C.add(P.id):C.size===1&&C.has(P.id)?C.clear():(C.clear(),C.add(P.id));const H=[...C];s==null||s(H.length>0?H:null)}else C.clear(),s==null||s(null);z()}),d.addEventListener("mouseleave",()=>{y=!1}),d.addEventListener("wheel",r=>{r.preventDefault();const h=d.getBoundingClientRect(),N=r.clientX-h.left,I=r.clientY-h.top,[P,q]=W(N,I),H=r.ctrlKey?1-r.deltaY*.01:r.deltaY>0?.9:1.1;n.scale=Math.max(.05,Math.min(10,n.scale*H)),n.x=P-N/n.scale,n.y=q-I/n.scale,z()},{passive:!1});let e=[],o=0,i=1,a=0,g=0,O=!1;d.addEventListener("touchstart",r=>{r.preventDefault(),e=Array.from(r.touches),e.length===2?(o=B(e[0],e[1]),i=n.scale):e.length===1&&(b=e[0].clientX,u=e[0].clientY,a=e[0].clientX,g=e[0].clientY,O=!1)},{passive:!1}),d.addEventListener("touchmove",r=>{r.preventDefault();const h=Array.from(r.touches);if(h.length===2&&e.length===2){const I=B(h[0],h[1])/o;n.scale=Math.max(.05,Math.min(10,i*I)),z()}else if(h.length===1){const N=h[0].clientX-b,I=h[0].clientY-u;(Math.abs(h[0].clientX-a)>10||Math.abs(h[0].clientY-g)>10)&&(O=!0),n.x-=N/n.scale,n.y-=I/n.scale,b=h[0].clientX,u=h[0].clientY,z()}e=h},{passive:!1}),d.addEventListener("touchend",r=>{if(r.preventDefault(),O||r.changedTouches.length!==1)return;const h=r.changedTouches[0],N=d.getBoundingClientRect(),I=h.clientX-N.left,P=h.clientY-N.top,q=le(I,P);if(q){C.size===1&&C.has(q.id)?C.clear():(C.clear(),C.add(q.id));const H=[...C];s==null||s(H.length>0?H:null)}else C.clear(),s==null||s(null);z()},{passive:!1}),d.addEventListener("gesturestart",r=>r.preventDefault()),d.addEventListener("gesturechange",r=>r.preventDefault());function B(r,h){const N=r.clientX-h.clientX,I=r.clientY-h.clientY;return Math.sqrt(N*N+I*I)}const ne=document.createElement("div");ne.className="zoom-controls";const j=document.createElement("button");j.className="zoom-btn",j.textContent="+",j.title="Zoom in",j.addEventListener("click",()=>{const r=d.clientWidth/2,h=d.clientHeight/2,[N,I]=W(r,h);n.scale=Math.min(10,n.scale*1.3),n.x=N-r/n.scale,n.y=I-h/n.scale,z()});const ue=document.createElement("button");ue.className="zoom-btn",ue.textContent="−",ue.title="Zoom out",ue.addEventListener("click",()=>{const r=d.clientWidth/2,h=d.clientHeight/2,[N,I]=W(r,h);n.scale=Math.max(.05,n.scale/1.3),n.x=N-r/n.scale,n.y=I-h/n.scale,z()});const me=document.createElement("button");return me.className="zoom-btn",me.textContent="○",me.title="Reset zoom",me.addEventListener("click",()=>{if(c){if(n={x:0,y:0,scale:1},c.nodes.length>0){let r=1/0,h=1/0,N=-1/0,I=-1/0;for(const H of c.nodes)H.x<r&&(r=H.x),H.y<h&&(h=H.y),H.x>N&&(N=H.x),H.y>I&&(I=H.y);const P=(r+N)/2,q=(h+I)/2;n.x=P-d.clientWidth/2,n.y=q-d.clientHeight/2}z()}}),ne.appendChild(j),ne.appendChild(me),ne.appendChild(ue),l.appendChild(ne),{loadGraph(r){if(cancelAnimationFrame(p),D=r,R=null,X=null,Z=null,c=Ye(r),k=1,C=new Set,A=null,n={x:0,y:0,scale:1},c.nodes.length>0){let h=1/0,N=1/0,I=-1/0,P=-1/0;for(const J of c.nodes)J.x<h&&(h=J.x),J.y<N&&(N=J.y),J.x>I&&(I=J.x),J.y>P&&(P=J.y);const q=(h+I)/2,H=(N+P)/2,G=d.clientWidth,ae=d.clientHeight;n.x=q-G/2,n.y=H-ae/2}x()},setFilteredNodeIds(r){A=r,z()},panToNode(r){this.panToNodes([r])},panToNodes(r){if(!c||r.length===0)return;const h=r.map(P=>c.nodeMap.get(P)).filter(Boolean);if(h.length===0)return;C=new Set(r),s==null||s(r);const N=d.clientWidth,I=d.clientHeight;if(h.length===1)$={x:n.x,y:n.y,time:performance.now()},T={x:h[0].x-N/(2*n.scale),y:h[0].y-I/(2*n.scale)};else{let P=1/0,q=1/0,H=-1/0,G=-1/0;for(const Y of h)Y.x<P&&(P=Y.x),Y.y<q&&(q=Y.y),Y.x>H&&(H=Y.x),Y.y>G&&(G=Y.y);const ae=ve*4,J=H-P+ae*2,Ce=G-q+ae*2,re=Math.min(N/J,I/Ce,n.scale);n.scale=re;const Ne=(P+H)/2,Se=(q+G)/2;$={x:n.x,y:n.y,time:performance.now()},T={x:Ne-N/(2*n.scale),y:Se-I/(2*n.scale)}}v()},setEdges(r){S=r,z()},setEdgeLabels(r){F=r,z()},setTypeHulls(r){U=r,z()},setMinimap(r){M=r,z()},centerView(){if(c){if(n={x:0,y:0,scale:1},c.nodes.length>0){let r=1/0,h=1/0,N=-1/0,I=-1/0;for(const P of c.nodes)P.x<r&&(r=P.x),P.y<h&&(h=P.y),P.x>N&&(N=P.x),P.y>I&&(I=P.y);n.x=(r+N)/2-d.clientWidth/2,n.y=(h+I)/2-d.clientHeight/2}z()}},panBy(r,h){n.x+=r/n.scale,n.y+=h/n.scale,z()},zoomBy(r){const h=d.clientWidth/2,N=d.clientHeight/2,[I,P]=W(h,N);n.scale=Math.max(.05,Math.min(10,n.scale*r)),n.x=I-h/n.scale,n.y=P-N/n.scale,z()},reheat(){k=.5,cancelAnimationFrame(p),x()},exportImage(r){if(!c)return"";const h=d.width,N=d.height;if(r==="png"){const H=document.createElement("canvas");H.width=h,H.height=N;const G=H.getContext("2d");return G.fillStyle=ie("--bg")||"#141414",G.fillRect(0,0,h,N),G.drawImage(d,0,0),se(G,h,N),H.toDataURL("image/png")}const I=d.toDataURL("image/png"),P=Math.max(16,Math.round(h/80)),q=`<svg xmlns="http://www.w3.org/2000/svg" width="${h}" height="${N}">
2
+ <image href="${I}" width="${h}" height="${N}"/>
3
+ <text x="${h-20}" y="${N-16}" text-anchor="end" font-family="system-ui, sans-serif" font-size="${P}" fill="#ffffff" opacity="0.4">backpackontology.com</text>
4
+ </svg>`;return"data:image/svg+xml;charset=utf-8,"+encodeURIComponent(q)},enterFocus(r,h){if(!D||!c)return;R||(X=c,Z={...n}),R=r,V=h;const N=dt(D,r,h);if(cancelAnimationFrame(p),c=Ye(N),k=1,C=new Set(r),A=null,n={x:0,y:0,scale:1},c.nodes.length>0){let I=1/0,P=1/0,q=-1/0,H=-1/0;for(const J of c.nodes)J.x<I&&(I=J.x),J.y<P&&(P=J.y),J.x>q&&(q=J.x),J.y>H&&(H=J.y);const G=(I+q)/2,ae=(P+H)/2;n.x=G-d.clientWidth/2,n.y=ae-d.clientHeight/2}x(),m==null||m({seedNodeIds:r,hops:h,totalNodes:N.nodes.length})},exitFocus(){!R||!X||(cancelAnimationFrame(p),c=X,n=Z??{x:0,y:0,scale:1},R=null,X=null,Z=null,C=new Set,A=null,z(),m==null||m(null))},isFocused(){return R!==null},getFocusInfo(){return!R||!c?null:{seedNodeIds:R,hops:V,totalNodes:c.nodes.length}},getNodeIds(){if(!c)return[];if(R){const r=new Set(R),h=c.nodes.filter(I=>r.has(I.id)).map(I=>I.id),N=c.nodes.filter(I=>!r.has(I.id)).map(I=>I.id);return[...h,...N]}return c.nodes.map(r=>r.id)},destroy(){cancelAnimationFrame(p),ee.disconnect()}};function se(r,h,N){const I=Math.max(16,Math.round(h/80));r.save(),r.font=`${I}px system-ui, sans-serif`,r.fillStyle="rgba(255, 255, 255, 0.4)",r.textAlign="right",r.textBaseline="bottom",r.fillText("backpackontology.com",h-20,N-16),r.restore()}}function Me(l){for(const s of Object.values(l.properties))if(typeof s=="string")return s;return l.id}const gt="✎";function yt(l,s,m,d){const t=document.createElement("div");t.id="info-panel",t.className="info-panel hidden",l.appendChild(t);let E=!1,n=[],c=-1,k=!1,p=null,C=[],A=!1,S=[],F=-1;function U(){t.classList.add("hidden"),t.classList.remove("info-panel-maximized"),t.innerHTML="",E=!1,n=[],c=-1}function M(T){!p||!m||(c<n.length-1&&(n=n.slice(0,c+1)),n.push(T),c=n.length-1,k=!0,m(T),k=!1)}function D(){if(c<=0||!p)return;c--,k=!0;const T=n[c];m==null||m(T),X(T,p),k=!1}function R(){if(c>=n.length-1||!p)return;c++,k=!0;const T=n[c];m==null||m(T),X(T,p),k=!1}function V(){const T=document.createElement("div");T.className="info-panel-toolbar";const $=document.createElement("button");$.className="info-toolbar-btn",$.textContent="←",$.title="Back",$.disabled=c<=0,$.addEventListener("click",D),T.appendChild($);const _=document.createElement("button");if(_.className="info-toolbar-btn",_.textContent="→",_.title="Forward",_.disabled=c>=n.length-1,_.addEventListener("click",R),T.appendChild(_),d&&C.length>0){const W=document.createElement("button");W.className="info-toolbar-btn info-focus-btn",W.textContent="◎",W.title="Focus on neighborhood (F)",W.disabled=A,A&&(W.style.opacity="0.3"),W.addEventListener("click",()=>{A||d(C)}),T.appendChild(W)}const K=document.createElement("button");K.className="info-toolbar-btn",K.textContent=E?"⎘":"⛶",K.title=E?"Restore":"Maximize",K.addEventListener("click",()=>{E=!E,t.classList.toggle("info-panel-maximized",E),K.textContent=E?"⎘":"⛶",K.title=E?"Restore":"Maximize"}),T.appendChild(K);const ee=document.createElement("button");return ee.className="info-toolbar-btn info-close-btn",ee.textContent="×",ee.title="Close",ee.addEventListener("click",U),T.appendChild(ee),T}function X(T,$){const _=$.nodes.find(u=>u.id===T);if(!_)return;const K=$.edges.filter(u=>u.sourceId===T||u.targetId===T);S=K.map(u=>u.sourceId===T?u.targetId:u.sourceId),F=-1,t.innerHTML="",t.classList.remove("hidden"),E&&t.classList.add("info-panel-maximized"),t.appendChild(V());const ee=document.createElement("div");ee.className="info-header";const W=document.createElement("span");if(W.className="info-type-badge",W.textContent=_.type,W.style.backgroundColor=de(_.type),s){W.classList.add("info-editable");const u=document.createElement("button");u.className="info-inline-edit",u.textContent=gt,u.addEventListener("click",e=>{e.stopPropagation();const o=document.createElement("input");o.type="text",o.className="info-edit-inline-input",o.value=_.type,W.textContent="",W.appendChild(o),o.focus(),o.select();const i=()=>{const a=o.value.trim();a&&a!==_.type?s.onChangeNodeType(T,a):(W.textContent=_.type,W.appendChild(u))};o.addEventListener("blur",i),o.addEventListener("keydown",a=>{a.key==="Enter"&&o.blur(),a.key==="Escape"&&(o.value=_.type,o.blur())})}),W.appendChild(u)}const le=document.createElement("h3");le.className="info-label",le.textContent=Me(_);const z=document.createElement("span");z.className="info-id",z.textContent=_.id,ee.appendChild(W),ee.appendChild(le),ee.appendChild(z),t.appendChild(ee);const fe=Object.keys(_.properties),he=Te("Properties");if(fe.length>0){const u=document.createElement("dl");u.className="info-props";for(const e of fe){const o=document.createElement("dt");o.textContent=e;const i=document.createElement("dd");if(s){const a=Re(_.properties[e]),g=document.createElement("textarea");g.className="info-edit-input",g.value=a,g.rows=1,g.addEventListener("input",()=>We(g)),g.addEventListener("keydown",B=>{B.key==="Enter"&&!B.shiftKey&&(B.preventDefault(),g.blur())}),g.addEventListener("blur",()=>{const B=g.value;B!==a&&s.onUpdateNode(T,{[e]:xt(B)})}),i.appendChild(g),requestAnimationFrame(()=>We(g));const O=document.createElement("button");O.className="info-delete-prop",O.textContent="×",O.title=`Remove ${e}`,O.addEventListener("click",()=>{const B={..._.properties};delete B[e],s.onUpdateNode(T,B)}),i.appendChild(O)}else i.appendChild(Ct(_.properties[e]));u.appendChild(o),u.appendChild(i)}he.appendChild(u)}if(s){const u=document.createElement("button");u.className="info-add-btn",u.textContent="+ Add property",u.addEventListener("click",()=>{const e=document.createElement("div");e.className="info-add-row";const o=document.createElement("input");o.type="text",o.className="info-edit-input",o.placeholder="key";const i=document.createElement("input");i.type="text",i.className="info-edit-input",i.placeholder="value";const a=document.createElement("button");a.className="info-add-save",a.textContent="Add",a.addEventListener("click",()=>{o.value&&s.onAddProperty(T,o.value,i.value)}),e.appendChild(o),e.appendChild(i),e.appendChild(a),he.appendChild(e),o.focus()}),he.appendChild(u)}if(t.appendChild(he),K.length>0){const u=Te(`Connections (${K.length})`),e=document.createElement("ul");e.className="info-connections";for(const o of K){const i=o.sourceId===T,a=i?o.targetId:o.sourceId,g=$.nodes.find(se=>se.id===a),O=g?Me(g):a,B=document.createElement("li");if(B.className="info-connection",m&&g&&(B.classList.add("info-connection-link"),B.addEventListener("click",se=>{se.target.closest(".info-delete-edge")||M(a)})),g){const se=document.createElement("span");se.className="info-target-dot",se.style.backgroundColor=de(g.type),B.appendChild(se)}const ne=document.createElement("span");ne.className="info-arrow",ne.textContent=i?"→":"←";const j=document.createElement("span");j.className="info-edge-type",j.textContent=o.type;const ue=document.createElement("span");ue.className="info-target",ue.textContent=O,B.appendChild(ne),B.appendChild(j),B.appendChild(ue);const me=Object.keys(o.properties);if(me.length>0){const se=document.createElement("div");se.className="info-edge-props";for(const r of me){const h=document.createElement("span");h.className="info-edge-prop",h.textContent=`${r}: ${Re(o.properties[r])}`,se.appendChild(h)}B.appendChild(se)}if(s){const se=document.createElement("button");se.className="info-delete-edge",se.textContent="×",se.title="Remove connection",se.addEventListener("click",r=>{r.stopPropagation(),s.onDeleteEdge(o.id)}),B.appendChild(se)}e.appendChild(B)}u.appendChild(e),t.appendChild(u)}const f=Te("Timestamps"),v=document.createElement("dl");v.className="info-props";const x=document.createElement("dt");x.textContent="created";const y=document.createElement("dd");y.textContent=je(_.createdAt);const w=document.createElement("dt");w.textContent="updated";const b=document.createElement("dd");if(b.textContent=je(_.updatedAt),v.appendChild(x),v.appendChild(y),v.appendChild(w),v.appendChild(b),f.appendChild(v),t.appendChild(f),s){const u=document.createElement("div");u.className="info-section info-danger";const e=document.createElement("button");e.className="info-delete-node",e.textContent="Delete node",e.addEventListener("click",()=>{s.onDeleteNode(T),U()}),u.appendChild(e),t.appendChild(u)}}function Z(T,$){const _=new Set(T),K=$.nodes.filter(x=>_.has(x.id));if(K.length===0)return;const ee=$.edges.filter(x=>_.has(x.sourceId)&&_.has(x.targetId));t.innerHTML="",t.classList.remove("hidden"),E&&t.classList.add("info-panel-maximized"),t.appendChild(V());const W=document.createElement("div");W.className="info-header";const le=document.createElement("h3");le.className="info-label",le.textContent=`${K.length} nodes selected`,W.appendChild(le);const z=document.createElement("div");z.style.cssText="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px";const fe=new Map;for(const x of K)fe.set(x.type,(fe.get(x.type)??0)+1);for(const[x,y]of fe){const w=document.createElement("span");w.className="info-type-badge",w.style.backgroundColor=de(x),w.textContent=y>1?`${x} (${y})`:x,z.appendChild(w)}W.appendChild(z),t.appendChild(W);const he=Te("Selected Nodes"),f=document.createElement("ul");f.className="info-connections";for(const x of K){const y=document.createElement("li");y.className="info-connection",m&&(y.classList.add("info-connection-link"),y.addEventListener("click",()=>{M(x.id)}));const w=document.createElement("span");w.className="info-target-dot",w.style.backgroundColor=de(x.type);const b=document.createElement("span");b.className="info-target",b.textContent=Me(x);const u=document.createElement("span");u.className="info-edge-type",u.textContent=x.type,y.appendChild(w),y.appendChild(b),y.appendChild(u),f.appendChild(y)}he.appendChild(f),t.appendChild(he);const v=Te(ee.length>0?`Connections Between Selected (${ee.length})`:"Connections Between Selected");if(ee.length===0){const x=document.createElement("p");x.style.cssText="font-size:12px;color:var(--text-dim)",x.textContent="No direct connections between selected nodes",v.appendChild(x)}else{const x=document.createElement("ul");x.className="info-connections";for(const y of ee){const w=$.nodes.find(j=>j.id===y.sourceId),b=$.nodes.find(j=>j.id===y.targetId),u=w?Me(w):y.sourceId,e=b?Me(b):y.targetId,o=document.createElement("li");if(o.className="info-connection",w){const j=document.createElement("span");j.className="info-target-dot",j.style.backgroundColor=de(w.type),o.appendChild(j)}const i=document.createElement("span");i.className="info-target",i.textContent=u;const a=document.createElement("span");a.className="info-arrow",a.textContent="→";const g=document.createElement("span");g.className="info-edge-type",g.textContent=y.type;const O=document.createElement("span");if(O.className="info-arrow",O.textContent="→",o.appendChild(i),o.appendChild(a),o.appendChild(g),o.appendChild(O),b){const j=document.createElement("span");j.className="info-target-dot",j.style.backgroundColor=de(b.type),o.appendChild(j)}const B=document.createElement("span");B.className="info-target",B.textContent=e,o.appendChild(B);const ne=Object.keys(y.properties);if(ne.length>0){const j=document.createElement("div");j.className="info-edge-props";for(const ue of ne){const me=document.createElement("span");me.className="info-edge-prop",me.textContent=`${ue}: ${Re(y.properties[ue])}`,j.appendChild(me)}o.appendChild(j)}x.appendChild(o)}v.appendChild(x)}t.appendChild(v)}return{show(T,$){if(p=$,C=T,T.length===1&&!k){const _=T[0];n[c]!==_&&(c<n.length-1&&(n=n.slice(0,c+1)),n.push(_),c=n.length-1)}T.length===1?X(T[0],$):T.length>1&&Z(T,$)},hide:U,goBack:D,goForward:R,cycleConnection(T){if(S.length===0)return null;F===-1?F=T===1?0:S.length-1:(F+=T,F>=S.length&&(F=0),F<0&&(F=S.length-1));const $=t.querySelectorAll(".info-connection");return $.forEach((_,K)=>{_.classList.toggle("info-connection-active",K===F)}),F>=0&&$[F]&&$[F].scrollIntoView({block:"nearest"}),S[F]??null},setFocusDisabled(T){A=T;const $=t.querySelector(".info-focus-btn");$&&($.disabled=T,$.style.opacity=T?"0.3":"")},get visible(){return!t.classList.contains("hidden")}}}function Te(l){const s=document.createElement("div");s.className="info-section";const m=document.createElement("h4");return m.className="info-section-title",m.textContent=l,s.appendChild(m),s}function Ct(l){if(Array.isArray(l)){const m=document.createElement("div");m.className="info-array";for(const d of l){const t=document.createElement("span");t.className="info-tag",t.textContent=String(d),m.appendChild(t)}return m}if(l!==null&&typeof l=="object"){const m=document.createElement("pre");return m.className="info-json",m.textContent=JSON.stringify(l,null,2),m}const s=document.createElement("span");return s.className="info-value",s.textContent=String(l??""),s}function Re(l){return Array.isArray(l)?l.map(String).join(", "):l!==null&&typeof l=="object"?JSON.stringify(l):String(l??"")}function xt(l){const s=l.trim();if(s==="true")return!0;if(s==="false")return!1;if(s!==""&&!isNaN(Number(s)))return Number(s);if(s.startsWith("[")&&s.endsWith("]")||s.startsWith("{")&&s.endsWith("}"))try{return JSON.parse(s)}catch{return l}return l}function We(l){l.style.height="auto",l.style.height=l.scrollHeight+"px"}function je(l){try{return new Date(l).toLocaleString()}catch{return l}}function Qe(l){for(const s of Object.values(l.properties))if(typeof s=="string")return s;return l.id}function Ge(l,s){const m=s.toLowerCase();if(Qe(l).toLowerCase().includes(m)||l.type.toLowerCase().includes(m))return!0;for(const d of Object.values(l.properties))if(typeof d=="string"&&d.toLowerCase().includes(m))return!0;return!1}function vt(l){let s=null,m=null,d=null,t=null;const E=document.createElement("div");E.className="search-overlay hidden";const n=document.createElement("div");n.className="search-input-wrap";const c=document.createElement("input");c.className="search-input",c.type="text",c.placeholder="Search nodes...",c.setAttribute("autocomplete","off"),c.setAttribute("spellcheck","false");const k=document.createElement("kbd");k.className="search-kbd",k.textContent="/",n.appendChild(c),n.appendChild(k);const p=document.createElement("ul");p.className="search-results hidden",E.appendChild(n),E.appendChild(p),l.appendChild(E);function C(){if(!s)return null;const M=c.value.trim();if(M.length===0)return null;const D=new Set;for(const R of s.nodes)Ge(R,M)&&D.add(R.id);return D}function A(){const M=C();m==null||m(M),S()}function S(){p.innerHTML="",F=-1;const M=c.value.trim();if(!s||M.length===0){p.classList.add("hidden");return}const D=[];for(const R of s.nodes)if(Ge(R,M)&&(D.push(R),D.length>=8))break;if(D.length===0){p.classList.add("hidden");return}for(const R of D){const V=document.createElement("li");V.className="search-result-item";const X=document.createElement("span");X.className="search-result-dot",X.style.backgroundColor=de(R.type);const Z=document.createElement("span");Z.className="search-result-label";const T=Qe(R);Z.textContent=T.length>36?T.slice(0,34)+"...":T;const $=document.createElement("span");$.className="search-result-type",$.textContent=R.type,V.appendChild(X),V.appendChild(Z),V.appendChild($),V.addEventListener("click",()=>{d==null||d(R.id),c.value="",p.classList.add("hidden"),A()}),p.appendChild(V)}p.classList.remove("hidden")}c.addEventListener("input",()=>{t&&clearTimeout(t),t=setTimeout(A,150)});let F=-1;function U(){const M=p.querySelectorAll(".search-result-item");M.forEach((D,R)=>{D.classList.toggle("search-result-active",R===F)}),F>=0&&M[F]&&M[F].scrollIntoView({block:"nearest"})}return c.addEventListener("keydown",M=>{const D=p.querySelectorAll(".search-result-item");M.key==="ArrowDown"?(M.preventDefault(),D.length>0&&(F=Math.min(F+1,D.length-1),U())):M.key==="ArrowUp"?(M.preventDefault(),D.length>0&&(F=Math.max(F-1,0),U())):M.key==="Enter"?(M.preventDefault(),F>=0&&D[F]?D[F].click():D.length>0&&D[0].click(),c.blur()):M.key==="Escape"&&(c.value="",c.blur(),p.classList.add("hidden"),F=-1,A())}),document.addEventListener("click",M=>{E.contains(M.target)||p.classList.add("hidden")}),c.addEventListener("focus",()=>k.classList.add("hidden")),c.addEventListener("blur",()=>{c.value.length===0&&k.classList.remove("hidden")}),{setLearningGraphData(M){s=M,c.value="",p.classList.add("hidden"),s&&s.nodes.length>0?E.classList.remove("hidden"):E.classList.add("hidden")},onFilterChange(M){m=M},onNodeSelect(M){d=M},clear(){c.value="",p.classList.add("hidden"),m==null||m(null)},focus(){c.focus()}}}function Et(l,s){let m=null,d=null,t=!0,E=null,n=!0,c=!0,k=!0,p="types",C="",A="";const S={types:new Set,nodeIds:new Set};function F(){if(!m)return[];const f=new Set;for(const v of m.nodes)S.types.has(v.type)&&f.add(v.id);for(const v of S.nodeIds)f.add(v);return[...f]}function U(f){if(S.nodeIds.has(f))return!0;const v=m==null?void 0:m.nodes.find(x=>x.id===f);return v?S.types.has(v.type):!1}function M(){return S.types.size===0&&S.nodeIds.size===0}function D(){const f=F();s.onFocusChange(f.length>0?f:null)}const R=document.createElement("button");R.className="tools-pane-toggle hidden",R.title="Graph Inspector",R.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h10"/></svg>';const V=document.createElement("div");V.className="tools-pane-content hidden",l.appendChild(R),l.appendChild(V),R.addEventListener("click",()=>{var f;t=!t,V.classList.toggle("hidden",t),R.classList.toggle("active",!t),t||(f=s.onOpen)==null||f.call(s)});function X(){if(V.innerHTML="",!d)return;const f=document.createElement("div");f.className="tools-pane-summary",f.innerHTML=`<span>${d.nodeCount} nodes</span><span class="tools-pane-sep">&middot;</span><span>${d.edgeCount} edges</span><span class="tools-pane-sep">&middot;</span><span>${d.types.length} types</span>`,V.appendChild(f);const v=document.createElement("div");v.className="tools-pane-tabs";const x=[{id:"types",label:"Types"},{id:"quality",label:"Quality"},{id:"controls",label:"Controls"}];for(const w of x){const b=document.createElement("button");b.className="tools-pane-tab",p===w.id&&b.classList.add("tools-pane-tab-active"),b.textContent=w.label,b.addEventListener("click",()=>{p=w.id,X()}),v.appendChild(b)}V.appendChild(v),M()||T(),p==="types"&&d.types.length>5?V.appendChild(_("Filter types...",C,w=>{C=w,Z()})):p==="quality"&&d.orphans.length+d.singletons.length+d.emptyNodes.length>5&&V.appendChild(_("Filter issues...",A,b=>{A=b,Z()}));const y=document.createElement("div");y.className="tools-pane-tab-content",V.appendChild(y),Z()}function Z(){const f=V.querySelector(".tools-pane-tab-content");f&&(f.innerHTML="",p==="types"?K(f):p==="quality"?ee(f):p==="controls"&&W(f))}function T(){if(!d||!m)return;const f=F();V.appendChild(z("Focused",v=>{for(const b of S.types){const u=d.types.find(O=>O.name===b);if(!u)continue;const e=document.createElement("div");e.className="tools-pane-row tools-pane-clickable";const o=document.createElement("span");o.className="tools-pane-dot",o.style.backgroundColor=de(u.name);const i=document.createElement("span");i.className="tools-pane-name",i.textContent=u.name;const a=document.createElement("span");a.className="tools-pane-count",a.textContent=`${u.count} nodes`;const g=document.createElement("button");g.className="tools-pane-edit tools-pane-focus-active",g.style.opacity="1",g.textContent="×",g.title=`Remove ${u.name} from focus`,e.appendChild(o),e.appendChild(i),e.appendChild(a),e.appendChild(g),g.addEventListener("click",O=>{O.stopPropagation(),S.types.delete(u.name),D(),X()}),v.appendChild(e)}for(const b of S.nodeIds){const u=m.nodes.find(B=>B.id===b);if(!u)continue;const e=Ve(u.properties)??u.id,o=document.createElement("div");o.className="tools-pane-row tools-pane-clickable";const i=document.createElement("span");i.className="tools-pane-dot",i.style.backgroundColor=de(u.type);const a=document.createElement("span");a.className="tools-pane-name",a.textContent=e;const g=document.createElement("span");g.className="tools-pane-count",g.textContent=u.type;const O=document.createElement("button");O.className="tools-pane-edit tools-pane-focus-active",O.style.opacity="1",O.textContent="×",O.title=`Remove ${e} from focus`,o.appendChild(i),o.appendChild(a),o.appendChild(g),o.appendChild(O),o.addEventListener("click",B=>{B.target.closest(".tools-pane-edit")||s.onNavigateToNode(b)}),O.addEventListener("click",B=>{B.stopPropagation(),S.nodeIds.delete(b),D(),X()}),v.appendChild(o)}const x=document.createElement("div");x.className="tools-pane-row tools-pane-clickable tools-pane-focus-clear";const y=document.createElement("span");y.className="tools-pane-name",y.style.color="var(--accent)",y.textContent=`${f.length} total`;const w=document.createElement("span");w.className="tools-pane-badge",w.textContent="clear all",x.appendChild(y),x.appendChild(w),x.addEventListener("click",()=>{S.types.clear(),S.nodeIds.clear(),D(),X()}),v.appendChild(x)}))}function $(f){const v=document.createElement("div");v.className="tools-pane-row tools-pane-clickable",E===f.name&&v.classList.add("active");const x=document.createElement("span");x.className="tools-pane-dot",x.style.backgroundColor=de(f.name);const y=document.createElement("span");y.className="tools-pane-name",y.textContent=f.name;const w=document.createElement("span");w.className="tools-pane-count",w.textContent=String(f.count);const b=document.createElement("button");b.className="tools-pane-edit tools-pane-focus-toggle",S.types.has(f.name)&&b.classList.add("tools-pane-focus-active"),b.textContent="◎",b.title=S.types.has(f.name)?`Remove ${f.name} from focus`:`Add ${f.name} to focus`;const u=document.createElement("button");return u.className="tools-pane-edit",u.textContent="✎",u.title=`Rename all ${f.name} nodes`,v.appendChild(x),v.appendChild(y),v.appendChild(w),v.appendChild(b),v.appendChild(u),v.addEventListener("click",e=>{e.target.closest(".tools-pane-edit")||(E===f.name?(E=null,s.onFilterByType(null)):(E=f.name,s.onFilterByType(f.name)),X())}),b.addEventListener("click",e=>{e.stopPropagation(),S.types.has(f.name)?S.types.delete(f.name):S.types.add(f.name),D(),X()}),u.addEventListener("click",e=>{e.stopPropagation(),fe(v,f.name,o=>{o&&o!==f.name&&s.onRenameNodeType(f.name,o)})}),v}function _(f,v,x){const y=document.createElement("input");return y.type="text",y.className="tools-pane-search",y.placeholder=f,y.value=v,y.addEventListener("input",()=>x(y.value)),y}function K(f){if(!d)return;const v=C.toLowerCase();if(d.types.length){const w=d.types.filter(b=>!S.types.has(b.name)).filter(b=>!v||b.name.toLowerCase().includes(v));w.length>0&&f.appendChild(z("Node Types",b=>{for(const u of w)b.appendChild($(u))}))}const x=d.edgeTypes.filter(w=>!v||w.name.toLowerCase().includes(v));x.length&&f.appendChild(z("Edge Types",w=>{for(const b of x){const u=document.createElement("div");u.className="tools-pane-row tools-pane-clickable";const e=document.createElement("span");e.className="tools-pane-name",e.textContent=b.name;const o=document.createElement("span");o.className="tools-pane-count",o.textContent=String(b.count);const i=document.createElement("button");i.className="tools-pane-edit",i.textContent="✎",i.title=`Rename all ${b.name} edges`,u.appendChild(e),u.appendChild(o),u.appendChild(i),i.addEventListener("click",a=>{a.stopPropagation(),fe(u,b.name,g=>{g&&g!==b.name&&s.onRenameEdgeType(b.name,g)})}),w.appendChild(u)}}));const y=d.mostConnected.filter(w=>!v||w.label.toLowerCase().includes(v)||w.type.toLowerCase().includes(v));y.length&&f.appendChild(z("Most Connected",w=>{for(const b of y){const u=document.createElement("div");u.className="tools-pane-row tools-pane-clickable";const e=document.createElement("span");e.className="tools-pane-dot",e.style.backgroundColor=de(b.type);const o=document.createElement("span");o.className="tools-pane-name",o.textContent=b.label;const i=document.createElement("span");i.className="tools-pane-count",i.textContent=`${b.connections}`;const a=document.createElement("button");a.className="tools-pane-edit tools-pane-focus-toggle",U(b.id)&&a.classList.add("tools-pane-focus-active"),a.textContent="◎",a.title=U(b.id)?`Remove ${b.label} from focus`:`Add ${b.label} to focus`,u.appendChild(e),u.appendChild(o),u.appendChild(i),u.appendChild(a),u.addEventListener("click",g=>{g.target.closest(".tools-pane-edit")||s.onNavigateToNode(b.id)}),a.addEventListener("click",g=>{g.stopPropagation(),S.nodeIds.has(b.id)?S.nodeIds.delete(b.id):S.nodeIds.add(b.id),D(),X()}),w.appendChild(u)}}))}function ee(f){if(!d)return;const v=A.toLowerCase(),x=d.orphans.filter(o=>!v||o.label.toLowerCase().includes(v)||o.type.toLowerCase().includes(v)),y=d.singletons.filter(o=>!v||o.name.toLowerCase().includes(v)),w=d.emptyNodes.filter(o=>!v||o.label.toLowerCase().includes(v)||o.type.toLowerCase().includes(v)),b=x.length>0,u=y.length>0,e=w.length>0;if(!b&&!u&&!e){const o=document.createElement("div");o.className="tools-pane-empty-msg",o.textContent="No issues found",f.appendChild(o);return}b&&f.appendChild(z("Orphans",o=>{for(const i of x.slice(0,5)){const a=document.createElement("div");a.className="tools-pane-row tools-pane-clickable tools-pane-issue";const g=document.createElement("span");g.className="tools-pane-dot",g.style.backgroundColor=de(i.type);const O=document.createElement("span");O.className="tools-pane-name",O.textContent=i.label;const B=document.createElement("span");B.className="tools-pane-badge",B.textContent="orphan";const ne=document.createElement("button");ne.className="tools-pane-edit tools-pane-focus-toggle",U(i.id)&&ne.classList.add("tools-pane-focus-active"),ne.textContent="◎",ne.title=U(i.id)?`Remove ${i.label} from focus`:`Add ${i.label} to focus`,a.appendChild(g),a.appendChild(O),a.appendChild(B),a.appendChild(ne),a.addEventListener("click",j=>{j.target.closest(".tools-pane-edit")||s.onNavigateToNode(i.id)}),ne.addEventListener("click",j=>{j.stopPropagation(),S.nodeIds.has(i.id)?S.nodeIds.delete(i.id):S.nodeIds.add(i.id),D(),X()}),o.appendChild(a)}if(x.length>5){const i=document.createElement("div");i.className="tools-pane-more",i.textContent=`+ ${x.length-5} more orphans`,o.appendChild(i)}})),u&&f.appendChild(z("Singletons",o=>{for(const i of y.slice(0,5)){const a=document.createElement("div");a.className="tools-pane-row tools-pane-issue";const g=document.createElement("span");g.className="tools-pane-dot",g.style.backgroundColor=de(i.name);const O=document.createElement("span");O.className="tools-pane-name",O.textContent=i.name;const B=document.createElement("span");B.className="tools-pane-badge",B.textContent="1 node",a.appendChild(g),a.appendChild(O),a.appendChild(B),o.appendChild(a)}})),e&&f.appendChild(z("Empty Nodes",o=>{for(const i of w.slice(0,5)){const a=document.createElement("div");a.className="tools-pane-row tools-pane-issue";const g=document.createElement("span");g.className="tools-pane-dot",g.style.backgroundColor=de(i.type);const O=document.createElement("span");O.className="tools-pane-name",O.textContent=i.label;const B=document.createElement("span");B.className="tools-pane-badge",B.textContent="empty",a.appendChild(g),a.appendChild(O),a.appendChild(B),o.appendChild(a)}if(d.emptyNodes.length>5){const i=document.createElement("div");i.className="tools-pane-more",i.textContent=`+ ${d.emptyNodes.length-5} more empty nodes`,o.appendChild(i)}}))}function W(f){f.appendChild(z("Display",v=>{const x=document.createElement("div");x.className="tools-pane-row tools-pane-clickable";const y=document.createElement("input");y.type="checkbox",y.checked=n,y.className="tools-pane-checkbox";const w=document.createElement("span");w.className="tools-pane-name",w.textContent="Edge labels",x.appendChild(y),x.appendChild(w),x.addEventListener("click",g=>{g.target!==y&&(y.checked=!y.checked),n=y.checked,s.onToggleEdgeLabels(n)}),v.appendChild(x);const b=document.createElement("div");b.className="tools-pane-row tools-pane-clickable";const u=document.createElement("input");u.type="checkbox",u.checked=c,u.className="tools-pane-checkbox";const e=document.createElement("span");e.className="tools-pane-name",e.textContent="Type regions",b.appendChild(u),b.appendChild(e),b.addEventListener("click",g=>{g.target!==u&&(u.checked=!u.checked),c=u.checked,s.onToggleTypeHulls(c)}),v.appendChild(b);const o=document.createElement("div");o.className="tools-pane-row tools-pane-clickable";const i=document.createElement("input");i.type="checkbox",i.checked=k,i.className="tools-pane-checkbox";const a=document.createElement("span");a.className="tools-pane-name",a.textContent="Minimap",o.appendChild(i),o.appendChild(a),o.addEventListener("click",g=>{g.target!==i&&(i.checked=!i.checked),k=i.checked,s.onToggleMinimap(k)}),v.appendChild(o)})),f.appendChild(z("Layout",v=>{v.appendChild(le("Clustering",0,1,.02,.08,x=>{s.onLayoutChange("clusterStrength",x)})),v.appendChild(le("Spacing",.5,20,.5,1.5,x=>{s.onLayoutChange("spacing",x)})),v.appendChild(le("Pan speed",20,200,10,60,x=>{s.onPanSpeedChange(x)}))})),f.appendChild(z("Export",v=>{const x=document.createElement("div");x.className="tools-pane-export-row";const y=document.createElement("button");y.className="tools-pane-export-btn",y.textContent="Export PNG",y.addEventListener("click",()=>s.onExport("png"));const w=document.createElement("button");w.className="tools-pane-export-btn",w.textContent="Export SVG",w.addEventListener("click",()=>s.onExport("svg")),x.appendChild(y),x.appendChild(w),v.appendChild(x)}))}function le(f,v,x,y,w,b){const u=document.createElement("div");u.className="tools-pane-slider-row";const e=document.createElement("span");e.className="tools-pane-slider-label",e.textContent=f;const o=document.createElement("input");o.type="range",o.className="tools-pane-slider",o.min=String(v),o.max=String(x),o.step=String(y),o.value=String(w);const i=document.createElement("span");return i.className="tools-pane-slider-value",i.textContent=String(w),o.addEventListener("input",()=>{const a=parseFloat(o.value);i.textContent=a%1===0?String(a):a.toFixed(2),b(a)}),u.appendChild(e),u.appendChild(o),u.appendChild(i),u}function z(f,v){const x=document.createElement("div");x.className="tools-pane-section";const y=document.createElement("div");return y.className="tools-pane-heading",y.textContent=f,x.appendChild(y),v(x),x}function fe(f,v,x){const y=document.createElement("input");y.className="tools-pane-inline-input",y.value=v,y.type="text";const w=f.innerHTML;f.innerHTML="",f.classList.add("tools-pane-editing"),f.appendChild(y),y.focus(),y.select();function b(){const u=y.value.trim();f.classList.remove("tools-pane-editing"),u&&u!==v?x(u):f.innerHTML=w}y.addEventListener("keydown",u=>{u.key==="Enter"&&(u.preventDefault(),b()),u.key==="Escape"&&(f.innerHTML=w,f.classList.remove("tools-pane-editing"))}),y.addEventListener("blur",b)}function he(f){const v=new Map,x=new Map,y=new Map,w=new Set;for(const a of f.nodes)v.set(a.type,(v.get(a.type)??0)+1);for(const a of f.edges)x.set(a.type,(x.get(a.type)??0)+1),y.set(a.sourceId,(y.get(a.sourceId)??0)+1),y.set(a.targetId,(y.get(a.targetId)??0)+1),w.add(a.sourceId),w.add(a.targetId);const b=a=>Ve(a.properties)??a.id,u=f.nodes.filter(a=>!w.has(a.id)).map(a=>({id:a.id,label:b(a),type:a.type})),e=[...v.entries()].filter(([,a])=>a===1).map(([a])=>({name:a})),o=f.nodes.filter(a=>Object.keys(a.properties).length===0).map(a=>({id:a.id,label:a.id,type:a.type})),i=f.nodes.map(a=>({id:a.id,label:b(a),type:a.type,connections:y.get(a.id)??0})).filter(a=>a.connections>0).sort((a,g)=>g.connections-a.connections).slice(0,5);return{nodeCount:f.nodes.length,edgeCount:f.edges.length,types:[...v.entries()].sort((a,g)=>g[1]-a[1]).map(([a,g])=>({name:a,count:g})),edgeTypes:[...x.entries()].sort((a,g)=>g[1]-a[1]).map(([a,g])=>({name:a,count:g})),orphans:u,singletons:e,emptyNodes:o,mostConnected:i}}return{collapse(){t=!0,V.classList.add("hidden"),R.classList.remove("active")},addToFocusSet(f){for(const v of f)S.nodeIds.add(v);D(),X()},clearFocusSet(){S.types.clear(),S.nodeIds.clear(),D(),X()},setData(f){m=f,E=null,S.types.clear(),S.nodeIds.clear(),m&&m.nodes.length>0?(d=he(m),R.classList.remove("hidden"),X()):(d=null,R.classList.add("hidden"),V.classList.add("hidden"))}}}function Ve(l){for(const s of Object.values(l))if(typeof s=="string")return s;return null}function bt(l,s){const m=s.toLowerCase().split("+"),d=m.pop(),t=m.includes("ctrl")||m.includes("cmd")||m.includes("meta"),E=m.includes("shift"),n=m.includes("alt");return t!==(l.ctrlKey||l.metaKey)||E!==l.shiftKey||n!==l.altKey||!t&&(l.ctrlKey||l.metaKey)?!1:d==="escape"?l.key==="Escape":d.length===1?l.key===s.split("+").pop():l.key.toLowerCase()===d}function Lt(){return{search:"Focus search",searchAlt:"Focus search (alt)",undo:"Undo",redo:"Redo",help:"Toggle help",escape:"Exit focus / close panel",focus:"Focus on selected / exit focus",toggleEdges:"Toggle edges on/off",center:"Center view on graph",nextNode:"Next node in view",prevNode:"Previous node in view",nextConnection:"Next connection",prevConnection:"Previous connection",historyBack:"Node history back",historyForward:"Node history forward",hopsIncrease:"Increase hops",hopsDecrease:"Decrease hops",panLeft:"Pan left",panDown:"Pan down",panUp:"Pan up",panRight:"Pan right",panFastLeft:"Pan fast left",zoomOut:"Zoom out",zoomIn:"Zoom in",panFastRight:"Pan fast right",spacingDecrease:"Decrease spacing",spacingIncrease:"Increase spacing",clusteringDecrease:"Decrease clustering",clusteringIncrease:"Increase clustering"}}const wt=[{key:"Click",description:"Select node"},{key:"Ctrl+Click",description:"Multi-select nodes"},{key:"Drag",description:"Pan canvas"},{key:"Scroll",description:"Zoom in/out"}],Nt=["search","searchAlt","undo","redo","help","focus","toggleEdges","center","nextNode","prevNode","nextConnection","prevConnection","historyBack","historyForward","hopsIncrease","hopsDecrease","panLeft","panDown","panUp","panRight","panFastLeft","panFastRight","zoomIn","zoomOut","spacingDecrease","spacingIncrease","clusteringDecrease","clusteringIncrease","escape"];function St(l){return l.split("+").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("+")}function It(l,s){const m=Lt(),d=document.createElement("div");d.className="shortcuts-overlay hidden";const t=document.createElement("div");t.className="shortcuts-modal";const E=document.createElement("h3");E.className="shortcuts-title",E.textContent="Keyboard Shortcuts";const n=document.createElement("div");n.className="shortcuts-list";for(const A of Nt){const S=s[A];if(!S)continue;const F=document.createElement("div");F.className="shortcuts-row";const U=document.createElement("div");U.className="shortcuts-keys";const M=document.createElement("kbd");M.textContent=St(S),U.appendChild(M);const D=document.createElement("span");D.className="shortcuts-desc",D.textContent=m[A],F.appendChild(U),F.appendChild(D),n.appendChild(F)}for(const A of wt){const S=document.createElement("div");S.className="shortcuts-row";const F=document.createElement("div");F.className="shortcuts-keys";const U=document.createElement("kbd");U.textContent=A.key,F.appendChild(U);const M=document.createElement("span");M.className="shortcuts-desc",M.textContent=A.description,S.appendChild(F),S.appendChild(M),n.appendChild(S)}const c=document.createElement("button");c.className="shortcuts-close",c.textContent="×",t.appendChild(c),t.appendChild(E),t.appendChild(n),d.appendChild(t),l.appendChild(d);function k(){d.classList.remove("hidden")}function p(){d.classList.add("hidden")}function C(){d.classList.toggle("hidden")}return c.addEventListener("click",p),d.addEventListener("click",A=>{A.target===d&&p()}),{show:k,hide:p,toggle:C}}function kt(l){const s=document.createElement("div");return s.className="empty-state",s.innerHTML=`
5
+ <div class="empty-state-content">
6
+ <div class="empty-state-icon">
7
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8
+ <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0022 16z"/>
9
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
10
+ <line x1="12" y1="22.08" x2="12" y2="12"/>
11
+ </svg>
12
+ </div>
13
+ <h2 class="empty-state-title">No learning graphs yet</h2>
14
+ <p class="empty-state-desc">Connect Backpack to Claude, then start a conversation. Claude will build your first learning graph automatically.</p>
15
+ <div class="empty-state-setup">
16
+ <div class="empty-state-label">Add Backpack to Claude Code:</div>
17
+ <code class="empty-state-code">claude mcp add backpack-local -s user -- npx backpack-ontology@latest</code>
18
+ </div>
19
+ <p class="empty-state-hint">Press <kbd>?</kbd> for keyboard shortcuts</p>
20
+ </div>
21
+ `,l.appendChild(s),{show(){s.classList.remove("hidden")},hide(){s.classList.add("hidden")}}}const Mt=30;function Tt(){let l=[],s=[];return{push(m){l.push(JSON.stringify(m)),l.length>Mt&&l.shift(),s=[]},undo(m){return l.length===0?null:(s.push(JSON.stringify(m)),JSON.parse(l.pop()))},redo(m){return s.length===0?null:(l.push(JSON.stringify(m)),JSON.parse(s.pop()))},canUndo(){return l.length>0},canRedo(){return s.length>0},clear(){l=[],s=[]}}}const At={search:"/",searchAlt:"ctrl+k",undo:"ctrl+z",redo:"ctrl+shift+z",help:"?",escape:"Escape",focus:"f",toggleEdges:"e",center:"c",nextNode:".",prevNode:",",nextConnection:">",prevConnection:"<",historyBack:"(",historyForward:")",hopsIncrease:"=",hopsDecrease:"-",panLeft:"h",panDown:"j",panUp:"k",panRight:"l",panFastLeft:"H",zoomOut:"J",zoomIn:"K",panFastRight:"L",spacingDecrease:"[",spacingIncrease:"]",clusteringDecrease:"{",clusteringIncrease:"}"},Dt={keybindings:At};let pe="",L=null;async function Ft(){const l=document.getElementById("canvas-container");let s=Dt.keybindings;try{const e=await fetch("/api/config");if(e.ok){const o=await e.json();s={...s,...o.keybindings??{}}}}catch{}const m=window.matchMedia("(prefers-color-scheme: dark)"),t=localStorage.getItem("backpack-theme")??(m.matches?"dark":"light");document.documentElement.setAttribute("data-theme",t);const E=document.createElement("button");E.className="theme-toggle",E.textContent=t==="light"?"☾":"☼",E.title="Toggle light/dark mode",E.addEventListener("click",()=>{const o=document.documentElement.getAttribute("data-theme")==="light"?"dark":"light";document.documentElement.setAttribute("data-theme",o),localStorage.setItem("backpack-theme",o),E.textContent=o==="light"?"☾":"☼"}),l.appendChild(E);const n=Tt();async function c(){if(!pe||!L)return;L.metadata.updatedAt=new Date().toISOString(),await Oe(pe,L),p.loadGraph(L),X.setLearningGraphData(L),Z.setData(L);const e=await Ae();z.setSummaries(e)}async function k(e){L=e,await Oe(pe,L),p.loadGraph(L),X.setLearningGraphData(L),Z.setData(L);const o=await Ae();z.setSummaries(o)}let p;const C=yt(l,{onUpdateNode(e,o){if(!L)return;n.push(L);const i=L.nodes.find(a=>a.id===e);i&&(i.properties={...i.properties,...o},i.updatedAt=new Date().toISOString(),c().then(()=>C.show([e],L)))},onChangeNodeType(e,o){if(!L)return;n.push(L);const i=L.nodes.find(a=>a.id===e);i&&(i.type=o,i.updatedAt=new Date().toISOString(),c().then(()=>C.show([e],L)))},onDeleteNode(e){L&&(n.push(L),L.nodes=L.nodes.filter(o=>o.id!==e),L.edges=L.edges.filter(o=>o.sourceId!==e&&o.targetId!==e),c())},onDeleteEdge(e){var i;if(!L)return;n.push(L);const o=(i=L.edges.find(a=>a.id===e))==null?void 0:i.sourceId;L.edges=L.edges.filter(a=>a.id!==e),c().then(()=>{o&&L&&C.show([o],L)})},onAddProperty(e,o,i){if(!L)return;n.push(L);const a=L.nodes.find(g=>g.id===e);a&&(a.properties[o]=i,a.updatedAt=new Date().toISOString(),c().then(()=>C.show([e],L)))}},e=>{p.panToNode(e)},e=>{Z.addToFocusSet(e)}),A=window.matchMedia("(max-width: 768px)");let S=[],F=!0,U=60,M=-1,D=null;function R(e){D&&D.remove(),D=document.createElement("div"),D.className="focus-indicator";const o=document.createElement("span");o.className="focus-indicator-label",o.textContent=`Focused: ${e.totalNodes} nodes`;const i=document.createElement("span");i.className="focus-indicator-hops",i.textContent=`${e.hops}`;const a=document.createElement("button");a.className="focus-indicator-btn",a.textContent="−",a.title="Fewer hops",a.disabled=e.hops===0,a.addEventListener("click",()=>{p.enterFocus(e.seedNodeIds,Math.max(0,e.hops-1))});const g=document.createElement("button");g.className="focus-indicator-btn",g.textContent="+",g.title="More hops",g.disabled=!1,g.addEventListener("click",()=>{p.enterFocus(e.seedNodeIds,e.hops+1)});const O=document.createElement("button");O.className="focus-indicator-btn focus-indicator-exit",O.textContent="×",O.title="Exit focus (Esc)",O.addEventListener("click",()=>Z.clearFocusSet()),D.appendChild(o),D.appendChild(a),D.appendChild(i),D.appendChild(g),D.appendChild(O)}function V(){D&&(D.remove(),D=null)}p=ht(l,e=>{S=e??[],e&&e.length>0&&L?(C.show(e,L),A.matches&&Z.collapse(),f(pe,e)):(C.hide(),pe&&f(pe))},e=>{if(e){R(e);const o=l.querySelector(".canvas-top-left");o&&D&&o.appendChild(D),f(pe,e.seedNodeIds),C.setFocusDisabled(e.hops===0)}else V(),C.setFocusDisabled(!1),pe&&f(pe)});const X=vt(l),Z=Et(l,{onFilterByType(e){if(L)if(e===null)p.setFilteredNodeIds(null);else{const o=new Set(((L==null?void 0:L.nodes)??[]).filter(i=>i.type===e).map(i=>i.id));p.setFilteredNodeIds(o)}},onNavigateToNode(e){p.panToNode(e),L&&C.show([e],L)},onFocusChange(e){e&&e.length>0?p.enterFocus(e,0):p.isFocused()&&p.exitFocus()},onRenameNodeType(e,o){if(L){n.push(L);for(const i of L.nodes)i.type===e&&(i.type=o,i.updatedAt=new Date().toISOString());c()}},onRenameEdgeType(e,o){if(L){n.push(L);for(const i of L.edges)i.type===e&&(i.type=o);c()}},onToggleEdgeLabels(e){p.setEdgeLabels(e)},onToggleTypeHulls(e){p.setTypeHulls(e)},onToggleMinimap(e){p.setMinimap(e)},onLayoutChange(e,o){ke({[e]:o}),p.reheat()},onPanSpeedChange(e){U=e},onExport(e){const o=p.exportImage(e);if(!o)return;const i=document.createElement("a");i.download=`${pe||"graph"}.${e}`,i.href=o,i.click()},onOpen(){A.matches&&C.hide()}}),T=document.createElement("div");T.className="canvas-top-bar";const $=document.createElement("div");$.className="canvas-top-left";const _=document.createElement("div");_.className="canvas-top-center";const K=document.createElement("div");K.className="canvas-top-right";const ee=l.querySelector(".tools-pane-toggle");ee&&$.appendChild(ee);const W=l.querySelector(".search-overlay");W&&_.appendChild(W);const le=l.querySelector(".zoom-controls");le&&K.appendChild(le),K.appendChild(E),T.appendChild($),T.appendChild(_),T.appendChild(K),l.appendChild(T),X.onFilterChange(e=>{p.setFilteredNodeIds(e)}),X.onNodeSelect(e=>{p.isFocused()&&Z.clearFocusSet(),p.panToNode(e),L&&C.show([e],L)});const z=ot(document.getElementById("sidebar"),{onSelect:e=>x(e),onRename:async(e,o)=>{await nt(e,o),pe===e&&(pe=o);const i=await Ae();z.setSummaries(i),z.setActive(pe),pe===o&&(L=await Be(o),p.loadGraph(L),X.setLearningGraphData(L),Z.setData(L))}}),fe=It(l,s),he=kt(l);function f(e,o){const i=[];o!=null&&o.length&&i.push("node="+o.map(encodeURIComponent).join(","));const a=p.getFocusInfo();a&&(i.push("focus="+a.seedNodeIds.map(encodeURIComponent).join(",")),i.push("hops="+a.hops));const g="#"+encodeURIComponent(e)+(i.length?"?"+i.join("&"):"");history.replaceState(null,"",g)}function v(){const e=window.location.hash.slice(1);if(!e)return{graph:null,nodes:[],focus:[],hops:1};const[o,i]=e.split("?"),a=o?decodeURIComponent(o):null;let g=[],O=[],B=1;if(i){const ne=new URLSearchParams(i),j=ne.get("node");j&&(g=j.split(",").map(decodeURIComponent));const ue=ne.get("focus");ue&&(O=ue.split(",").map(decodeURIComponent));const me=ne.get("hops");me&&(B=Math.max(0,parseInt(me,10)||1))}return{graph:a,nodes:g,focus:O,hops:B}}async function x(e,o,i,a){if(pe=e,z.setActive(e),C.hide(),V(),X.clear(),n.clear(),L=await Be(e),ke(it(L.nodes.length)),p.loadGraph(L),X.setLearningGraphData(L),Z.setData(L),he.hide(),f(e),i!=null&&i.length&&L){const g=i.filter(O=>L.nodes.some(B=>B.id===O));if(g.length){setTimeout(()=>{p.enterFocus(g,a??1)},500);return}}if(o!=null&&o.length&&L){const g=o.filter(O=>L.nodes.some(B=>B.id===O));g.length&&setTimeout(()=>{p.panToNodes(g),L&&C.show(g,L),f(e,g)},500)}}const y=await Ae();z.setSummaries(y);const w=v(),b=w.graph&&y.some(e=>e.name===w.graph)?w.graph:y.length>0?y[0].name:null;b?await x(b,w.nodes.length?w.nodes:void 0,w.focus.length?w.focus:void 0,w.hops):he.show();const u={search(){X.focus()},searchAlt(){X.focus()},undo(){if(L){const e=n.undo(L);e&&k(e)}},redo(){if(L){const e=n.redo(L);e&&k(e)}},focus(){p.isFocused()?Z.clearFocusSet():S.length>0&&Z.addToFocusSet(S)},hopsDecrease(){const e=p.getFocusInfo();e&&e.hops>0&&p.enterFocus(e.seedNodeIds,e.hops-1)},hopsIncrease(){const e=p.getFocusInfo();e&&p.enterFocus(e.seedNodeIds,e.hops+1)},nextNode(){const e=p.getNodeIds();e.length>0&&(M=(M+1)%e.length,p.panToNode(e[M]),L&&C.show([e[M]],L))},prevNode(){const e=p.getNodeIds();e.length>0&&(M=M<=0?e.length-1:M-1,p.panToNode(e[M]),L&&C.show([e[M]],L))},nextConnection(){const e=C.cycleConnection(1);e&&p.panToNode(e)},prevConnection(){const e=C.cycleConnection(-1);e&&p.panToNode(e)},historyBack(){C.goBack()},historyForward(){C.goForward()},center(){p.centerView()},toggleEdges(){F=!F,p.setEdges(F)},panLeft(){p.panBy(-U,0)},panDown(){p.panBy(0,U)},panUp(){p.panBy(0,-U)},panRight(){p.panBy(U,0)},panFastLeft(){p.panBy(-U*3,0)},zoomOut(){p.zoomBy(.8)},zoomIn(){p.zoomBy(1.25)},panFastRight(){p.panBy(U*3,0)},spacingDecrease(){const e=De();ke({spacing:Math.max(.5,e.spacing-.5)}),p.reheat()},spacingIncrease(){const e=De();ke({spacing:Math.min(20,e.spacing+.5)}),p.reheat()},clusteringDecrease(){const e=De();ke({clusterStrength:Math.max(0,e.clusterStrength-.03)}),p.reheat()},clusteringIncrease(){const e=De();ke({clusterStrength:Math.min(1,e.clusterStrength+.03)}),p.reheat()},help(){fe.toggle()},escape(){p.isFocused()?Z.clearFocusSet():fe.hide()}};document.addEventListener("keydown",e=>{var o;if(!(e.target instanceof HTMLInputElement||e.target instanceof HTMLTextAreaElement)){for(const[i,a]of Object.entries(s))if(bt(e,a)){(i==="search"||i==="searchAlt"||i==="undo"||i==="redo")&&e.preventDefault(),(o=u[i])==null||o.call(u);return}}}),window.addEventListener("hashchange",()=>{const e=v();if(e.graph&&e.graph!==pe)x(e.graph,e.nodes.length?e.nodes:void 0,e.focus.length?e.focus:void 0,e.hops);else if(e.graph&&e.focus.length&&L)p.enterFocus(e.focus,e.hops);else if(e.graph&&e.nodes.length&&L){p.isFocused()&&p.exitFocus();const o=e.nodes.filter(i=>L.nodes.some(a=>a.id===i));o.length&&(p.panToNodes(o),C.show(o,L))}})}Ft();
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Backpack Viewer</title>
7
- <script type="module" crossorigin src="/assets/index-Mi0vDG5K.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-z15vEFEy.css">
7
+ <script type="module" crossorigin src="/assets/index-D-5q69aO.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CKtt38XS.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app">
package/dist/canvas.d.ts CHANGED
@@ -9,14 +9,20 @@ export declare function initCanvas(container: HTMLElement, onNodeClick?: (nodeId
9
9
  setFilteredNodeIds(ids: Set<string> | null): void;
10
10
  panToNode(nodeId: string): void;
11
11
  panToNodes(nodeIds: string[]): void;
12
+ setEdges(visible: boolean): void;
12
13
  setEdgeLabels(visible: boolean): void;
13
14
  setTypeHulls(visible: boolean): void;
14
15
  setMinimap(visible: boolean): void;
16
+ centerView(): void;
17
+ panBy(dx: number, dy: number): void;
18
+ zoomBy(factor: number): void;
15
19
  reheat(): void;
16
20
  exportImage(format: "png" | "svg"): string;
17
21
  enterFocus(seedNodeIds: string[], hops: number): void;
18
22
  exitFocus(): void;
19
23
  isFocused(): boolean;
20
24
  getFocusInfo(): FocusInfo | null;
25
+ /** Get all node IDs in the current layout (subgraph if focused, full graph otherwise). Seed nodes first. */
26
+ getNodeIds(): string[];
21
27
  destroy(): void;
22
28
  };