backpack-viewer 0.1.3 → 0.2.5
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 +33 -72
- package/bin/serve.js +86 -9
- package/dist/assets/index-BwXh5IUT.js +1 -0
- package/dist/assets/index-DQfh3jIv.css +1 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +6 -9
- package/src/api.ts +0 -13
- package/src/canvas.ts +0 -409
- package/src/colors.ts +0 -40
- package/src/info-panel.ts +0 -230
- package/src/layout.ts +0 -138
- package/src/main.ts +0 -68
- package/src/sidebar.ts +0 -80
- package/src/style.css +0 -337
- package/tsconfig.json +0 -15
- package/tsconfig.node.json +0 -13
- package/vite.config.ts +0 -75
package/README.md
CHANGED
|
@@ -1,94 +1,55 @@
|
|
|
1
|
-
# Backpack
|
|
1
|
+
# Backpack Viewer
|
|
2
2
|
|
|
3
|
-
A web-based
|
|
3
|
+
**See your knowledge graph.** A web-based visualizer for [Backpack](https://www.npmjs.com/package/backpack-ontology) ontologies with force-directed layout, interactive navigation, and live reload.
|
|
4
4
|
|
|
5
|
-
## Quick
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
# Build the ontology engine first (required — viewer depends on it)
|
|
9
|
-
cd ../backpack-ontology && npm run build
|
|
10
|
-
|
|
11
|
-
# Install and start the viewer
|
|
12
|
-
cd ../backpack-viewer
|
|
13
|
-
npm install
|
|
14
|
-
npm run dev
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Open [http://localhost:5173](http://localhost:5173). The sidebar lists all ontologies stored by backpack-ontology. Click one to visualize it.
|
|
7
|
+
Tell Claude:
|
|
18
8
|
|
|
19
|
-
|
|
9
|
+
> "Show me my knowledge graph"
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
Or run it directly:
|
|
22
12
|
|
|
13
|
+
```bash
|
|
14
|
+
npx backpack-viewer
|
|
23
15
|
```
|
|
24
|
-
Claude (MCP tools) ──writes──> StorageBackend ──persists──> ontology data
|
|
25
|
-
│
|
|
26
|
-
Viewer (Vite plugin) ──reads via─────┘
|
|
27
|
-
│
|
|
28
|
-
HTTP API ──> Browser ──> Canvas 2D
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The viewer connects to backpack-ontology through the `StorageBackend` interface — the same abstraction the engine uses for persistence. It calls two methods:
|
|
32
|
-
|
|
33
|
-
- `listOntologies()` — returns names, descriptions, and counts
|
|
34
|
-
- `loadOntology(name)` — returns the full graph (nodes + edges)
|
|
35
16
|
|
|
36
|
-
|
|
17
|
+
Opens http://localhost:5173. Click any ontology in the sidebar to visualize it.
|
|
37
18
|
|
|
38
|
-
|
|
19
|
+
## Features
|
|
39
20
|
|
|
40
|
-
|
|
21
|
+
- **Live reload**: add knowledge via Claude and watch it appear in real time
|
|
22
|
+
- **Pan and zoom**: click-drag to pan, scroll to zoom
|
|
23
|
+
- **Inspect**: click any item to see its properties, connections, and metadata
|
|
24
|
+
- **Edit**: rename ontologies, edit node types and properties, add or remove items inline
|
|
25
|
+
- **Search**: filter ontologies by name in the sidebar
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
## How it works
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
- **Nodes**: Colored circles. Colors are deterministic by node type (hash → palette). Label below, type badge above.
|
|
46
|
-
- **Edges**: Straight lines with arrowheads. Edge type label at midpoint. Self-loops rendered as small circles.
|
|
47
|
-
- **Navigation**: Mouse drag to pan. Scroll wheel to zoom. Trackpad pinch to zoom. Touch drag/pinch on mobile.
|
|
48
|
-
- **Node inspection**: Click any node to open a detail panel showing all properties, connections, and timestamps. Selected nodes glow and highlight their connected edges. Non-connected nodes dim to focus attention.
|
|
29
|
+
The viewer reads ontology data from the same local files that the MCP server writes to. Changes appear automatically, no refresh needed.
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **Colors**: Deterministic hash of `node.type` into a 16-color palette — no hardcoded type lists
|
|
56
|
-
- **Properties**: Iterated dynamically, never assumed to have specific keys
|
|
57
|
-
- **Edge cases**: Self-loops, multiple edges between same pair, nodes with no string properties, empty edge properties
|
|
58
|
-
|
|
59
|
-
## API Endpoints
|
|
60
|
-
|
|
61
|
-
The Vite dev server exposes two endpoints (served by the ontology-api plugin):
|
|
31
|
+
```
|
|
32
|
+
backpack-ontology (MCP) ──writes──> ~/.local/share/backpack/ontologies/
|
|
33
|
+
│
|
|
34
|
+
backpack-viewer ──reads──────────────────┘
|
|
35
|
+
```
|
|
62
36
|
|
|
63
|
-
|
|
64
|
-
|----------|---------|
|
|
65
|
-
| `GET /api/ontologies` | `OntologySummary[]` — name, description, nodeCount, edgeCount |
|
|
66
|
-
| `GET /api/ontologies/:name` | `OntologyData` — full graph with all nodes and edges |
|
|
37
|
+
## Reference
|
|
67
38
|
|
|
68
|
-
|
|
39
|
+
| Variable | Effect |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `PORT` | Override the default port (default: `5173`) |
|
|
42
|
+
| `XDG_DATA_HOME` | Override data location (default: `~/.local/share`) |
|
|
43
|
+
| `BACKPACK_DIR` | Override data directory |
|
|
69
44
|
|
|
70
|
-
|
|
71
|
-
backpack-viewer/
|
|
72
|
-
├── vite.config.ts # Vite plugin: StorageBackend → HTTP API + file watcher
|
|
73
|
-
├── index.html # Single page shell
|
|
74
|
-
└── src/
|
|
75
|
-
├── main.ts # Entry point, wires sidebar + canvas + live reload
|
|
76
|
-
├── api.ts # fetch() wrappers returning backpack-ontology types
|
|
77
|
-
├── sidebar.ts # Ontology list with text filter
|
|
78
|
-
├── canvas.ts # Canvas 2D rendering + pan/zoom/pinch + node selection
|
|
79
|
-
├── info-panel.ts # Node detail panel (properties, connections, timestamps)
|
|
80
|
-
├── layout.ts # Force-directed graph layout algorithm
|
|
81
|
-
├── colors.ts # Deterministic type → color mapping
|
|
82
|
-
└── style.css # Dark theme
|
|
83
|
-
```
|
|
45
|
+
## Support
|
|
84
46
|
|
|
85
|
-
|
|
47
|
+
Questions, feedback, or partnership inquiries: **support@backpackontology.com**
|
|
86
48
|
|
|
87
|
-
|
|
88
|
-
- **Dev**: `vite`, `typescript`
|
|
49
|
+
## Privacy
|
|
89
50
|
|
|
90
|
-
|
|
51
|
+
See the [Privacy Policy](https://github.com/noahirzinger/backpack-ontology/blob/main/PRIVACY.md). The viewer itself collects no data.
|
|
91
52
|
|
|
92
53
|
## License
|
|
93
54
|
|
|
94
|
-
Apache
|
|
55
|
+
Licensed under the [Apache License, Version 2.0](./LICENSE).
|
package/bin/serve.js
CHANGED
|
@@ -1,19 +1,96 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createServer } from "vite";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import http from "node:http";
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const root = path.resolve(__dirname, "..");
|
|
9
|
-
|
|
10
|
+
const distDir = path.resolve(root, "dist");
|
|
10
11
|
const port = parseInt(process.env.PORT || "5173", 10);
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
13
|
+
const hasDistBuild = fs.existsSync(path.join(distDir, "index.html"));
|
|
14
|
+
|
|
15
|
+
if (hasDistBuild) {
|
|
16
|
+
// --- Production: static file server + API (zero native deps) ---
|
|
17
|
+
const { JsonFileBackend, dataDir } = await import("backpack-ontology");
|
|
18
|
+
|
|
19
|
+
const storage = new JsonFileBackend();
|
|
20
|
+
await storage.initialize();
|
|
21
|
+
|
|
22
|
+
const MIME_TYPES = {
|
|
23
|
+
".html": "text/html",
|
|
24
|
+
".js": "application/javascript",
|
|
25
|
+
".css": "text/css",
|
|
26
|
+
".json": "application/json",
|
|
27
|
+
".svg": "image/svg+xml",
|
|
28
|
+
".png": "image/png",
|
|
29
|
+
".ico": "image/x-icon",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const server = http.createServer(async (req, res) => {
|
|
33
|
+
const url = req.url?.replace(/\?.*$/, "") || "/";
|
|
34
|
+
|
|
35
|
+
// --- API routes ---
|
|
36
|
+
if (url === "/api/ontologies") {
|
|
37
|
+
try {
|
|
38
|
+
const summaries = await storage.listOntologies();
|
|
39
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify(summaries));
|
|
41
|
+
} catch {
|
|
42
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
43
|
+
res.end("[]");
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (url.startsWith("/api/ontologies/")) {
|
|
49
|
+
const name = decodeURIComponent(url.replace("/api/ontologies/", ""));
|
|
50
|
+
try {
|
|
51
|
+
const data = await storage.loadOntology(name);
|
|
52
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify(data));
|
|
54
|
+
} catch {
|
|
55
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
56
|
+
res.end(JSON.stringify({ error: "Ontology not found" }));
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Static files ---
|
|
62
|
+
let filePath = path.join(distDir, url === "/" ? "index.html" : url);
|
|
63
|
+
|
|
64
|
+
// SPA fallback: serve index.html for non-file routes
|
|
65
|
+
if (!fs.existsSync(filePath)) {
|
|
66
|
+
filePath = path.join(distDir, "index.html");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const data = fs.readFileSync(filePath);
|
|
71
|
+
const ext = path.extname(filePath);
|
|
72
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
73
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
74
|
+
res.end(data);
|
|
75
|
+
} catch {
|
|
76
|
+
res.writeHead(404);
|
|
77
|
+
res.end("Not found");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
server.listen(port, () => {
|
|
82
|
+
console.log(` Backpack Viewer running at http://localhost:${port}/`);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
// --- Development: use Vite for HMR + TypeScript compilation ---
|
|
86
|
+
const { createServer } = await import("vite");
|
|
87
|
+
|
|
88
|
+
const server = await createServer({
|
|
89
|
+
root,
|
|
90
|
+
configFile: path.resolve(root, "vite.config.ts"),
|
|
91
|
+
server: { port, open: true },
|
|
92
|
+
});
|
|
17
93
|
|
|
18
|
-
await server.listen();
|
|
19
|
-
server.printUrls();
|
|
94
|
+
await server.listen();
|
|
95
|
+
server.printUrls();
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const p of document.querySelectorAll('link[rel="modulepreload"]'))n(p);new MutationObserver(p=>{for(const e of p)if(e.type==="childList")for(const c of e.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&n(c)}).observe(document,{childList:!0,subtree:!0});function t(p){const e={};return p.integrity&&(e.integrity=p.integrity),p.referrerPolicy&&(e.referrerPolicy=p.referrerPolicy),p.crossOrigin==="use-credentials"?e.credentials="include":p.crossOrigin==="anonymous"?e.credentials="omit":e.credentials="same-origin",e}function n(p){if(p.ep)return;p.ep=!0;const e=t(p);fetch(p.href,e)}})();async function ne(){const a=await fetch("/api/ontologies");return a.ok?a.json():[]}async function oe(a){const o=await fetch(`/api/ontologies/${encodeURIComponent(a)}`);if(!o.ok)throw new Error(`Failed to load ontology: ${a}`);return o.json()}async function be(a,o){if(!(await fetch(`/api/ontologies/${encodeURIComponent(a)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).ok)throw new Error(`Failed to save ontology: ${a}`)}async function Ne(a,o){if(!(await fetch(`/api/ontologies/${encodeURIComponent(a)}/rename`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:o})})).ok)throw new Error(`Failed to rename ontology: ${a}`)}function we(a,o){const t=typeof o=="function"?{onSelect:o}:o,n=document.createElement("h2");n.textContent="Backpack Ontology Viewer";const p=document.createElement("input");p.type="text",p.placeholder="Filter...",p.id="filter";const e=document.createElement("ul");e.id="ontology-list";const c=document.createElement("div");c.className="sidebar-footer",c.innerHTML='<a href="mailto:support@backpackontology.com">support@backpackontology.com</a><span>Feedback & support</span>',a.appendChild(n),a.appendChild(p),a.appendChild(e),a.appendChild(c);let y=[],i="";return p.addEventListener("input",()=>{const u=p.value.toLowerCase();for(const d of y){const s=d.dataset.name??"";d.style.display=s.includes(u)?"":"none"}}),{setSummaries(u){e.innerHTML="",y=u.map(d=>{const s=document.createElement("li");s.className="ontology-item",s.dataset.name=d.name;const r=document.createElement("span");r.className="name",r.textContent=d.name;const N=document.createElement("span");if(N.className="stats",N.textContent=`${d.nodeCount} nodes, ${d.edgeCount} edges`,s.appendChild(r),s.appendChild(N),t.onRename){const x=document.createElement("button");x.className="sidebar-edit-btn",x.textContent="✎",x.title="Rename";const Y=t.onRename;x.addEventListener("click",w=>{w.stopPropagation();const I=document.createElement("input");I.type="text",I.className="sidebar-rename-input",I.value=d.name,r.textContent="",r.appendChild(I),x.style.display="none",I.focus(),I.select();const C=()=>{const f=I.value.trim();f&&f!==d.name?Y(d.name,f):(r.textContent=d.name,x.style.display="")};I.addEventListener("blur",C),I.addEventListener("keydown",f=>{f.key==="Enter"&&I.blur(),f.key==="Escape"&&(I.value=d.name,I.blur())})}),s.appendChild(x)}return s.addEventListener("click",()=>t.onSelect(d.name)),e.appendChild(s),s}),i&&this.setActive(i)},setActive(u){i=u;for(const d of y)d.classList.toggle("active",d.dataset.name===u)}}}const Le=5e3,Se=.005,he=150,de=.9,le=.01,re=30,se=50;function Me(a,o){for(const t of Object.values(a))if(typeof t=="string")return t;return o}function Ie(a){const o=Math.sqrt(a.nodes.length)*he*.5,t=new Map,n=a.nodes.map((e,c)=>{const y=2*Math.PI*c/a.nodes.length,i={id:e.id,x:Math.cos(y)*o,y:Math.sin(y)*o,vx:0,vy:0,label:Me(e.properties,e.id),type:e.type};return t.set(e.id,i),i}),p=a.edges.map(e=>({sourceId:e.sourceId,targetId:e.targetId,type:e.type}));return{nodes:n,edges:p,nodeMap:t}}function Te(a,o){const{nodes:t,edges:n,nodeMap:p}=a;for(let e=0;e<t.length;e++)for(let c=e+1;c<t.length;c++){const y=t[e],i=t[c];let u=i.x-y.x,d=i.y-y.y,s=Math.sqrt(u*u+d*d);s<re&&(s=re);const r=Le*o/(s*s),N=u/s*r,x=d/s*r;y.vx-=N,y.vy-=x,i.vx+=N,i.vy+=x}for(const e of n){const c=p.get(e.sourceId),y=p.get(e.targetId);if(!c||!y)continue;const i=y.x-c.x,u=y.y-c.y,d=Math.sqrt(i*i+u*u);if(d===0)continue;const s=Se*(d-he)*o,r=i/d*s,N=u/d*s;c.vx+=r,c.vy+=N,y.vx-=r,y.vy-=N}for(const e of t)e.vx-=e.x*le*o,e.vy-=e.y*le*o;for(const e of t){e.vx*=de,e.vy*=de;const c=Math.sqrt(e.vx*e.vx+e.vy*e.vy);c>se&&(e.vx=e.vx/c*se,e.vy=e.vy/c*se),e.x+=e.vx,e.y+=e.vy}return o*.995}const pe=["#d4a27f","#c17856","#b07a5e","#d4956b","#a67c5a","#cc9e7c","#c4866a","#cb8e6c","#b8956e","#a88a70","#d9b08c","#c4a882","#e8b898","#b5927a","#a8886e","#d1a990"],ue=new Map;function G(a){const o=ue.get(a);if(o)return o;let t=0;for(let p=0;p<a.length;p++)t=(t<<5)-t+a.charCodeAt(p)|0;const n=pe[Math.abs(t)%pe.length];return ue.set(a,n),n}function X(a){return getComputedStyle(document.documentElement).getPropertyValue(a).trim()}const K=20,Ae=.001;function ke(a,o){const t=a.querySelector("canvas"),n=t.getContext("2d"),p=window.devicePixelRatio||1;let e={x:0,y:0,scale:1},c=null,y=1,i=0,u=new Set,d=null,s=null,r=null;const N=300;function x(){t.width=t.clientWidth*p,t.height=t.clientHeight*p,C()}const Y=new ResizeObserver(x);Y.observe(a),x();function w(m,l){return[m/e.scale+e.x,l/e.scale+e.y]}function I(m,l){if(!c)return null;const[v,S]=w(m,l);for(let E=c.nodes.length-1;E>=0;E--){const B=c.nodes[E],R=v-B.x,H=S-B.y;if(R*R+H*H<=K*K)return B}return null}function C(){if(!c){n.clearRect(0,0,t.width,t.height);return}const m=X("--canvas-edge"),l=X("--canvas-edge-highlight"),v=X("--canvas-edge-dim"),S=X("--canvas-edge-label"),E=X("--canvas-edge-label-highlight"),B=X("--canvas-edge-label-dim"),R=X("--canvas-arrow"),H=X("--canvas-arrow-highlight"),z=X("--canvas-node-label"),F=X("--canvas-node-label-dim"),_=X("--canvas-type-badge"),ge=X("--canvas-type-badge-dim"),Ce=X("--canvas-selection-border"),ve=X("--canvas-node-border");n.save(),n.setTransform(p,0,0,p,0,0),n.clearRect(0,0,t.clientWidth,t.clientHeight),n.save(),n.translate(-e.x*e.scale,-e.y*e.scale),n.scale(e.scale,e.scale);for(const A of c.edges){const q=c.nodeMap.get(A.sourceId),W=c.nodeMap.get(A.targetId);if(!q||!W)continue;const te=d===null||d.has(A.sourceId),Q=d===null||d.has(A.targetId),J=te&&Q;if(d!==null&&!te&&!Q)continue;const U=u.size>0&&(u.has(A.sourceId)||u.has(A.targetId))||d!==null&&J,ie=d!==null&&!J;if(A.sourceId===A.targetId){L(q,A.type,U,m,l,S,E);continue}n.beginPath(),n.moveTo(q.x,q.y),n.lineTo(W.x,W.y),n.strokeStyle=U?l:ie?v:m,n.lineWidth=U?2.5:1.5,n.stroke(),f(q.x,q.y,W.x,W.y,U,R,H);const Ee=(q.x+W.x)/2,xe=(q.y+W.y)/2;n.fillStyle=U?E:ie?B:S,n.font="9px system-ui, sans-serif",n.textAlign="center",n.textBaseline="bottom",n.fillText(A.type,Ee,xe-4)}for(const A of c.nodes){const q=G(A.type),W=u.has(A.id),te=u.size>0&&c.edges.some(U=>u.has(U.sourceId)&&U.targetId===A.id||u.has(U.targetId)&&U.sourceId===A.id),Q=d!==null&&!d.has(A.id),J=Q||u.size>0&&!W&&!te;W&&(n.save(),n.shadowColor=q,n.shadowBlur=20,n.beginPath(),n.arc(A.x,A.y,K+3,0,Math.PI*2),n.fillStyle=q,n.globalAlpha=.3,n.fill(),n.restore()),n.beginPath(),n.arc(A.x,A.y,K,0,Math.PI*2),n.fillStyle=q,n.globalAlpha=Q?.1:J?.3:1,n.fill(),n.strokeStyle=W?Ce:ve,n.lineWidth=W?3:1.5,n.stroke();const ce=A.label.length>24?A.label.slice(0,22)+"...":A.label;n.fillStyle=J?F:z,n.font="11px system-ui, sans-serif",n.textAlign="center",n.textBaseline="top",n.fillText(ce,A.x,A.y+K+4),n.fillStyle=J?ge:_,n.font="9px system-ui, sans-serif",n.textBaseline="bottom",n.fillText(A.type,A.x,A.y-K-3),n.globalAlpha=1}n.restore(),n.restore()}function f(m,l,v,S,E,B,R){const H=Math.atan2(S-l,v-m),z=v-Math.cos(H)*K,F=S-Math.sin(H)*K,_=8;n.beginPath(),n.moveTo(z,F),n.lineTo(z-_*Math.cos(H-.4),F-_*Math.sin(H-.4)),n.lineTo(z-_*Math.cos(H+.4),F-_*Math.sin(H+.4)),n.closePath(),n.fillStyle=E?R:B,n.fill()}function L(m,l,v,S,E,B,R){const H=m.x+K+15,z=m.y-K-15;n.beginPath(),n.arc(H,z,15,0,Math.PI*2),n.strokeStyle=v?E:S,n.lineWidth=v?2.5:1.5,n.stroke(),n.fillStyle=v?R:B,n.font="9px system-ui, sans-serif",n.textAlign="center",n.fillText(l,H,z-18)}function k(){if(!s||!r)return;const m=performance.now()-r.time,l=Math.min(m/N,1),v=1-Math.pow(1-l,3);e.x=r.x+(s.x-r.x)*v,e.y=r.y+(s.y-r.y)*v,C(),l<1?requestAnimationFrame(k):(s=null,r=null)}function $(){!c||y<Ae||(y=Te(c,y),C(),i=requestAnimationFrame($))}let g=!1,M=!1,h=0,O=0;t.addEventListener("mousedown",m=>{g=!0,M=!1,h=m.clientX,O=m.clientY}),t.addEventListener("mousemove",m=>{if(!g)return;const l=m.clientX-h,v=m.clientY-O;(Math.abs(l)>2||Math.abs(v)>2)&&(M=!0),e.x-=l/e.scale,e.y-=v/e.scale,h=m.clientX,O=m.clientY,C()}),t.addEventListener("mouseup",m=>{if(g=!1,M)return;const l=t.getBoundingClientRect(),v=m.clientX-l.left,S=m.clientY-l.top,E=I(v,S),B=m.ctrlKey||m.metaKey;if(E){B?u.has(E.id)?u.delete(E.id):u.add(E.id):u.size===1&&u.has(E.id)?u.clear():(u.clear(),u.add(E.id));const R=[...u];o==null||o(R.length>0?R:null)}else u.clear(),o==null||o(null);C()}),t.addEventListener("mouseleave",()=>{g=!1}),t.addEventListener("wheel",m=>{m.preventDefault();const l=t.getBoundingClientRect(),v=m.clientX-l.left,S=m.clientY-l.top,[E,B]=w(v,S),R=m.ctrlKey?1-m.deltaY*.01:m.deltaY>0?.9:1.1;e.scale=Math.max(.05,Math.min(10,e.scale*R)),e.x=E-v/e.scale,e.y=B-S/e.scale,C()},{passive:!1});let T=[],D=0,j=1;t.addEventListener("touchstart",m=>{m.preventDefault(),T=Array.from(m.touches),T.length===2?(D=P(T[0],T[1]),j=e.scale):T.length===1&&(h=T[0].clientX,O=T[0].clientY)},{passive:!1}),t.addEventListener("touchmove",m=>{m.preventDefault();const l=Array.from(m.touches);if(l.length===2&&T.length===2){const S=P(l[0],l[1])/D;e.scale=Math.max(.05,Math.min(10,j*S)),C()}else if(l.length===1){const v=l[0].clientX-h,S=l[0].clientY-O;e.x-=v/e.scale,e.y-=S/e.scale,h=l[0].clientX,O=l[0].clientY,C()}T=l},{passive:!1});function P(m,l){const v=m.clientX-l.clientX,S=m.clientY-l.clientY;return Math.sqrt(v*v+S*S)}return{loadGraph(m){if(cancelAnimationFrame(i),c=Ie(m),y=1,u=new Set,d=null,e={x:0,y:0,scale:1},c.nodes.length>0){let l=1/0,v=1/0,S=-1/0,E=-1/0;for(const F of c.nodes)F.x<l&&(l=F.x),F.y<v&&(v=F.y),F.x>S&&(S=F.x),F.y>E&&(E=F.y);const B=(l+S)/2,R=(v+E)/2,H=t.clientWidth,z=t.clientHeight;e.x=B-H/2,e.y=R-z/2}$()},setFilteredNodeIds(m){d=m,C()},panToNode(m){if(!c)return;const l=c.nodeMap.get(m);if(!l)return;u=new Set([m]),o==null||o([m]);const v=t.clientWidth,S=t.clientHeight;r={x:e.x,y:e.y,time:performance.now()},s={x:l.x-v/(2*e.scale),y:l.y-S/(2*e.scale)},k()},destroy(){cancelAnimationFrame(i),Y.disconnect()}}}function Z(a){for(const o of Object.values(a.properties))if(typeof o=="string")return o;return a.id}const Oe="✎";function Pe(a,o){const t=document.createElement("div");t.id="info-panel",t.className="info-panel hidden",a.appendChild(t);function n(){t.classList.add("hidden"),t.innerHTML=""}function p(c,y){const i=y.nodes.find(g=>g.id===c);if(!i)return;const u=y.edges.filter(g=>g.sourceId===c||g.targetId===c);t.innerHTML="",t.classList.remove("hidden");const d=document.createElement("button");d.className="info-close",d.textContent="×",d.addEventListener("click",n),t.appendChild(d);const s=document.createElement("div");s.className="info-header";const r=document.createElement("span");if(r.className="info-type-badge",r.textContent=i.type,r.style.backgroundColor=G(i.type),o){r.classList.add("info-editable");const g=document.createElement("button");g.className="info-inline-edit",g.textContent=Oe,g.addEventListener("click",M=>{M.stopPropagation();const h=document.createElement("input");h.type="text",h.className="info-edit-inline-input",h.value=i.type,r.textContent="",r.appendChild(h),h.focus(),h.select();const O=()=>{const T=h.value.trim();T&&T!==i.type?o.onChangeNodeType(c,T):(r.textContent=i.type,r.appendChild(g))};h.addEventListener("blur",O),h.addEventListener("keydown",T=>{T.key==="Enter"&&h.blur(),T.key==="Escape"&&(h.value=i.type,h.blur())})}),r.appendChild(g)}const N=document.createElement("h3");N.className="info-label",N.textContent=Z(i);const x=document.createElement("span");x.className="info-id",x.textContent=i.id,s.appendChild(r),s.appendChild(N),s.appendChild(x),t.appendChild(s);const Y=Object.keys(i.properties),w=ee("Properties");if(Y.length>0){const g=document.createElement("dl");g.className="info-props";for(const M of Y){const h=document.createElement("dt");h.textContent=M;const O=document.createElement("dd");if(o){const T=ae(i.properties[M]),D=document.createElement("input");D.type="text",D.className="info-edit-input",D.value=T,D.addEventListener("keydown",P=>{P.key==="Enter"&&D.blur()}),D.addEventListener("blur",()=>{const P=D.value;P!==T&&o.onUpdateNode(c,{[M]:Be(P)})}),O.appendChild(D);const j=document.createElement("button");j.className="info-delete-prop",j.textContent="×",j.title=`Remove ${M}`,j.addEventListener("click",()=>{const P={...i.properties};delete P[M],o.onUpdateNode(c,P)}),O.appendChild(j)}else O.appendChild(De(i.properties[M]));g.appendChild(h),g.appendChild(O)}w.appendChild(g)}if(o){const g=document.createElement("button");g.className="info-add-btn",g.textContent="+ Add property",g.addEventListener("click",()=>{const M=document.createElement("div");M.className="info-add-row";const h=document.createElement("input");h.type="text",h.className="info-edit-input",h.placeholder="key";const O=document.createElement("input");O.type="text",O.className="info-edit-input",O.placeholder="value";const T=document.createElement("button");T.className="info-add-save",T.textContent="Add",T.addEventListener("click",()=>{h.value&&o.onAddProperty(c,h.value,O.value)}),M.appendChild(h),M.appendChild(O),M.appendChild(T),w.appendChild(M),h.focus()}),w.appendChild(g)}if(t.appendChild(w),u.length>0){const g=ee(`Connections (${u.length})`),M=document.createElement("ul");M.className="info-connections";for(const h of u){const O=h.sourceId===c,T=O?h.targetId:h.sourceId,D=y.nodes.find(E=>E.id===T),j=D?Z(D):T,P=document.createElement("li");if(P.className="info-connection",D){const E=document.createElement("span");E.className="info-target-dot",E.style.backgroundColor=G(D.type),P.appendChild(E)}const m=document.createElement("span");m.className="info-arrow",m.textContent=O?"→":"←";const l=document.createElement("span");l.className="info-edge-type",l.textContent=h.type;const v=document.createElement("span");v.className="info-target",v.textContent=j,P.appendChild(m),P.appendChild(l),P.appendChild(v);const S=Object.keys(h.properties);if(S.length>0){const E=document.createElement("div");E.className="info-edge-props";for(const B of S){const R=document.createElement("span");R.className="info-edge-prop",R.textContent=`${B}: ${ae(h.properties[B])}`,E.appendChild(R)}P.appendChild(E)}if(o){const E=document.createElement("button");E.className="info-delete-edge",E.textContent="×",E.title="Remove connection",E.addEventListener("click",B=>{B.stopPropagation(),o.onDeleteEdge(h.id)}),P.appendChild(E)}M.appendChild(P)}g.appendChild(M),t.appendChild(g)}const I=ee("Timestamps"),C=document.createElement("dl");C.className="info-props";const f=document.createElement("dt");f.textContent="created";const L=document.createElement("dd");L.textContent=me(i.createdAt);const k=document.createElement("dt");k.textContent="updated";const $=document.createElement("dd");if($.textContent=me(i.updatedAt),C.appendChild(f),C.appendChild(L),C.appendChild(k),C.appendChild($),I.appendChild(C),t.appendChild(I),o){const g=document.createElement("div");g.className="info-section info-danger";const M=document.createElement("button");M.className="info-delete-node",M.textContent="Delete node",M.addEventListener("click",()=>{o.onDeleteNode(c),n()}),g.appendChild(M),t.appendChild(g)}}function e(c,y){const i=new Set(c),u=y.nodes.filter(f=>i.has(f.id));if(u.length===0)return;const d=y.edges.filter(f=>i.has(f.sourceId)&&i.has(f.targetId));t.innerHTML="",t.classList.remove("hidden");const s=document.createElement("button");s.className="info-close",s.textContent="×",s.addEventListener("click",n),t.appendChild(s);const r=document.createElement("div");r.className="info-header";const N=document.createElement("h3");N.className="info-label",N.textContent=`${u.length} nodes selected`,r.appendChild(N);const x=document.createElement("div");x.style.cssText="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px";const Y=new Map;for(const f of u)Y.set(f.type,(Y.get(f.type)??0)+1);for(const[f,L]of Y){const k=document.createElement("span");k.className="info-type-badge",k.style.backgroundColor=G(f),k.textContent=L>1?`${f} (${L})`:f,x.appendChild(k)}r.appendChild(x),t.appendChild(r);const w=ee("Selected Nodes"),I=document.createElement("ul");I.className="info-connections";for(const f of u){const L=document.createElement("li");L.className="info-connection";const k=document.createElement("span");k.className="info-target-dot",k.style.backgroundColor=G(f.type);const $=document.createElement("span");$.className="info-target",$.textContent=Z(f);const g=document.createElement("span");g.className="info-edge-type",g.textContent=f.type,L.appendChild(k),L.appendChild($),L.appendChild(g),I.appendChild(L)}w.appendChild(I),t.appendChild(w);const C=ee(d.length>0?`Connections Between Selected (${d.length})`:"Connections Between Selected");if(d.length===0){const f=document.createElement("p");f.style.cssText="font-size:12px;color:var(--text-dim)",f.textContent="No direct connections between selected nodes",C.appendChild(f)}else{const f=document.createElement("ul");f.className="info-connections";for(const L of d){const k=y.nodes.find(l=>l.id===L.sourceId),$=y.nodes.find(l=>l.id===L.targetId),g=k?Z(k):L.sourceId,M=$?Z($):L.targetId,h=document.createElement("li");if(h.className="info-connection",k){const l=document.createElement("span");l.className="info-target-dot",l.style.backgroundColor=G(k.type),h.appendChild(l)}const O=document.createElement("span");O.className="info-target",O.textContent=g;const T=document.createElement("span");T.className="info-arrow",T.textContent="→";const D=document.createElement("span");D.className="info-edge-type",D.textContent=L.type;const j=document.createElement("span");if(j.className="info-arrow",j.textContent="→",h.appendChild(O),h.appendChild(T),h.appendChild(D),h.appendChild(j),$){const l=document.createElement("span");l.className="info-target-dot",l.style.backgroundColor=G($.type),h.appendChild(l)}const P=document.createElement("span");P.className="info-target",P.textContent=M,h.appendChild(P);const m=Object.keys(L.properties);if(m.length>0){const l=document.createElement("div");l.className="info-edge-props";for(const v of m){const S=document.createElement("span");S.className="info-edge-prop",S.textContent=`${v}: ${ae(L.properties[v])}`,l.appendChild(S)}h.appendChild(l)}f.appendChild(h)}C.appendChild(f)}t.appendChild(C)}return{show(c,y){c.length===1?p(c[0],y):c.length>1&&e(c,y)},hide:n,get visible(){return!t.classList.contains("hidden")}}}function ee(a){const o=document.createElement("div");o.className="info-section";const t=document.createElement("h4");return t.className="info-section-title",t.textContent=a,o.appendChild(t),o}function De(a){if(Array.isArray(a)){const t=document.createElement("div");t.className="info-array";for(const n of a){const p=document.createElement("span");p.className="info-tag",p.textContent=String(n),t.appendChild(p)}return t}if(a!==null&&typeof a=="object"){const t=document.createElement("pre");return t.className="info-json",t.textContent=JSON.stringify(a,null,2),t}const o=document.createElement("span");return o.className="info-value",o.textContent=String(a??""),o}function ae(a){return Array.isArray(a)?a.map(String).join(", "):a!==null&&typeof a=="object"?JSON.stringify(a):String(a??"")}function Be(a){const o=a.trim();if(o==="true")return!0;if(o==="false")return!1;if(o!==""&&!isNaN(Number(o)))return Number(o);if(o.startsWith("[")&&o.endsWith("]")||o.startsWith("{")&&o.endsWith("}"))try{return JSON.parse(o)}catch{return a}return a}function me(a){try{return new Date(a).toLocaleString()}catch{return a}}function ye(a){for(const o of Object.values(a.properties))if(typeof o=="string")return o;return a.id}function fe(a,o){const t=o.toLowerCase();if(ye(a).toLowerCase().includes(t)||a.type.toLowerCase().includes(t))return!0;for(const n of Object.values(a.properties))if(typeof n=="string"&&n.toLowerCase().includes(t))return!0;return!1}function Re(a){let o=null,t=null,n=null,p=new Set,e=null;const c=document.createElement("div");c.className="search-overlay hidden";const y=document.createElement("div");y.className="search-input-wrap";const i=document.createElement("input");i.className="search-input",i.type="text",i.placeholder="Search nodes...",i.setAttribute("autocomplete","off"),i.setAttribute("spellcheck","false");const u=document.createElement("kbd");u.className="search-kbd",u.textContent="/",y.appendChild(i),y.appendChild(u);const d=document.createElement("ul");d.className="search-results hidden";const s=document.createElement("div");s.className="type-chips",c.appendChild(y),c.appendChild(d),c.appendChild(s),a.appendChild(c);function r(){if(s.innerHTML="",!o)return;const w=new Map;for(const C of o.nodes)w.set(C.type,(w.get(C.type)??0)+1);const I=[...w.keys()].sort();p=new Set;for(const C of I){const f=document.createElement("button");f.className="type-chip",f.dataset.type=C;const L=document.createElement("span");L.className="type-chip-dot",L.style.backgroundColor=G(C);const k=document.createElement("span");k.textContent=`${C} (${w.get(C)})`,f.appendChild(L),f.appendChild(k),f.addEventListener("click",()=>{p.has(C)?(p.delete(C),f.classList.remove("active")):(p.add(C),f.classList.add("active")),x()}),s.appendChild(f)}}function N(){if(!o)return null;const w=i.value.trim(),I=p.size===0,C=w.length===0;if(C&&I)return null;const f=new Set;for(const L of o.nodes)!I&&!p.has(L.type)||(C||fe(L,w))&&f.add(L.id);return f}function x(){const w=N();t==null||t(w),Y()}function Y(){d.innerHTML="";const w=i.value.trim();if(!o||w.length===0){d.classList.add("hidden");return}const I=p.size===0,C=[];for(const f of o.nodes)if(!(!I&&!p.has(f.type))&&fe(f,w)&&(C.push(f),C.length>=8))break;if(C.length===0){d.classList.add("hidden");return}for(const f of C){const L=document.createElement("li");L.className="search-result-item";const k=document.createElement("span");k.className="search-result-dot",k.style.backgroundColor=G(f.type);const $=document.createElement("span");$.className="search-result-label";const g=ye(f);$.textContent=g.length>36?g.slice(0,34)+"...":g;const M=document.createElement("span");M.className="search-result-type",M.textContent=f.type,L.appendChild(k),L.appendChild($),L.appendChild(M),L.addEventListener("click",()=>{n==null||n(f.id),i.value="",d.classList.add("hidden"),x()}),d.appendChild(L)}d.classList.remove("hidden")}return i.addEventListener("input",()=>{e&&clearTimeout(e),e=setTimeout(x,150)}),i.addEventListener("keydown",w=>{if(w.key==="Escape")i.value="",i.blur(),d.classList.add("hidden"),x();else if(w.key==="Enter"){const I=d.querySelector(".search-result-item");I==null||I.click()}}),document.addEventListener("click",w=>{c.contains(w.target)||d.classList.add("hidden")}),i.addEventListener("focus",()=>u.classList.add("hidden")),i.addEventListener("blur",()=>{i.value.length===0&&u.classList.remove("hidden")}),{setOntologyData(w){o=w,i.value="",d.classList.add("hidden"),o&&o.nodes.length>0?(c.classList.remove("hidden"),r()):c.classList.add("hidden")},onFilterChange(w){t=w},onNodeSelect(w){n=w},clear(){i.value="",d.classList.add("hidden"),p.clear(),t==null||t(null)},focus(){i.focus()}}}let V="",b=null;async function $e(){const a=document.getElementById("canvas-container"),o=window.matchMedia("(prefers-color-scheme: dark)"),n=localStorage.getItem("backpack-theme")??(o.matches?"dark":"light");document.documentElement.setAttribute("data-theme",n);const p=document.createElement("button");p.className="theme-toggle",p.textContent=n==="light"?"☾":"☼",p.title="Toggle light/dark mode",p.addEventListener("click",()=>{const r=document.documentElement.getAttribute("data-theme")==="light"?"dark":"light";document.documentElement.setAttribute("data-theme",r),localStorage.setItem("backpack-theme",r),p.textContent=r==="light"?"☾":"☼"}),a.appendChild(p);async function e(){if(!V||!b)return;b.metadata.updatedAt=new Date().toISOString(),await be(V,b),y.loadGraph(b),i.setOntologyData(b);const s=await ne();u.setSummaries(s)}const c=Pe(a,{onUpdateNode(s,r){if(!b)return;const N=b.nodes.find(x=>x.id===s);N&&(N.properties={...N.properties,...r},N.updatedAt=new Date().toISOString(),e().then(()=>c.show([s],b)))},onChangeNodeType(s,r){if(!b)return;const N=b.nodes.find(x=>x.id===s);N&&(N.type=r,N.updatedAt=new Date().toISOString(),e().then(()=>c.show([s],b)))},onDeleteNode(s){b&&(b.nodes=b.nodes.filter(r=>r.id!==s),b.edges=b.edges.filter(r=>r.sourceId!==s&&r.targetId!==s),e())},onDeleteEdge(s){var N;if(!b)return;const r=(N=b.edges.find(x=>x.id===s))==null?void 0:N.sourceId;b.edges=b.edges.filter(x=>x.id!==s),e().then(()=>{r&&b&&c.show([r],b)})},onAddProperty(s,r,N){if(!b)return;const x=b.nodes.find(Y=>Y.id===s);x&&(x.properties[r]=N,x.updatedAt=new Date().toISOString(),e().then(()=>c.show([s],b)))}}),y=ke(a,s=>{s&&s.length>0&&b?c.show(s,b):c.hide()}),i=Re(a);i.onFilterChange(s=>{y.setFilteredNodeIds(s)}),i.onNodeSelect(s=>{y.panToNode(s),b&&c.show([s],b)});const u=we(document.getElementById("sidebar"),{onSelect:async s=>{V=s,u.setActive(s),c.hide(),i.clear(),b=await oe(s),y.loadGraph(b),i.setOntologyData(b)},onRename:async(s,r)=>{await Ne(s,r),V===s&&(V=r);const N=await ne();u.setSummaries(N),u.setActive(V),V===r&&(b=await oe(r),y.loadGraph(b),i.setOntologyData(b))}}),d=await ne();u.setSummaries(d),d.length>0&&(V=d[0].name,u.setActive(V),b=await oe(V),y.loadGraph(b),i.setOntologyData(b)),document.addEventListener("keydown",s=>{s.target instanceof HTMLInputElement||s.target instanceof HTMLTextAreaElement||(s.key==="/"||s.key==="k"&&(s.metaKey||s.ctrlKey))&&(s.preventDefault(),i.focus())})}$e();
|
|
@@ -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)}.theme-toggle{position:absolute;top:16px;right:16px;z-index:30;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)}#canvas-container{flex:1;position:relative;overflow:hidden}#graph-canvas{position:absolute;top:0;left:0;width:100%;height:100%;cursor:grab}#graph-canvas:active{cursor:grabbing}.search-overlay{position:absolute;top:16px;left:50%;transform:translate(-50%);z-index:20;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;width:380px;max-width:calc(100vw - 340px)}.search-input{width:100%;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{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}.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-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)}.info-panel.hidden{display:none}.info-close{position:absolute;top:12px;right:14px;background:none;border:none;color:var(--text-muted);font-size:20px;cursor:pointer;line-height:1;padding:4px}.info-close:hover{color:var(--text)}.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-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;color:var(--text);flex:1;min-width:0}.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}
|
|
@@ -4,6 +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 Ontology Viewer</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BwXh5IUT.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DQfh3jIv.css">
|
|
7
9
|
</head>
|
|
8
10
|
<body>
|
|
9
11
|
<div id="app">
|
|
@@ -12,6 +14,5 @@
|
|
|
12
14
|
<canvas id="graph-canvas"></canvas>
|
|
13
15
|
</div>
|
|
14
16
|
</div>
|
|
15
|
-
<script type="module" src="/src/main.ts"></script>
|
|
16
17
|
</body>
|
|
17
18
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpack-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Web-based graph visualizer for backpack-ontology — Canvas 2D, force-directed layout, live reload",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Noah Irzinger",
|
|
@@ -10,28 +10,25 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
13
|
-
"
|
|
14
|
-
"index.html",
|
|
15
|
-
"vite.config.ts",
|
|
16
|
-
"tsconfig.json",
|
|
17
|
-
"tsconfig.node.json"
|
|
13
|
+
"dist"
|
|
18
14
|
],
|
|
19
15
|
"scripts": {
|
|
20
16
|
"dev": "vite",
|
|
21
17
|
"build": "tsc && vite build",
|
|
22
18
|
"preview": "vite preview",
|
|
23
19
|
"serve": "node bin/serve.js",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
24
21
|
"release:patch": "npm version patch && git push && git push --tags",
|
|
25
22
|
"release:minor": "npm version minor && git push && git push --tags",
|
|
26
23
|
"release:major": "npm version major && git push && git push --tags"
|
|
27
24
|
},
|
|
28
25
|
"dependencies": {
|
|
29
|
-
"backpack-ontology": "^0.1.3"
|
|
30
|
-
"vite": "^6.0.0"
|
|
26
|
+
"backpack-ontology": "^0.1.3"
|
|
31
27
|
},
|
|
32
28
|
"devDependencies": {
|
|
33
29
|
"@types/node": "^25.5.0",
|
|
34
|
-
"typescript": "^5.8.0"
|
|
30
|
+
"typescript": "^5.8.0",
|
|
31
|
+
"vite": "^6.0.0"
|
|
35
32
|
},
|
|
36
33
|
"engines": {
|
|
37
34
|
"node": ">=18"
|
package/src/api.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { OntologyData, OntologySummary } from "backpack-ontology";
|
|
2
|
-
|
|
3
|
-
export async function listOntologies(): Promise<OntologySummary[]> {
|
|
4
|
-
const res = await fetch("/api/ontologies");
|
|
5
|
-
if (!res.ok) return [];
|
|
6
|
-
return res.json();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function loadOntology(name: string): Promise<OntologyData> {
|
|
10
|
-
const res = await fetch(`/api/ontologies/${encodeURIComponent(name)}`);
|
|
11
|
-
if (!res.ok) throw new Error(`Failed to load ontology: ${name}`);
|
|
12
|
-
return res.json();
|
|
13
|
-
}
|