backpack-viewer 0.1.4 → 0.2.6
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 +27 -28
- 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,55 +1,54 @@
|
|
|
1
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
|
-
##
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npm install -g backpack-viewer
|
|
9
|
-
```
|
|
7
|
+
Tell Claude:
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
> "Show me my knowledge graph"
|
|
10
|
+
|
|
11
|
+
Or run it directly:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
backpack-viewer
|
|
14
|
+
npx backpack-viewer
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Opens
|
|
17
|
+
Opens http://localhost:5173. Click any ontology in the sidebar to visualize it.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## Features
|
|
20
20
|
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **Inspect**:
|
|
24
|
-
- **
|
|
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
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
## How it works
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
The viewer reads ontology data from the same local files that the MCP server writes to. Changes appear automatically, no refresh needed.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
```
|
|
32
|
+
backpack-ontology (MCP) ──writes──> ~/.local/share/backpack/ontologies/
|
|
33
|
+
│
|
|
34
|
+
backpack-viewer ──reads──────────────────┘
|
|
35
|
+
```
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
## Reference
|
|
33
38
|
|
|
34
39
|
| Variable | Effect |
|
|
35
|
-
|
|
40
|
+
|---|---|
|
|
36
41
|
| `PORT` | Override the default port (default: `5173`) |
|
|
37
42
|
| `XDG_DATA_HOME` | Override data location (default: `~/.local/share`) |
|
|
38
43
|
| `BACKPACK_DIR` | Override data directory |
|
|
39
44
|
|
|
40
|
-
##
|
|
45
|
+
## Support
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
Questions, feedback, or partnership inquiries: **support@backpackontology.com**
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
backpack-ontology (MCP) ──writes──> StorageBackend
|
|
46
|
-
│
|
|
47
|
-
backpack-viewer ──reads──────────────────┘
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Support
|
|
49
|
+
## Privacy
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
See the [Privacy Policy](https://github.com/noahirzinger/backpack-ontology/blob/main/PRIVACY.md). The viewer itself collects no data.
|
|
53
52
|
|
|
54
53
|
## License
|
|
55
54
|
|
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.6",
|
|
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.
|
|
30
|
-
"vite": "^6.0.0"
|
|
26
|
+
"backpack-ontology": "^0.2.8"
|
|
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
|
-
}
|