backpack-viewer 0.2.13 → 0.2.14
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/dist/app/assets/index-CR8Iepyw.js +21 -0
- package/dist/app/assets/index-FMdnOuXa.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +6 -0
- package/dist/canvas.js +245 -25
- package/dist/empty-state.d.ts +4 -0
- package/dist/empty-state.js +27 -0
- package/dist/history.d.ts +10 -0
- package/dist/history.js +36 -0
- package/dist/layout.d.ts +8 -1
- package/dist/layout.js +65 -11
- package/dist/main.js +239 -17
- package/dist/shortcuts.d.ts +4 -0
- package/dist/shortcuts.js +66 -0
- package/dist/style.css +471 -17
- package/dist/tools-pane.d.ts +18 -0
- package/dist/tools-pane.js +436 -0
- package/package.json +1 -1
- package/dist/app/assets/index-C1crWHUS.css +0 -1
- package/dist/app/assets/index-DI_1rZKx.js +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const y of document.querySelectorAll('link[rel="modulepreload"]'))e(y);new MutationObserver(y=>{for(const o of y)if(o.type==="childList")for(const s of o.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&e(s)}).observe(document,{childList:!0,subtree:!0});function a(y){const o={};return y.integrity&&(o.integrity=y.integrity),y.referrerPolicy&&(o.referrerPolicy=y.referrerPolicy),y.crossOrigin==="use-credentials"?o.credentials="include":y.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function e(y){if(y.ep)return;y.ep=!0;const o=a(y);fetch(y.href,o)}})();async function Le(){const c=await fetch("/api/ontologies");return c.ok?c.json():[]}async function Se(c){const t=await fetch(`/api/ontologies/${encodeURIComponent(c)}`);if(!t.ok)throw new Error(`Failed to load ontology: ${c}`);return t.json()}async function ke(c,t){if(!(await fetch(`/api/ontologies/${encodeURIComponent(c)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).ok)throw new Error(`Failed to save ontology: ${c}`)}async function Ye(c,t){if(!(await fetch(`/api/ontologies/${encodeURIComponent(c)}/rename`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t})})).ok)throw new Error(`Failed to rename ontology: ${c}`)}function Xe(c,t){const a=typeof t=="function"?{onSelect:t}:t,e=document.createElement("h2");e.textContent="Backpack Viewer";const y=document.createElement("input");y.type="text",y.placeholder="Filter...",y.id="filter";const o=document.createElement("ul");o.id="ontology-list";const s=document.createElement("div");s.className="sidebar-footer",s.innerHTML='<a href="mailto:support@backpackontology.com">support@backpackontology.com</a><span>Feedback & support</span>',c.appendChild(e),c.appendChild(y),c.appendChild(o),c.appendChild(s);let N=[],p="";return y.addEventListener("input",()=>{const g=y.value.toLowerCase();for(const x of N){const I=x.dataset.name??"";x.style.display=I.includes(g)?"":"none"}}),{setSummaries(g){o.innerHTML="",N=g.map(x=>{const I=document.createElement("li");I.className="ontology-item",I.dataset.name=x.name;const T=document.createElement("span");T.className="name",T.textContent=x.name;const B=document.createElement("span");if(B.className="stats",B.textContent=`${x.nodeCount} nodes, ${x.edgeCount} edges`,I.appendChild(T),I.appendChild(B),a.onRename){const z=document.createElement("button");z.className="sidebar-edit-btn",z.textContent="✎",z.title="Rename";const U=a.onRename;z.addEventListener("click",C=>{C.stopPropagation();const w=document.createElement("input");w.type="text",w.className="sidebar-rename-input",w.value=x.name,T.textContent="",T.appendChild(w),z.style.display="none",w.focus(),w.select();const m=()=>{const i=w.value.trim();i&&i!==x.name?U(x.name,i):(T.textContent=x.name,z.style.display="")};w.addEventListener("blur",m),w.addEventListener("keydown",i=>{i.key==="Enter"&&w.blur(),i.key==="Escape"&&(w.value=x.name,w.blur())})}),I.appendChild(z)}return I.addEventListener("click",()=>a.onSelect(x.name)),o.appendChild(I),I}),p&&this.setActive(p)},setActive(g){p=g;for(const x of N)x.classList.toggle("active",x.dataset.name===g)}}}const Fe={clusterStrength:.05,spacing:1},Ue=5e3,We=8e3,je=.005,Be=100,De=250,Me=.9,Te=.01,Ie=30,we=50;let xe={...Fe};function Ge(c){c.clusterStrength!==void 0&&(xe.clusterStrength=c.clusterStrength),c.spacing!==void 0&&(xe.spacing=c.spacing)}function _e(c,t){for(const a of Object.values(c))if(typeof a=="string")return a;return t}function qe(c){const t=new Map,a=[...new Set(c.nodes.map(p=>p.type))],e=Math.sqrt(a.length)*De*.6,y=new Map,o=new Map;for(const p of c.nodes)o.set(p.type,(o.get(p.type)??0)+1);const s=c.nodes.map(p=>{const g=a.indexOf(p.type),x=2*Math.PI*g/Math.max(a.length,1),I=Math.cos(x)*e,T=Math.sin(x)*e,B=y.get(p.type)??0;y.set(p.type,B+1);const z=o.get(p.type)??1,U=2*Math.PI*B/z,C=Be*.6,w={id:p.id,x:I+Math.cos(U)*C,y:T+Math.sin(U)*C,vx:0,vy:0,label:_e(p.properties,p.id),type:p.type};return t.set(p.id,w),w}),N=c.edges.map(p=>({sourceId:p.sourceId,targetId:p.targetId,type:p.type}));return{nodes:s,edges:N,nodeMap:t}}function Ke(c,t){const{nodes:a,edges:e,nodeMap:y}=c;for(let s=0;s<a.length;s++)for(let N=s+1;N<a.length;N++){const p=a[s],g=a[N];let x=g.x-p.x,I=g.y-p.y,T=Math.sqrt(x*x+I*I);T<Ie&&(T=Ie);const z=(p.type===g.type?Ue:We*xe.spacing)*t/(T*T),U=x/T*z,C=I/T*z;p.vx-=U,p.vy-=C,g.vx+=U,g.vy+=C}for(const s of e){const N=y.get(s.sourceId),p=y.get(s.targetId);if(!N||!p)continue;const g=p.x-N.x,x=p.y-N.y,I=Math.sqrt(g*g+x*x);if(I===0)continue;const T=N.type===p.type?Be*xe.spacing:De*xe.spacing,B=je*(I-T)*t,z=g/I*B,U=x/I*B;N.vx+=z,N.vy+=U,p.vx-=z,p.vy-=U}for(const s of a)s.vx-=s.x*Te*t,s.vy-=s.y*Te*t;const o=new Map;for(const s of a){const N=o.get(s.type)??{x:0,y:0,count:0};N.x+=s.x,N.y+=s.y,N.count++,o.set(s.type,N)}for(const s of o.values())s.x/=s.count,s.y/=s.count;for(const s of a){const N=o.get(s.type);s.vx+=(N.x-s.x)*xe.clusterStrength*t,s.vy+=(N.y-s.y)*xe.clusterStrength*t}for(const s of a){s.vx*=Me,s.vy*=Me;const N=Math.sqrt(s.vx*s.vx+s.vy*s.vy);N>we&&(s.vx=s.vx/N*we,s.vy=s.vy/N*we),s.x+=s.vx,s.y+=s.vy}return t*.995}const Ae=["#d4a27f","#c17856","#b07a5e","#d4956b","#a67c5a","#cc9e7c","#c4866a","#cb8e6c","#b8956e","#a88a70","#d9b08c","#c4a882","#e8b898","#b5927a","#a8886e","#d1a990"],Pe=new Map;function ae(c){const t=Pe.get(c);if(t)return t;let a=0;for(let y=0;y<c.length;y++)a=(a<<5)-a+c.charCodeAt(y)|0;const e=Ae[Math.abs(a)%Ae.length];return Pe.set(c,e),e}function ee(c){return getComputedStyle(document.documentElement).getPropertyValue(c).trim()}const de=20,Ve=.001;function Je(c,t){const a=c.querySelector("canvas"),e=a.getContext("2d"),y=window.devicePixelRatio||1;let o={x:0,y:0,scale:1},s=null,N=1,p=0,g=new Set,x=null,I=!0,T=!0,B=!0,z=null,U=null;const C=300;function w(){a.width=a.clientWidth*y,a.height=a.clientHeight*y,u()}const m=new ResizeObserver(w);m.observe(c),w();function i(d,r){return[d/o.scale+o.x,r/o.scale+o.y]}function l(d,r){if(!s)return null;const[L,M]=i(d,r);for(let O=s.nodes.length-1;O>=0;O--){const Y=s.nodes[O],P=L-Y.x,W=M-Y.y;if(P*P+W*W<=de*de)return Y}return null}function u(){if(!s){e.clearRect(0,0,a.width,a.height);return}const d=ee("--canvas-edge"),r=ee("--canvas-edge-highlight"),L=ee("--canvas-edge-dim"),M=ee("--canvas-edge-label"),O=ee("--canvas-edge-label-highlight"),Y=ee("--canvas-edge-label-dim"),P=ee("--canvas-arrow"),W=ee("--canvas-arrow-highlight"),ne=ee("--canvas-node-label"),Q=ee("--canvas-node-label-dim"),re=ee("--canvas-type-badge"),se=ee("--canvas-type-badge-dim"),ye=ee("--canvas-selection-border"),Ce=ee("--canvas-node-border");if(e.save(),e.setTransform(y,0,0,y,0,0),e.clearRect(0,0,a.clientWidth,a.clientHeight),e.save(),e.translate(-o.x*o.scale,-o.y*o.scale),e.scale(o.scale,o.scale),T){const R=new Map;for(const V of s.nodes){if(x!==null&&!x.has(V.id))continue;const Z=R.get(V.type)??[];Z.push(V),R.set(V.type,Z)}for(const[V,Z]of R){if(Z.length<2)continue;const me=ae(V),pe=de*2.5;let ce=1/0,ue=1/0,oe=-1/0,K=-1/0;for(const ve of Z)ve.x<ce&&(ce=ve.x),ve.y<ue&&(ue=ve.y),ve.x>oe&&(oe=ve.x),ve.y>K&&(K=ve.y);e.beginPath();const he=(oe-ce)/2+pe,fe=(K-ue)/2+pe,He=(ce+oe)/2,ze=(ue+K)/2;e.ellipse(He,ze,he,fe,0,0,Math.PI*2),e.fillStyle=me,e.globalAlpha=.05,e.fill(),e.strokeStyle=me,e.globalAlpha=.12,e.lineWidth=1,e.setLineDash([4,4]),e.stroke(),e.setLineDash([]),e.globalAlpha=1}}for(const R of s.edges){const V=s.nodeMap.get(R.sourceId),Z=s.nodeMap.get(R.targetId);if(!V||!Z)continue;const me=x===null||x.has(R.sourceId),pe=x===null||x.has(R.targetId),ce=me&&pe;if(x!==null&&!me&&!pe)continue;const oe=g.size>0&&(g.has(R.sourceId)||g.has(R.targetId))||x!==null&&ce,K=x!==null&&!ce;if(R.sourceId===R.targetId){D(V,R.type,oe,d,r,M,O);continue}if(e.beginPath(),e.moveTo(V.x,V.y),e.lineTo(Z.x,Z.y),e.strokeStyle=oe?r:K?L:d,e.lineWidth=oe?2.5:1.5,e.stroke(),S(V.x,V.y,Z.x,Z.y,oe,P,W),I){const he=(V.x+Z.x)/2,fe=(V.y+Z.y)/2;e.fillStyle=oe?O:K?Y:M,e.font="9px system-ui, sans-serif",e.textAlign="center",e.textBaseline="bottom",e.fillText(R.type,he,fe-4)}}for(const R of s.nodes){const V=ae(R.type),Z=g.has(R.id),me=g.size>0&&s.edges.some(oe=>g.has(oe.sourceId)&&oe.targetId===R.id||g.has(oe.targetId)&&oe.sourceId===R.id),pe=x!==null&&!x.has(R.id),ce=pe||g.size>0&&!Z&&!me;Z&&(e.save(),e.shadowColor=V,e.shadowBlur=20,e.beginPath(),e.arc(R.x,R.y,de+3,0,Math.PI*2),e.fillStyle=V,e.globalAlpha=.3,e.fill(),e.restore()),e.beginPath(),e.arc(R.x,R.y,de,0,Math.PI*2),e.fillStyle=V,e.globalAlpha=pe?.1:ce?.3:1,e.fill(),e.strokeStyle=Z?ye:Ce,e.lineWidth=Z?3:1.5,e.stroke();const ue=R.label.length>24?R.label.slice(0,22)+"...":R.label;e.fillStyle=ce?Q:ne,e.font="11px system-ui, sans-serif",e.textAlign="center",e.textBaseline="top",e.fillText(ue,R.x,R.y+de+4),e.fillStyle=ce?se:re,e.font="9px system-ui, sans-serif",e.textBaseline="bottom",e.fillText(R.type,R.x,R.y-de-3),e.globalAlpha=1}e.restore(),e.restore(),B&&s.nodes.length>1&&b()}function b(){if(!s)return;const d=140,r=100,L=8,M=a.clientWidth-d-16,O=a.clientHeight-r-16;let Y=1/0,P=1/0,W=-1/0,ne=-1/0;for(const K of s.nodes)K.x<Y&&(Y=K.x),K.y<P&&(P=K.y),K.x>W&&(W=K.x),K.y>ne&&(ne=K.y);const Q=W-Y||1,re=ne-P||1,se=Math.min((d-L*2)/Q,(r-L*2)/re),ye=M+L+(d-L*2-Q*se)/2,Ce=O+L+(r-L*2-re*se)/2;e.save(),e.setTransform(y,0,0,y,0,0),e.fillStyle=ee("--bg-surface")||"#1a1a1a",e.globalAlpha=.85,e.beginPath(),e.roundRect(M,O,d,r,8),e.fill(),e.strokeStyle=ee("--border")||"#2a2a2a",e.globalAlpha=1,e.lineWidth=1,e.stroke(),e.globalAlpha=.15,e.strokeStyle=ee("--canvas-edge")||"#555",e.lineWidth=.5;for(const K of s.edges){const he=s.nodeMap.get(K.sourceId),fe=s.nodeMap.get(K.targetId);!he||!fe||K.sourceId===K.targetId||(e.beginPath(),e.moveTo(ye+(he.x-Y)*se,Ce+(he.y-P)*se),e.lineTo(ye+(fe.x-Y)*se,Ce+(fe.y-P)*se),e.stroke())}e.globalAlpha=.8;for(const K of s.nodes){const he=ye+(K.x-Y)*se,fe=Ce+(K.y-P)*se;e.beginPath(),e.arc(he,fe,2,0,Math.PI*2),e.fillStyle=ae(K.type),e.fill()}const R=o.x,V=o.y,Z=o.x+a.clientWidth/o.scale,me=o.y+a.clientHeight/o.scale,pe=ye+(R-Y)*se,ce=Ce+(V-P)*se,ue=(Z-R)*se,oe=(me-V)*se;e.globalAlpha=.3,e.strokeStyle=ee("--accent")||"#d4a27f",e.lineWidth=1.5,e.strokeRect(Math.max(M,Math.min(pe,M+d)),Math.max(O,Math.min(ce,O+r)),Math.min(ue,d),Math.min(oe,r)),e.globalAlpha=1,e.restore()}function S(d,r,L,M,O,Y,P){const W=Math.atan2(M-r,L-d),ne=L-Math.cos(W)*de,Q=M-Math.sin(W)*de,re=8;e.beginPath(),e.moveTo(ne,Q),e.lineTo(ne-re*Math.cos(W-.4),Q-re*Math.sin(W-.4)),e.lineTo(ne-re*Math.cos(W+.4),Q-re*Math.sin(W+.4)),e.closePath(),e.fillStyle=O?P:Y,e.fill()}function D(d,r,L,M,O,Y,P){const W=d.x+de+15,ne=d.y-de-15;e.beginPath(),e.arc(W,ne,15,0,Math.PI*2),e.strokeStyle=L?O:M,e.lineWidth=L?2.5:1.5,e.stroke(),I&&(e.fillStyle=L?P:Y,e.font="9px system-ui, sans-serif",e.textAlign="center",e.fillText(r,W,ne-18))}function $(){if(!z||!U)return;const d=performance.now()-U.time,r=Math.min(d/C,1),L=1-Math.pow(1-r,3);o.x=U.x+(z.x-U.x)*L,o.y=U.y+(z.y-U.y)*L,u(),r<1?requestAnimationFrame($):(z=null,U=null)}function f(){!s||N<Ve||(N=Ke(s,N),u(),p=requestAnimationFrame(f))}let X=!1,H=!1,n=0,h=0;a.addEventListener("mousedown",d=>{X=!0,H=!1,n=d.clientX,h=d.clientY}),a.addEventListener("mousemove",d=>{if(!X)return;const r=d.clientX-n,L=d.clientY-h;(Math.abs(r)>2||Math.abs(L)>2)&&(H=!0),o.x-=r/o.scale,o.y-=L/o.scale,n=d.clientX,h=d.clientY,u()}),a.addEventListener("mouseup",d=>{if(X=!1,H)return;const r=a.getBoundingClientRect(),L=d.clientX-r.left,M=d.clientY-r.top,O=l(L,M),Y=d.ctrlKey||d.metaKey;if(O){Y?g.has(O.id)?g.delete(O.id):g.add(O.id):g.size===1&&g.has(O.id)?g.clear():(g.clear(),g.add(O.id));const P=[...g];t==null||t(P.length>0?P:null)}else g.clear(),t==null||t(null);u()}),a.addEventListener("mouseleave",()=>{X=!1}),a.addEventListener("wheel",d=>{d.preventDefault();const r=a.getBoundingClientRect(),L=d.clientX-r.left,M=d.clientY-r.top,[O,Y]=i(L,M),P=d.ctrlKey?1-d.deltaY*.01:d.deltaY>0?.9:1.1;o.scale=Math.max(.05,Math.min(10,o.scale*P)),o.x=O-L/o.scale,o.y=Y-M/o.scale,u()},{passive:!1});let v=[],k=0,F=1,A=0,_=0,G=!1;a.addEventListener("touchstart",d=>{d.preventDefault(),v=Array.from(d.touches),v.length===2?(k=J(v[0],v[1]),F=o.scale):v.length===1&&(n=v[0].clientX,h=v[0].clientY,A=v[0].clientX,_=v[0].clientY,G=!1)},{passive:!1}),a.addEventListener("touchmove",d=>{d.preventDefault();const r=Array.from(d.touches);if(r.length===2&&v.length===2){const M=J(r[0],r[1])/k;o.scale=Math.max(.05,Math.min(10,F*M)),u()}else if(r.length===1){const L=r[0].clientX-n,M=r[0].clientY-h;(Math.abs(r[0].clientX-A)>10||Math.abs(r[0].clientY-_)>10)&&(G=!0),o.x-=L/o.scale,o.y-=M/o.scale,n=r[0].clientX,h=r[0].clientY,u()}v=r},{passive:!1}),a.addEventListener("touchend",d=>{if(d.preventDefault(),G||d.changedTouches.length!==1)return;const r=d.changedTouches[0],L=a.getBoundingClientRect(),M=r.clientX-L.left,O=r.clientY-L.top,Y=l(M,O);if(Y){g.size===1&&g.has(Y.id)?g.clear():(g.clear(),g.add(Y.id));const P=[...g];t==null||t(P.length>0?P:null)}else g.clear(),t==null||t(null);u()},{passive:!1}),a.addEventListener("gesturestart",d=>d.preventDefault()),a.addEventListener("gesturechange",d=>d.preventDefault());function J(d,r){const L=d.clientX-r.clientX,M=d.clientY-r.clientY;return Math.sqrt(L*L+M*M)}const te=document.createElement("div");te.className="zoom-controls";const j=document.createElement("button");j.className="zoom-btn",j.textContent="+",j.title="Zoom in",j.addEventListener("click",()=>{const d=a.clientWidth/2,r=a.clientHeight/2,[L,M]=i(d,r);o.scale=Math.min(10,o.scale*1.3),o.x=L-d/o.scale,o.y=M-r/o.scale,u()});const le=document.createElement("button");le.className="zoom-btn",le.textContent="−",le.title="Zoom out",le.addEventListener("click",()=>{const d=a.clientWidth/2,r=a.clientHeight/2,[L,M]=i(d,r);o.scale=Math.max(.05,o.scale/1.3),o.x=L-d/o.scale,o.y=M-r/o.scale,u()});const q=document.createElement("button");return q.className="zoom-btn",q.textContent="○",q.title="Reset zoom",q.addEventListener("click",()=>{if(s){if(o={x:0,y:0,scale:1},s.nodes.length>0){let d=1/0,r=1/0,L=-1/0,M=-1/0;for(const P of s.nodes)P.x<d&&(d=P.x),P.y<r&&(r=P.y),P.x>L&&(L=P.x),P.y>M&&(M=P.y);const O=(d+L)/2,Y=(r+M)/2;o.x=O-a.clientWidth/2,o.y=Y-a.clientHeight/2}u()}}),te.appendChild(j),te.appendChild(q),te.appendChild(le),c.appendChild(te),{loadGraph(d){if(cancelAnimationFrame(p),s=qe(d),N=1,g=new Set,x=null,o={x:0,y:0,scale:1},s.nodes.length>0){let r=1/0,L=1/0,M=-1/0,O=-1/0;for(const Q of s.nodes)Q.x<r&&(r=Q.x),Q.y<L&&(L=Q.y),Q.x>M&&(M=Q.x),Q.y>O&&(O=Q.y);const Y=(r+M)/2,P=(L+O)/2,W=a.clientWidth,ne=a.clientHeight;o.x=Y-W/2,o.y=P-ne/2}f()},setFilteredNodeIds(d){x=d,u()},panToNode(d){this.panToNodes([d])},panToNodes(d){if(!s||d.length===0)return;const r=d.map(O=>s.nodeMap.get(O)).filter(Boolean);if(r.length===0)return;g=new Set(d),t==null||t(d);const L=a.clientWidth,M=a.clientHeight;if(r.length===1)U={x:o.x,y:o.y,time:performance.now()},z={x:r[0].x-L/(2*o.scale),y:r[0].y-M/(2*o.scale)};else{let O=1/0,Y=1/0,P=-1/0,W=-1/0;for(const R of r)R.x<O&&(O=R.x),R.y<Y&&(Y=R.y),R.x>P&&(P=R.x),R.y>W&&(W=R.y);const ne=de*4,Q=P-O+ne*2,re=W-Y+ne*2,se=Math.min(L/Q,M/re,o.scale);o.scale=se;const ye=(O+P)/2,Ce=(Y+W)/2;U={x:o.x,y:o.y,time:performance.now()},z={x:ye-L/(2*o.scale),y:Ce-M/(2*o.scale)}}$()},setEdgeLabels(d){I=d,u()},setTypeHulls(d){T=d,u()},setMinimap(d){B=d,u()},reheat(){N=.5,cancelAnimationFrame(p),f()},exportImage(d){if(!s)return"";const r=a.width,L=a.height;if(d==="png"){const P=document.createElement("canvas");P.width=r,P.height=L;const W=P.getContext("2d");return W.fillStyle=ee("--bg")||"#141414",W.fillRect(0,0,r,L),W.drawImage(a,0,0),ge(W,r,L),P.toDataURL("image/png")}const M=a.toDataURL("image/png"),O=Math.max(16,Math.round(r/80)),Y=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${L}">
|
|
2
|
+
<image href="${M}" width="${r}" height="${L}"/>
|
|
3
|
+
<text x="${r-20}" y="${L-16}" text-anchor="end" font-family="system-ui, sans-serif" font-size="${O}" fill="#ffffff" opacity="0.4">backpackontology.com</text>
|
|
4
|
+
</svg>`;return"data:image/svg+xml;charset=utf-8,"+encodeURIComponent(Y)},destroy(){cancelAnimationFrame(p),m.disconnect()}};function ge(d,r,L){const M=Math.max(16,Math.round(r/80));d.save(),d.font=`${M}px system-ui, sans-serif`,d.fillStyle="rgba(255, 255, 255, 0.4)",d.textAlign="right",d.textBaseline="bottom",d.fillText("backpackontology.com",r-20,L-16),d.restore()}}function Ee(c){for(const t of Object.values(c.properties))if(typeof t=="string")return t;return c.id}const Ze="✎";function Qe(c,t,a){const e=document.createElement("div");e.id="info-panel",e.className="info-panel hidden",c.appendChild(e);let y=!1,o=[],s=-1,N=!1,p=null;function g(){e.classList.add("hidden"),e.classList.remove("info-panel-maximized"),e.innerHTML="",y=!1,o=[],s=-1}function x(C){!p||!a||(s<o.length-1&&(o=o.slice(0,s+1)),o.push(C),s=o.length-1,N=!0,a(C),N=!1)}function I(){s<=0||!p||!a||(s--,N=!0,a(o[s]),N=!1)}function T(){s>=o.length-1||!p||!a||(s++,N=!0,a(o[s]),N=!1)}function B(){const C=document.createElement("div");C.className="info-panel-toolbar";const w=document.createElement("button");w.className="info-toolbar-btn",w.textContent="←",w.title="Back",w.disabled=s<=0,w.addEventListener("click",I),C.appendChild(w);const m=document.createElement("button");m.className="info-toolbar-btn",m.textContent="→",m.title="Forward",m.disabled=s>=o.length-1,m.addEventListener("click",T),C.appendChild(m);const i=document.createElement("button");i.className="info-toolbar-btn",i.textContent=y?"⎘":"⛶",i.title=y?"Restore":"Maximize",i.addEventListener("click",()=>{y=!y,e.classList.toggle("info-panel-maximized",y),i.textContent=y?"⎘":"⛶",i.title=y?"Restore":"Maximize"}),C.appendChild(i);const l=document.createElement("button");return l.className="info-toolbar-btn info-close-btn",l.textContent="×",l.title="Close",l.addEventListener("click",g),C.appendChild(l),C}function z(C,w){const m=w.nodes.find(k=>k.id===C);if(!m)return;const i=w.edges.filter(k=>k.sourceId===C||k.targetId===C);e.innerHTML="",e.classList.remove("hidden"),y&&e.classList.add("info-panel-maximized"),e.appendChild(B());const l=document.createElement("div");l.className="info-header";const u=document.createElement("span");if(u.className="info-type-badge",u.textContent=m.type,u.style.backgroundColor=ae(m.type),t){u.classList.add("info-editable");const k=document.createElement("button");k.className="info-inline-edit",k.textContent=Ze,k.addEventListener("click",F=>{F.stopPropagation();const A=document.createElement("input");A.type="text",A.className="info-edit-inline-input",A.value=m.type,u.textContent="",u.appendChild(A),A.focus(),A.select();const _=()=>{const G=A.value.trim();G&&G!==m.type?t.onChangeNodeType(C,G):(u.textContent=m.type,u.appendChild(k))};A.addEventListener("blur",_),A.addEventListener("keydown",G=>{G.key==="Enter"&&A.blur(),G.key==="Escape"&&(A.value=m.type,A.blur())})}),u.appendChild(k)}const b=document.createElement("h3");b.className="info-label",b.textContent=Ee(m);const S=document.createElement("span");S.className="info-id",S.textContent=m.id,l.appendChild(u),l.appendChild(b),l.appendChild(S),e.appendChild(l);const D=Object.keys(m.properties),$=be("Properties");if(D.length>0){const k=document.createElement("dl");k.className="info-props";for(const F of D){const A=document.createElement("dt");A.textContent=F;const _=document.createElement("dd");if(t){const G=Ne(m.properties[F]),J=document.createElement("input");J.type="text",J.className="info-edit-input",J.value=G,J.addEventListener("keydown",j=>{j.key==="Enter"&&J.blur()}),J.addEventListener("blur",()=>{const j=J.value;j!==G&&t.onUpdateNode(C,{[F]:tt(j)})}),_.appendChild(J);const te=document.createElement("button");te.className="info-delete-prop",te.textContent="×",te.title=`Remove ${F}`,te.addEventListener("click",()=>{const j={...m.properties};delete j[F],t.onUpdateNode(C,j)}),_.appendChild(te)}else _.appendChild(et(m.properties[F]));k.appendChild(A),k.appendChild(_)}$.appendChild(k)}if(t){const k=document.createElement("button");k.className="info-add-btn",k.textContent="+ Add property",k.addEventListener("click",()=>{const F=document.createElement("div");F.className="info-add-row";const A=document.createElement("input");A.type="text",A.className="info-edit-input",A.placeholder="key";const _=document.createElement("input");_.type="text",_.className="info-edit-input",_.placeholder="value";const G=document.createElement("button");G.className="info-add-save",G.textContent="Add",G.addEventListener("click",()=>{A.value&&t.onAddProperty(C,A.value,_.value)}),F.appendChild(A),F.appendChild(_),F.appendChild(G),$.appendChild(F),A.focus()}),$.appendChild(k)}if(e.appendChild($),i.length>0){const k=be(`Connections (${i.length})`),F=document.createElement("ul");F.className="info-connections";for(const A of i){const _=A.sourceId===C,G=_?A.targetId:A.sourceId,J=w.nodes.find(r=>r.id===G),te=J?Ee(J):G,j=document.createElement("li");if(j.className="info-connection",a&&J&&(j.classList.add("info-connection-link"),j.addEventListener("click",r=>{r.target.closest(".info-delete-edge")||x(G)})),J){const r=document.createElement("span");r.className="info-target-dot",r.style.backgroundColor=ae(J.type),j.appendChild(r)}const le=document.createElement("span");le.className="info-arrow",le.textContent=_?"→":"←";const q=document.createElement("span");q.className="info-edge-type",q.textContent=A.type;const ge=document.createElement("span");ge.className="info-target",ge.textContent=te,j.appendChild(le),j.appendChild(q),j.appendChild(ge);const d=Object.keys(A.properties);if(d.length>0){const r=document.createElement("div");r.className="info-edge-props";for(const L of d){const M=document.createElement("span");M.className="info-edge-prop",M.textContent=`${L}: ${Ne(A.properties[L])}`,r.appendChild(M)}j.appendChild(r)}if(t){const r=document.createElement("button");r.className="info-delete-edge",r.textContent="×",r.title="Remove connection",r.addEventListener("click",L=>{L.stopPropagation(),t.onDeleteEdge(A.id)}),j.appendChild(r)}F.appendChild(j)}k.appendChild(F),e.appendChild(k)}const f=be("Timestamps"),X=document.createElement("dl");X.className="info-props";const H=document.createElement("dt");H.textContent="created";const n=document.createElement("dd");n.textContent=Re(m.createdAt);const h=document.createElement("dt");h.textContent="updated";const v=document.createElement("dd");if(v.textContent=Re(m.updatedAt),X.appendChild(H),X.appendChild(n),X.appendChild(h),X.appendChild(v),f.appendChild(X),e.appendChild(f),t){const k=document.createElement("div");k.className="info-section info-danger";const F=document.createElement("button");F.className="info-delete-node",F.textContent="Delete node",F.addEventListener("click",()=>{t.onDeleteNode(C),g()}),k.appendChild(F),e.appendChild(k)}}function U(C,w){const m=new Set(C),i=w.nodes.filter(H=>m.has(H.id));if(i.length===0)return;const l=w.edges.filter(H=>m.has(H.sourceId)&&m.has(H.targetId));e.innerHTML="",e.classList.remove("hidden"),y&&e.classList.add("info-panel-maximized"),e.appendChild(B());const u=document.createElement("div");u.className="info-header";const b=document.createElement("h3");b.className="info-label",b.textContent=`${i.length} nodes selected`,u.appendChild(b);const S=document.createElement("div");S.style.cssText="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px";const D=new Map;for(const H of i)D.set(H.type,(D.get(H.type)??0)+1);for(const[H,n]of D){const h=document.createElement("span");h.className="info-type-badge",h.style.backgroundColor=ae(H),h.textContent=n>1?`${H} (${n})`:H,S.appendChild(h)}u.appendChild(S),e.appendChild(u);const $=be("Selected Nodes"),f=document.createElement("ul");f.className="info-connections";for(const H of i){const n=document.createElement("li");n.className="info-connection",a&&(n.classList.add("info-connection-link"),n.addEventListener("click",()=>{x(H.id)}));const h=document.createElement("span");h.className="info-target-dot",h.style.backgroundColor=ae(H.type);const v=document.createElement("span");v.className="info-target",v.textContent=Ee(H);const k=document.createElement("span");k.className="info-edge-type",k.textContent=H.type,n.appendChild(h),n.appendChild(v),n.appendChild(k),f.appendChild(n)}$.appendChild(f),e.appendChild($);const X=be(l.length>0?`Connections Between Selected (${l.length})`:"Connections Between Selected");if(l.length===0){const H=document.createElement("p");H.style.cssText="font-size:12px;color:var(--text-dim)",H.textContent="No direct connections between selected nodes",X.appendChild(H)}else{const H=document.createElement("ul");H.className="info-connections";for(const n of l){const h=w.nodes.find(q=>q.id===n.sourceId),v=w.nodes.find(q=>q.id===n.targetId),k=h?Ee(h):n.sourceId,F=v?Ee(v):n.targetId,A=document.createElement("li");if(A.className="info-connection",h){const q=document.createElement("span");q.className="info-target-dot",q.style.backgroundColor=ae(h.type),A.appendChild(q)}const _=document.createElement("span");_.className="info-target",_.textContent=k;const G=document.createElement("span");G.className="info-arrow",G.textContent="→";const J=document.createElement("span");J.className="info-edge-type",J.textContent=n.type;const te=document.createElement("span");if(te.className="info-arrow",te.textContent="→",A.appendChild(_),A.appendChild(G),A.appendChild(J),A.appendChild(te),v){const q=document.createElement("span");q.className="info-target-dot",q.style.backgroundColor=ae(v.type),A.appendChild(q)}const j=document.createElement("span");j.className="info-target",j.textContent=F,A.appendChild(j);const le=Object.keys(n.properties);if(le.length>0){const q=document.createElement("div");q.className="info-edge-props";for(const ge of le){const d=document.createElement("span");d.className="info-edge-prop",d.textContent=`${ge}: ${Ne(n.properties[ge])}`,q.appendChild(d)}A.appendChild(q)}H.appendChild(A)}X.appendChild(H)}e.appendChild(X)}return{show(C,w){if(p=w,C.length===1&&!N){const m=C[0];o[s]!==m&&(s<o.length-1&&(o=o.slice(0,s+1)),o.push(m),s=o.length-1)}C.length===1?z(C[0],w):C.length>1&&U(C,w)},hide:g,get visible(){return!e.classList.contains("hidden")}}}function be(c){const t=document.createElement("div");t.className="info-section";const a=document.createElement("h4");return a.className="info-section-title",a.textContent=c,t.appendChild(a),t}function et(c){if(Array.isArray(c)){const a=document.createElement("div");a.className="info-array";for(const e of c){const y=document.createElement("span");y.className="info-tag",y.textContent=String(e),a.appendChild(y)}return a}if(c!==null&&typeof c=="object"){const a=document.createElement("pre");return a.className="info-json",a.textContent=JSON.stringify(c,null,2),a}const t=document.createElement("span");return t.className="info-value",t.textContent=String(c??""),t}function Ne(c){return Array.isArray(c)?c.map(String).join(", "):c!==null&&typeof c=="object"?JSON.stringify(c):String(c??"")}function tt(c){const t=c.trim();if(t==="true")return!0;if(t==="false")return!1;if(t!==""&&!isNaN(Number(t)))return Number(t);if(t.startsWith("[")&&t.endsWith("]")||t.startsWith("{")&&t.endsWith("}"))try{return JSON.parse(t)}catch{return c}return c}function Re(c){try{return new Date(c).toLocaleString()}catch{return c}}function $e(c){for(const t of Object.values(c.properties))if(typeof t=="string")return t;return c.id}function Oe(c,t){const a=t.toLowerCase();if($e(c).toLowerCase().includes(a)||c.type.toLowerCase().includes(a))return!0;for(const e of Object.values(c.properties))if(typeof e=="string"&&e.toLowerCase().includes(a))return!0;return!1}function nt(c){let t=null,a=null,e=null,y=new Set,o=null;const s=document.createElement("div");s.className="search-overlay hidden";const N=document.createElement("div");N.className="search-input-wrap";const p=document.createElement("input");p.className="search-input",p.type="text",p.placeholder="Search nodes...",p.setAttribute("autocomplete","off"),p.setAttribute("spellcheck","false");const g=document.createElement("kbd");g.className="search-kbd",g.textContent="/";const x=document.createElement("button");x.className="chip-toggle",x.setAttribute("aria-label","Toggle filter chips"),x.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>';let I=!1;x.addEventListener("click",()=>{I=!I,B.classList.toggle("hidden",!I),x.classList.toggle("active",I)}),N.appendChild(p),N.appendChild(g),N.appendChild(x);const T=document.createElement("ul");T.className="search-results hidden";const B=document.createElement("div");B.className="type-chips hidden",s.appendChild(N),s.appendChild(T),s.appendChild(B),c.appendChild(s);function z(){if(B.innerHTML="",!t)return;const m=new Map;for(const l of t.nodes)m.set(l.type,(m.get(l.type)??0)+1);const i=[...m.keys()].sort();y=new Set;for(const l of i){const u=document.createElement("button");u.className="type-chip",u.dataset.type=l;const b=document.createElement("span");b.className="type-chip-dot",b.style.backgroundColor=ae(l);const S=document.createElement("span");S.textContent=`${l} (${m.get(l)})`,u.appendChild(b),u.appendChild(S),u.addEventListener("click",()=>{y.has(l)?(y.delete(l),u.classList.remove("active")):(y.add(l),u.classList.add("active")),C()}),B.appendChild(u)}}function U(){if(!t)return null;const m=p.value.trim(),i=y.size===0,l=m.length===0;if(l&&i)return null;const u=new Set;for(const b of t.nodes)!i&&!y.has(b.type)||(l||Oe(b,m))&&u.add(b.id);return u}function C(){const m=U();a==null||a(m),w()}function w(){T.innerHTML="";const m=p.value.trim();if(!t||m.length===0){T.classList.add("hidden");return}const i=y.size===0,l=[];for(const u of t.nodes)if(!(!i&&!y.has(u.type))&&Oe(u,m)&&(l.push(u),l.length>=8))break;if(l.length===0){T.classList.add("hidden");return}for(const u of l){const b=document.createElement("li");b.className="search-result-item";const S=document.createElement("span");S.className="search-result-dot",S.style.backgroundColor=ae(u.type);const D=document.createElement("span");D.className="search-result-label";const $=$e(u);D.textContent=$.length>36?$.slice(0,34)+"...":$;const f=document.createElement("span");f.className="search-result-type",f.textContent=u.type,b.appendChild(S),b.appendChild(D),b.appendChild(f),b.addEventListener("click",()=>{e==null||e(u.id),p.value="",T.classList.add("hidden"),C()}),T.appendChild(b)}T.classList.remove("hidden")}return p.addEventListener("input",()=>{o&&clearTimeout(o),o=setTimeout(C,150)}),p.addEventListener("keydown",m=>{if(m.key==="Escape")p.value="",p.blur(),T.classList.add("hidden"),C();else if(m.key==="Enter"){const i=T.querySelector(".search-result-item");i==null||i.click()}}),document.addEventListener("click",m=>{s.contains(m.target)||T.classList.add("hidden")}),p.addEventListener("focus",()=>g.classList.add("hidden")),p.addEventListener("blur",()=>{p.value.length===0&&g.classList.remove("hidden")}),{setLearningGraphData(m){t=m,p.value="",T.classList.add("hidden"),t&&t.nodes.length>0?(s.classList.remove("hidden"),z()):s.classList.add("hidden")},onFilterChange(m){a=m},onNodeSelect(m){e=m},clear(){p.value="",T.classList.add("hidden"),y.clear(),I=!1,B.classList.add("hidden"),x.classList.remove("active"),a==null||a(null)},focus(){p.focus()}}}function ot(c,t){let a=null,e=null,y=!0,o=null,s=!0,N=!0,p=!0;const g=document.createElement("button");g.className="tools-pane-toggle hidden",g.title="Graph Inspector",g.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h10"/></svg>';const x=document.createElement("div");x.className="tools-pane-content hidden",c.appendChild(g),c.appendChild(x),g.addEventListener("click",()=>{var C;y=!y,x.classList.toggle("hidden",y),g.classList.toggle("active",!y),y||(C=t.onOpen)==null||C.call(t)});function I(){if(x.innerHTML="",!e)return;const C=document.createElement("div");C.className="tools-pane-summary",C.innerHTML=`<span>${e.nodeCount} nodes</span><span class="tools-pane-sep">·</span><span>${e.edgeCount} edges</span><span class="tools-pane-sep">·</span><span>${e.types.length} types</span>`,x.appendChild(C),e.types.length&&x.appendChild(B("Node Types",m=>{for(const i of e.types){const l=document.createElement("div");l.className="tools-pane-row tools-pane-clickable",o===i.name&&l.classList.add("active");const u=document.createElement("span");u.className="tools-pane-dot",u.style.backgroundColor=ae(i.name);const b=document.createElement("span");b.className="tools-pane-name",b.textContent=i.name;const S=document.createElement("span");S.className="tools-pane-count",S.textContent=String(i.count);const D=document.createElement("button");D.className="tools-pane-edit",D.textContent="✎",D.title=`Rename all ${i.name} nodes`,l.appendChild(u),l.appendChild(b),l.appendChild(S),l.appendChild(D),l.addEventListener("click",$=>{$.target.closest(".tools-pane-edit")||(o===i.name?(o=null,t.onFilterByType(null)):(o=i.name,t.onFilterByType(i.name)),I())}),D.addEventListener("click",$=>{$.stopPropagation(),z(l,i.name,f=>{f&&f!==i.name&&t.onRenameNodeType(i.name,f)})}),m.appendChild(l)}})),e.edgeTypes.length&&x.appendChild(B("Edge Types",m=>{for(const i of e.edgeTypes){const l=document.createElement("div");l.className="tools-pane-row tools-pane-clickable";const u=document.createElement("span");u.className="tools-pane-name",u.textContent=i.name;const b=document.createElement("span");b.className="tools-pane-count",b.textContent=String(i.count);const S=document.createElement("button");S.className="tools-pane-edit",S.textContent="✎",S.title=`Rename all ${i.name} edges`,l.appendChild(u),l.appendChild(b),l.appendChild(S),S.addEventListener("click",D=>{D.stopPropagation(),z(l,i.name,$=>{$&&$!==i.name&&t.onRenameEdgeType(i.name,$)})}),m.appendChild(l)}})),e.mostConnected.length&&x.appendChild(B("Most Connected",m=>{for(const i of e.mostConnected){const l=document.createElement("div");l.className="tools-pane-row tools-pane-clickable";const u=document.createElement("span");u.className="tools-pane-dot",u.style.backgroundColor=ae(i.type);const b=document.createElement("span");b.className="tools-pane-name",b.textContent=i.label;const S=document.createElement("span");S.className="tools-pane-count",S.textContent=`${i.connections}`,l.appendChild(u),l.appendChild(b),l.appendChild(S),l.addEventListener("click",()=>{t.onNavigateToNode(i.id)}),m.appendChild(l)}}));const w=[];e.orphans.length&&w.push(`${e.orphans.length} orphan${e.orphans.length>1?"s":""}`),e.singletons.length&&w.push(`${e.singletons.length} singleton type${e.singletons.length>1?"s":""}`),e.emptyNodes.length&&w.push(`${e.emptyNodes.length} empty node${e.emptyNodes.length>1?"s":""}`),w.length&&x.appendChild(B("Quality",m=>{for(const i of e.orphans.slice(0,5)){const l=document.createElement("div");l.className="tools-pane-row tools-pane-clickable tools-pane-issue";const u=document.createElement("span");u.className="tools-pane-dot",u.style.backgroundColor=ae(i.type);const b=document.createElement("span");b.className="tools-pane-name",b.textContent=i.label;const S=document.createElement("span");S.className="tools-pane-badge",S.textContent="orphan",l.appendChild(u),l.appendChild(b),l.appendChild(S),l.addEventListener("click",()=>{t.onNavigateToNode(i.id)}),m.appendChild(l)}if(e.orphans.length>5){const i=document.createElement("div");i.className="tools-pane-more",i.textContent=`+ ${e.orphans.length-5} more orphans`,m.appendChild(i)}for(const i of e.singletons.slice(0,5)){const l=document.createElement("div");l.className="tools-pane-row tools-pane-issue";const u=document.createElement("span");u.className="tools-pane-dot",u.style.backgroundColor=ae(i.name);const b=document.createElement("span");b.className="tools-pane-name",b.textContent=i.name;const S=document.createElement("span");S.className="tools-pane-badge",S.textContent="1 node",l.appendChild(u),l.appendChild(b),l.appendChild(S),m.appendChild(l)}})),x.appendChild(B("Controls",m=>{const i=document.createElement("div");i.className="tools-pane-row tools-pane-clickable";const l=document.createElement("input");l.type="checkbox",l.checked=s,l.className="tools-pane-checkbox";const u=document.createElement("span");u.className="tools-pane-name",u.textContent="Edge labels",i.appendChild(l),i.appendChild(u),i.addEventListener("click",v=>{v.target!==l&&(l.checked=!l.checked),s=l.checked,t.onToggleEdgeLabels(s)}),m.appendChild(i);const b=document.createElement("div");b.className="tools-pane-row tools-pane-clickable";const S=document.createElement("input");S.type="checkbox",S.checked=N,S.className="tools-pane-checkbox";const D=document.createElement("span");D.className="tools-pane-name",D.textContent="Type regions",b.appendChild(S),b.appendChild(D),b.addEventListener("click",v=>{v.target!==S&&(S.checked=!S.checked),N=S.checked,t.onToggleTypeHulls(N)}),m.appendChild(b);const $=document.createElement("div");$.className="tools-pane-row tools-pane-clickable";const f=document.createElement("input");f.type="checkbox",f.checked=p,f.className="tools-pane-checkbox";const X=document.createElement("span");X.className="tools-pane-name",X.textContent="Minimap",$.appendChild(f),$.appendChild(X),$.addEventListener("click",v=>{v.target!==f&&(f.checked=!f.checked),p=f.checked,t.onToggleMinimap(p)}),m.appendChild($),m.appendChild(T("Clustering",0,.15,.01,.05,v=>{t.onLayoutChange("clusterStrength",v)})),m.appendChild(T("Spacing",.5,3,.1,1,v=>{t.onLayoutChange("spacing",v)}));const H=document.createElement("div");H.className="tools-pane-export-row";const n=document.createElement("button");n.className="tools-pane-export-btn",n.textContent="Export PNG",n.addEventListener("click",()=>t.onExport("png"));const h=document.createElement("button");h.className="tools-pane-export-btn",h.textContent="Export SVG",h.addEventListener("click",()=>t.onExport("svg")),H.appendChild(n),H.appendChild(h),m.appendChild(H)}))}function T(C,w,m,i,l,u){const b=document.createElement("div");b.className="tools-pane-slider-row";const S=document.createElement("span");S.className="tools-pane-slider-label",S.textContent=C;const D=document.createElement("input");D.type="range",D.className="tools-pane-slider",D.min=String(w),D.max=String(m),D.step=String(i),D.value=String(l);const $=document.createElement("span");return $.className="tools-pane-slider-value",$.textContent=String(l),D.addEventListener("input",()=>{const f=parseFloat(D.value);$.textContent=f%1===0?String(f):f.toFixed(2),u(f)}),b.appendChild(S),b.appendChild(D),b.appendChild($),b}function B(C,w){const m=document.createElement("div");m.className="tools-pane-section";const i=document.createElement("div");return i.className="tools-pane-heading",i.textContent=C,m.appendChild(i),w(m),m}function z(C,w,m){const i=document.createElement("input");i.className="tools-pane-inline-input",i.value=w,i.type="text";const l=C.innerHTML;C.innerHTML="",C.classList.add("tools-pane-editing"),C.appendChild(i),i.focus(),i.select();function u(){const b=i.value.trim();C.classList.remove("tools-pane-editing"),b&&b!==w?m(b):C.innerHTML=l}i.addEventListener("keydown",b=>{b.key==="Enter"&&(b.preventDefault(),u()),b.key==="Escape"&&(C.innerHTML=l,C.classList.remove("tools-pane-editing"))}),i.addEventListener("blur",u)}function U(C){const w=new Map,m=new Map,i=new Map,l=new Set;for(const f of C.nodes)w.set(f.type,(w.get(f.type)??0)+1);for(const f of C.edges)m.set(f.type,(m.get(f.type)??0)+1),i.set(f.sourceId,(i.get(f.sourceId)??0)+1),i.set(f.targetId,(i.get(f.targetId)??0)+1),l.add(f.sourceId),l.add(f.targetId);const u=f=>st(f.properties)??f.id,b=C.nodes.filter(f=>!l.has(f.id)).map(f=>({id:f.id,label:u(f),type:f.type})),S=[...w.entries()].filter(([,f])=>f===1).map(([f])=>({name:f})),D=C.nodes.filter(f=>Object.keys(f.properties).length===0).map(f=>({id:f.id,label:f.id,type:f.type})),$=C.nodes.map(f=>({id:f.id,label:u(f),type:f.type,connections:i.get(f.id)??0})).filter(f=>f.connections>0).sort((f,X)=>X.connections-f.connections).slice(0,5);return{nodeCount:C.nodes.length,edgeCount:C.edges.length,types:[...w.entries()].sort((f,X)=>X[1]-f[1]).map(([f,X])=>({name:f,count:X})),edgeTypes:[...m.entries()].sort((f,X)=>X[1]-f[1]).map(([f,X])=>({name:f,count:X})),orphans:b,singletons:S,emptyNodes:D,mostConnected:$}}return{collapse(){y=!0,x.classList.add("hidden"),g.classList.remove("active")},setData(C){a=C,o=null,a&&a.nodes.length>0?(e=U(a),g.classList.remove("hidden"),I()):(e=null,g.classList.add("hidden"),x.classList.add("hidden"))}}}function st(c){for(const t of Object.values(c))if(typeof t=="string")return t;return null}const at=[{key:"/",alt:"Ctrl+K",description:"Focus search"},{key:"Ctrl+Z",description:"Undo"},{key:"Ctrl+Shift+Z",description:"Redo"},{key:"?",description:"Show this help"},{key:"Esc",description:"Close panel / clear search"},{key:"Click",description:"Select node"},{key:"Ctrl+Click",description:"Multi-select nodes"},{key:"Drag",description:"Pan canvas"},{key:"Scroll",description:"Zoom in/out"}];function ct(c){const t=document.createElement("div");t.className="shortcuts-overlay hidden";const a=document.createElement("div");a.className="shortcuts-modal";const e=document.createElement("h3");e.className="shortcuts-title",e.textContent="Keyboard Shortcuts";const y=document.createElement("div");y.className="shortcuts-list";for(const p of at){const g=document.createElement("div");g.className="shortcuts-row";const x=document.createElement("div");x.className="shortcuts-keys";const I=document.createElement("kbd");if(I.textContent=p.key,x.appendChild(I),p.alt){const B=document.createElement("span");B.className="shortcuts-or",B.textContent="or",x.appendChild(B);const z=document.createElement("kbd");z.textContent=p.alt,x.appendChild(z)}const T=document.createElement("span");T.className="shortcuts-desc",T.textContent=p.description,g.appendChild(x),g.appendChild(T),y.appendChild(g)}const o=document.createElement("button");o.className="shortcuts-close",o.textContent="×",a.appendChild(o),a.appendChild(e),a.appendChild(y),t.appendChild(a),c.appendChild(t);function s(){t.classList.remove("hidden")}function N(){t.classList.add("hidden")}return o.addEventListener("click",N),t.addEventListener("click",p=>{p.target===t&&N()}),{show:s,hide:N}}function it(c){const t=document.createElement("div");return t.className="empty-state",t.innerHTML=`
|
|
5
|
+
<div class="empty-state-content">
|
|
6
|
+
<div class="empty-state-icon">
|
|
7
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
8
|
+
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0022 16z"/>
|
|
9
|
+
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
|
10
|
+
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
|
11
|
+
</svg>
|
|
12
|
+
</div>
|
|
13
|
+
<h2 class="empty-state-title">No learning graphs yet</h2>
|
|
14
|
+
<p class="empty-state-desc">Connect Backpack to Claude, then start a conversation. Claude will build your first learning graph automatically.</p>
|
|
15
|
+
<div class="empty-state-setup">
|
|
16
|
+
<div class="empty-state-label">Add Backpack to Claude Code:</div>
|
|
17
|
+
<code class="empty-state-code">claude mcp add backpack-local -s user -- npx backpack-ontology@latest</code>
|
|
18
|
+
</div>
|
|
19
|
+
<p class="empty-state-hint">Press <kbd>?</kbd> for keyboard shortcuts</p>
|
|
20
|
+
</div>
|
|
21
|
+
`,c.appendChild(t),{show(){t.classList.remove("hidden")},hide(){t.classList.add("hidden")}}}const lt=30;function dt(){let c=[],t=[];return{push(a){c.push(JSON.stringify(a)),c.length>lt&&c.shift(),t=[]},undo(a){return c.length===0?null:(t.push(JSON.stringify(a)),JSON.parse(c.pop()))},redo(a){return t.length===0?null:(c.push(JSON.stringify(a)),JSON.parse(t.pop()))},canUndo(){return c.length>0},canRedo(){return t.length>0},clear(){c=[],t=[]}}}let ie="",E=null;async function rt(){const c=document.getElementById("canvas-container"),t=window.matchMedia("(prefers-color-scheme: dark)"),e=localStorage.getItem("backpack-theme")??(t.matches?"dark":"light");document.documentElement.setAttribute("data-theme",e);const y=document.createElement("button");y.className="theme-toggle",y.textContent=e==="light"?"☾":"☼",y.title="Toggle light/dark mode",y.addEventListener("click",()=>{const h=document.documentElement.getAttribute("data-theme")==="light"?"dark":"light";document.documentElement.setAttribute("data-theme",h),localStorage.setItem("backpack-theme",h),y.textContent=h==="light"?"☾":"☼"}),c.appendChild(y);const o=dt();async function s(){if(!ie||!E)return;E.metadata.updatedAt=new Date().toISOString(),await ke(ie,E),p.loadGraph(E),I.setLearningGraphData(E),T.setData(E);const n=await Le();l.setSummaries(n)}async function N(n){E=n,await ke(ie,E),p.loadGraph(E),I.setLearningGraphData(E),T.setData(E);const h=await Le();l.setSummaries(h)}let p;const g=Qe(c,{onUpdateNode(n,h){if(!E)return;o.push(E);const v=E.nodes.find(k=>k.id===n);v&&(v.properties={...v.properties,...h},v.updatedAt=new Date().toISOString(),s().then(()=>g.show([n],E)))},onChangeNodeType(n,h){if(!E)return;o.push(E);const v=E.nodes.find(k=>k.id===n);v&&(v.type=h,v.updatedAt=new Date().toISOString(),s().then(()=>g.show([n],E)))},onDeleteNode(n){E&&(o.push(E),E.nodes=E.nodes.filter(h=>h.id!==n),E.edges=E.edges.filter(h=>h.sourceId!==n&&h.targetId!==n),s())},onDeleteEdge(n){var v;if(!E)return;o.push(E);const h=(v=E.edges.find(k=>k.id===n))==null?void 0:v.sourceId;E.edges=E.edges.filter(k=>k.id!==n),s().then(()=>{h&&E&&g.show([h],E)})},onAddProperty(n,h,v){if(!E)return;o.push(E);const k=E.nodes.find(F=>F.id===n);k&&(k.properties[h]=v,k.updatedAt=new Date().toISOString(),s().then(()=>g.show([n],E)))}},n=>{p.panToNode(n)}),x=window.matchMedia("(max-width: 768px)");p=Je(c,n=>{n&&n.length>0&&E?(g.show(n,E),x.matches&&T.collapse(),S(ie,n)):(g.hide(),ie&&S(ie))});const I=nt(c),T=ot(c,{onFilterByType(n){if(E)if(n===null)p.setFilteredNodeIds(null);else{const h=new Set(((E==null?void 0:E.nodes)??[]).filter(v=>v.type===n).map(v=>v.id));p.setFilteredNodeIds(h)}},onNavigateToNode(n){p.panToNode(n),E&&g.show([n],E)},onRenameNodeType(n,h){if(E){o.push(E);for(const v of E.nodes)v.type===n&&(v.type=h,v.updatedAt=new Date().toISOString());s()}},onRenameEdgeType(n,h){if(E){o.push(E);for(const v of E.edges)v.type===n&&(v.type=h);s()}},onToggleEdgeLabels(n){p.setEdgeLabels(n)},onToggleTypeHulls(n){p.setTypeHulls(n)},onToggleMinimap(n){p.setMinimap(n)},onLayoutChange(n,h){Ge({[n]:h}),p.reheat()},onExport(n){const h=p.exportImage(n);if(!h)return;const v=document.createElement("a");v.download=`${ie||"graph"}.${n}`,v.href=h,v.click()},onOpen(){x.matches&&g.hide()}}),B=document.createElement("div");B.className="canvas-top-bar";const z=document.createElement("div");z.className="canvas-top-left";const U=document.createElement("div");U.className="canvas-top-center";const C=document.createElement("div");C.className="canvas-top-right";const w=c.querySelector(".tools-pane-toggle");w&&z.appendChild(w);const m=c.querySelector(".search-overlay");m&&U.appendChild(m);const i=c.querySelector(".zoom-controls");i&&C.appendChild(i),C.appendChild(y),B.appendChild(z),B.appendChild(U),B.appendChild(C),c.appendChild(B),I.onFilterChange(n=>{p.setFilteredNodeIds(n)}),I.onNodeSelect(n=>{p.panToNode(n),E&&g.show([n],E)});const l=Xe(document.getElementById("sidebar"),{onSelect:n=>$(n),onRename:async(n,h)=>{await Ye(n,h),ie===n&&(ie=h);const v=await Le();l.setSummaries(v),l.setActive(ie),ie===h&&(E=await Se(h),p.loadGraph(E),I.setLearningGraphData(E),T.setData(E))}}),u=ct(c),b=it(c);function S(n,h){const v="#"+encodeURIComponent(n)+(h!=null&&h.length?"?node="+h.map(encodeURIComponent).join(","):"");history.replaceState(null,"",v)}function D(){const n=window.location.hash.slice(1);if(!n)return{graph:null,nodes:[]};const[h,v]=n.split("?"),k=h?decodeURIComponent(h):null;let F=[];if(v){const _=new URLSearchParams(v).get("node");_&&(F=_.split(",").map(decodeURIComponent))}return{graph:k,nodes:F}}async function $(n,h){if(ie=n,l.setActive(n),g.hide(),I.clear(),o.clear(),E=await Se(n),p.loadGraph(E),I.setLearningGraphData(E),T.setData(E),b.hide(),S(n),h!=null&&h.length&&E){const v=h.filter(k=>E.nodes.some(F=>F.id===k));v.length&&setTimeout(()=>{p.panToNodes(v),E&&g.show(v,E),S(n,v)},500)}}const f=await Le();l.setSummaries(f);const X=D(),H=X.graph&&f.some(n=>n.name===X.graph)?X.graph:f.length>0?f[0].name:null;H?await $(H,X.nodes.length?X.nodes:void 0):b.show(),document.addEventListener("keydown",n=>{if(!(n.target instanceof HTMLInputElement||n.target instanceof HTMLTextAreaElement))if(n.key==="/"||n.key==="k"&&(n.metaKey||n.ctrlKey))n.preventDefault(),I.focus();else if(n.key==="z"&&(n.metaKey||n.ctrlKey)&&n.shiftKey){if(n.preventDefault(),E){const h=o.redo(E);h&&N(h)}}else if(n.key==="z"&&(n.metaKey||n.ctrlKey)){if(n.preventDefault(),E){const h=o.undo(E);h&&N(h)}}else n.key==="?"?u.show():n.key==="Escape"&&u.hide()}),window.addEventListener("hashchange",()=>{const n=D();if(n.graph&&n.graph!==ie)$(n.graph,n.nodes.length?n.nodes:void 0);else if(n.graph&&n.nodes.length&&E){const h=n.nodes.filter(v=>E.nodes.some(k=>k.id===v));h.length&&(p.panToNodes(h),g.show(h,E))}})}rt();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*{margin:0;padding:0;box-sizing:border-box}:root{--bg: #141414;--bg-surface: #1a1a1a;--bg-hover: #222222;--bg-active: #2a2a2a;--bg-elevated: #1e1e1e;--bg-inset: #111111;--border: #2a2a2a;--text: #d4d4d4;--text-strong: #e5e5e5;--text-muted: #737373;--text-dim: #525252;--accent: #d4a27f;--accent-hover: #e8b898;--badge-text: #141414;--glass-bg: rgba(20, 20, 20, .85);--glass-border: rgba(255, 255, 255, .08);--chip-bg: rgba(42, 42, 42, .7);--chip-bg-active: rgba(42, 42, 42, .9);--chip-bg-hover: rgba(50, 50, 50, .9);--chip-border-active: rgba(255, 255, 255, .06);--shadow: rgba(0, 0, 0, .6);--shadow-strong: rgba(0, 0, 0, .5);--canvas-edge: rgba(255, 255, 255, .08);--canvas-edge-highlight: rgba(212, 162, 127, .5);--canvas-edge-dim: rgba(255, 255, 255, .03);--canvas-edge-label: rgba(255, 255, 255, .2);--canvas-edge-label-highlight: rgba(212, 162, 127, .7);--canvas-edge-label-dim: rgba(255, 255, 255, .05);--canvas-arrow: rgba(255, 255, 255, .12);--canvas-arrow-highlight: rgba(212, 162, 127, .5);--canvas-node-label: #a3a3a3;--canvas-node-label-dim: rgba(212, 212, 212, .2);--canvas-type-badge: rgba(115, 115, 115, .5);--canvas-type-badge-dim: rgba(115, 115, 115, .15);--canvas-selection-border: #d4d4d4;--canvas-node-border: rgba(255, 255, 255, .15)}[data-theme=light]{--bg: #f5f5f4;--bg-surface: #fafaf9;--bg-hover: #f0efee;--bg-active: #e7e5e4;--bg-elevated: #f0efee;--bg-inset: #e7e5e4;--border: #d6d3d1;--text: #292524;--text-strong: #1c1917;--text-muted: #78716c;--text-dim: #a8a29e;--accent: #c17856;--accent-hover: #b07a5e;--badge-text: #fafaf9;--glass-bg: rgba(250, 250, 249, .85);--glass-border: rgba(0, 0, 0, .08);--chip-bg: rgba(214, 211, 209, .5);--chip-bg-active: rgba(214, 211, 209, .8);--chip-bg-hover: rgba(200, 197, 195, .8);--chip-border-active: rgba(0, 0, 0, .08);--shadow: rgba(0, 0, 0, .1);--shadow-strong: rgba(0, 0, 0, .15);--canvas-edge: rgba(0, 0, 0, .1);--canvas-edge-highlight: rgba(193, 120, 86, .6);--canvas-edge-dim: rgba(0, 0, 0, .03);--canvas-edge-label: rgba(0, 0, 0, .25);--canvas-edge-label-highlight: rgba(193, 120, 86, .8);--canvas-edge-label-dim: rgba(0, 0, 0, .06);--canvas-arrow: rgba(0, 0, 0, .15);--canvas-arrow-highlight: rgba(193, 120, 86, .6);--canvas-node-label: #57534e;--canvas-node-label-dim: rgba(87, 83, 78, .2);--canvas-type-badge: rgba(87, 83, 78, .5);--canvas-type-badge-dim: rgba(87, 83, 78, .15);--canvas-selection-border: #292524;--canvas-node-border: rgba(0, 0, 0, .1)}body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);overflow:hidden}#app{display:flex;height:100vh;width:100vw}#sidebar{width:280px;min-width:280px;background:var(--bg-surface);border-right:1px solid var(--border);display:flex;flex-direction:column;padding:16px;overflow-y:auto}#sidebar h2{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:14px}#sidebar input{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px;outline:none;margin-bottom:12px}#sidebar input:focus{border-color:var(--accent)}#sidebar input::placeholder{color:var(--text-dim)}#ontology-list{list-style:none;display:flex;flex-direction:column;gap:2px}.ontology-item{padding:10px 12px;border-radius:6px;cursor:pointer;transition:background .15s}.ontology-item:hover{background:var(--bg-hover)}.ontology-item.active{background:var(--bg-active)}.ontology-item .name{display:block;font-size:13px;font-weight:500;color:var(--text)}.ontology-item .stats{display:block;font-size:11px;color:var(--text-dim);margin-top:2px}.sidebar-edit-btn{position:absolute;right:8px;top:10px;background:none;border:none;color:var(--text-dim);font-size:11px;cursor:pointer;opacity:0;transition:opacity .1s}.ontology-item{position:relative}.ontology-item:hover .sidebar-edit-btn{opacity:.7}.sidebar-edit-btn:hover{opacity:1!important;color:var(--text)}.sidebar-rename-input{background:transparent;border:none;border-bottom:1px solid var(--accent);color:var(--text);font:inherit;font-size:13px;font-weight:500;outline:none;width:100%;padding:0}.sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);text-align:center}.sidebar-footer a{display:block;font-size:12px;font-weight:500;color:var(--accent);text-decoration:none;margin-bottom:4px}.sidebar-footer a:hover{color:var(--accent-hover)}.sidebar-footer span{display:block;font-size:10px;color:var(--text-dim)}.canvas-top-bar{position:absolute;top:16px;left:16px;right:16px;z-index:30;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none}.canvas-top-left,.canvas-top-center,.canvas-top-right{pointer-events:auto;display:flex;align-items:center;gap:4px}.canvas-top-center{flex:1;justify-content:center}.theme-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.theme-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.zoom-controls{display:flex;gap:4px}.zoom-btn{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.zoom-btn:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}#canvas-container{flex:1;position:relative;overflow:hidden;touch-action:none}#graph-canvas{position:absolute;top:0;left:0;touch-action:none;width:100%;height:100%;cursor:grab}#graph-canvas:active{cursor:grabbing}.search-overlay{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;max-height:calc(100vh - 48px);pointer-events:none}.search-overlay>*{pointer-events:auto}.search-overlay.hidden{display:none}.search-input-wrap{position:relative;display:flex;align-items:center;gap:6px;width:380px;max-width:calc(100vw - 340px)}.search-input{flex:1;min-width:0;padding:10px 36px 10px 16px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text);font-size:14px;outline:none;transition:border-color .15s,box-shadow .15s}.search-input:focus{border-color:#d4a27f66;box-shadow:0 0 0 3px #d4a27f1a}.search-input::placeholder{color:var(--text-dim)}.search-kbd{position:absolute;right:10px;top:50%;transform:translateY(-50%);padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg-surface);color:var(--text-dim);font-size:11px;font-family:monospace;pointer-events:none}.search-kbd.hidden{display:none}.search-results{list-style:none;width:380px;max-width:calc(100vw - 340px);background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:10px;overflow:hidden;box-shadow:0 8px 32px var(--shadow-strong)}.search-results.hidden{display:none}.search-result-item{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;transition:background .1s}.search-result-item:hover{background:var(--bg-hover)}.search-result-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.search-result-label{font-size:13px;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-result-type{font-size:11px;color:var(--text-dim);flex-shrink:0}.chip-toggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text-dim);cursor:pointer;transition:border-color .15s,color .15s;pointer-events:auto}.chip-toggle:hover{border-color:#d4a27f4d;color:var(--text)}.chip-toggle.active{border-color:#d4a27f66;color:var(--accent)}.type-chips{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;max-width:500px;max-height:200px;overflow-y:auto;padding:4px;border-radius:10px}.type-chips.hidden{display:none}.type-chip{display:flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid transparent;border-radius:12px;background:var(--chip-bg);color:var(--text-dim);font-size:11px;cursor:pointer;transition:all .15s;white-space:nowrap}.type-chip.active{background:var(--chip-bg-active);color:var(--text-muted);border-color:var(--chip-border-active)}.type-chip:hover{background:var(--chip-bg-hover)}.type-chip-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.type-chip:not(.active) .type-chip-dot{opacity:.3}.info-panel{position:absolute;top:56px;right:16px;width:360px;max-height:calc(100vh - 72px);background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;overflow-y:auto;padding:20px;z-index:10;box-shadow:0 8px 32px var(--shadow);transition:top .25s ease,right .25s ease,bottom .25s ease,left .25s ease,width .25s ease,max-height .25s ease,border-radius .25s ease}.info-panel.hidden{display:none}.info-panel.info-panel-maximized{top:0;right:0;bottom:0;left:0;width:auto;max-height:none;border-radius:0;z-index:40}.info-panel-toolbar{position:absolute;top:12px;right:14px;display:flex;align-items:center;gap:2px;z-index:1}.info-toolbar-btn{background:none;border:none;color:var(--text-muted);font-size:16px;cursor:pointer;padding:4px 6px;line-height:1;border-radius:4px;transition:color .15s,background .15s}.info-toolbar-btn:hover:not(:disabled){color:var(--text);background:var(--bg-hover)}.info-toolbar-btn:disabled{color:var(--text-dim);cursor:default;opacity:.3}.info-close-btn{font-size:20px}.info-connection-link{cursor:pointer;transition:background .15s}.info-connection-link:hover{background:var(--bg-active)}.info-connection-link .info-target{color:var(--accent);text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color .15s}.info-connection-link:hover .info-target{text-decoration-color:var(--accent)}.info-header{margin-bottom:16px}.info-type-badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;color:var(--badge-text);margin-bottom:8px}.info-label{font-size:18px;font-weight:600;color:var(--text-strong);margin-bottom:4px;word-break:break-word}.info-id{display:block;font-size:11px;color:var(--text-dim);font-family:monospace}.info-section{margin-bottom:16px}.info-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border)}.info-props{display:grid;grid-template-columns:auto 1fr;gap:4px 12px}.info-props dt{font-size:12px;color:var(--text-muted);padding-top:2px}.info-props dd{font-size:12px;color:var(--text);word-break:break-word;display:flex;align-items:center;gap:4px}.info-value{white-space:pre-wrap}.info-array{display:flex;flex-wrap:wrap;gap:4px}.info-tag{display:inline-block;padding:2px 8px;background:var(--bg-hover);border-radius:4px;font-size:11px;color:var(--text-muted)}.info-json{font-size:11px;font-family:monospace;color:var(--text-muted);background:var(--bg-inset);padding:6px 8px;border-radius:4px;overflow-x:auto;white-space:pre}.info-connections{list-style:none;display:flex;flex-direction:column;gap:6px}.info-connection{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--bg-elevated);border-radius:6px;font-size:12px;flex-wrap:wrap}.info-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}.tools-pane-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.tools-pane-toggle.hidden{display:none}.tools-pane-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.tools-pane-toggle.active{color:var(--accent);border-color:#d4a27f66}.tools-pane-content{position:absolute;top:56px;left:16px;z-index:20;width:200px;max-height:calc(100vh - 72px);overflow-y:auto;background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:12px;box-shadow:0 8px 32px var(--shadow)}.tools-pane-content.hidden{display:none}.tools-pane-section{margin-bottom:12px}.tools-pane-section:last-child{margin-bottom:0}.tools-pane-heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);margin-bottom:6px}.tools-pane-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:12px}.tools-pane-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.tools-pane-name{flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tools-pane-count{color:var(--text-dim);font-size:11px;flex-shrink:0}.tools-pane-summary{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-muted);padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--border)}.tools-pane-sep{color:var(--text-dim)}.tools-pane-clickable{cursor:pointer;border-radius:4px;padding:3px 4px;margin:0 -4px;transition:background .1s}.tools-pane-clickable:hover{background:var(--bg-hover)}.tools-pane-clickable.active{background:var(--bg-hover);outline:1px solid var(--border)}.tools-pane-badge{font-size:9px;color:var(--accent);flex-shrink:0;opacity:.8}.tools-pane-issue .tools-pane-name{color:var(--text-muted)}.tools-pane-more{font-size:10px;color:var(--text-dim);padding:4px 0 0}.tools-pane-edit{background:none;border:none;color:var(--text-dim);font-size:11px;cursor:pointer;padding:0 2px;opacity:0;transition:opacity .1s,color .1s;flex-shrink:0}.tools-pane-row:hover .tools-pane-edit{opacity:1}.tools-pane-edit:hover{color:var(--accent)}.tools-pane-editing{background:none!important}.tools-pane-inline-input{width:100%;background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:12px;padding:2px 6px;outline:none}.tools-pane-slider-row{display:flex;align-items:center;gap:6px;padding:4px 0}.tools-pane-slider-label{font-size:11px;color:var(--text-muted);white-space:nowrap;min-width:56px}.tools-pane-slider{flex:1;min-width:0;height:4px;accent-color:var(--accent);cursor:pointer}.tools-pane-slider-value{font-size:10px;color:var(--text-dim);min-width:28px;text-align:right;font-family:monospace}.tools-pane-checkbox{width:14px;height:14px;accent-color:var(--accent);cursor:pointer;flex-shrink:0}.tools-pane-export-row{display:flex;gap:4px;margin-top:6px}.tools-pane-export-btn{flex:1;padding:4px 8px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s}.tools-pane-export-btn:hover{color:var(--text);border-color:var(--text-muted)}.tools-pane-empty{font-size:11px;color:var(--text-dim);text-align:center;padding:8px 0}.empty-state{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;z-index:5;pointer-events:none}.empty-state.hidden{display:none}.empty-state-content{text-align:center;max-width:420px;padding:40px 24px}.empty-state-icon{color:var(--text-dim);margin-bottom:16px}.empty-state-title{font-size:18px;font-weight:600;color:var(--text);margin-bottom:8px}.empty-state-desc{font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:20px}.empty-state-setup{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:16px}.empty-state-label{font-size:11px;color:var(--text-dim);margin-bottom:8px}.empty-state-code{display:block;font-size:12px;color:var(--accent);font-family:monospace;word-break:break-all}.empty-state-hint{font-size:11px;color:var(--text-dim)}.empty-state-hint kbd{padding:1px 5px;border:1px solid var(--border);border-radius:3px;background:var(--bg-surface);font-family:monospace;font-size:11px}.shortcuts-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.shortcuts-overlay.hidden{display:none}.shortcuts-modal{background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;padding:24px;min-width:300px;max-width:400px;box-shadow:0 16px 48px var(--shadow);position:relative}.shortcuts-close{position:absolute;top:12px;right:14px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;padding:0 4px;line-height:1}.shortcuts-close:hover{color:var(--text)}.shortcuts-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:16px}.shortcuts-list{display:flex;flex-direction:column;gap:8px}.shortcuts-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.shortcuts-keys{display:flex;align-items:center;gap:4px}.shortcuts-keys kbd{padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text);font-size:11px;font-family:monospace}.shortcuts-or{font-size:10px;color:var(--text-dim)}.shortcuts-desc{font-size:12px;color:var(--text-muted)}@media(max-width:768px){#app{flex-direction:column}#sidebar{width:100%;min-width:0;max-height:35vh;border-right:none;border-bottom:1px solid var(--border)}.info-panel{top:auto;bottom:72px;right:8px;left:8px;width:auto;max-height:calc(100% - 200px);overflow-y:auto}.info-panel.info-panel-maximized{bottom:0;left:0;right:0}.canvas-top-bar{top:8px;left:8px;right:8px}.tools-pane-content{top:48px;left:8px;width:160px;max-height:calc(100% - 200px);max-width:calc(100vw - 24px)}.tools-pane-edit{opacity:.6}}
|
package/dist/app/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Backpack Viewer</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CR8Iepyw.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-FMdnOuXa.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app">
|
package/dist/canvas.d.ts
CHANGED
|
@@ -3,5 +3,11 @@ export declare function initCanvas(container: HTMLElement, onNodeClick?: (nodeId
|
|
|
3
3
|
loadGraph(data: LearningGraphData): void;
|
|
4
4
|
setFilteredNodeIds(ids: Set<string> | null): void;
|
|
5
5
|
panToNode(nodeId: string): void;
|
|
6
|
+
panToNodes(nodeIds: string[]): void;
|
|
7
|
+
setEdgeLabels(visible: boolean): void;
|
|
8
|
+
setTypeHulls(visible: boolean): void;
|
|
9
|
+
setMinimap(visible: boolean): void;
|
|
10
|
+
reheat(): void;
|
|
11
|
+
exportImage(format: "png" | "svg"): string;
|
|
6
12
|
destroy(): void;
|
|
7
13
|
};
|
package/dist/canvas.js
CHANGED
|
@@ -16,6 +16,9 @@ export function initCanvas(container, onNodeClick) {
|
|
|
16
16
|
let animFrame = 0;
|
|
17
17
|
let selectedNodeIds = new Set();
|
|
18
18
|
let filteredNodeIds = null; // null = no filter (show all)
|
|
19
|
+
let showEdgeLabels = true;
|
|
20
|
+
let showTypeHulls = true;
|
|
21
|
+
let showMinimap = true;
|
|
19
22
|
// Pan animation state
|
|
20
23
|
let panTarget = null;
|
|
21
24
|
let panStart = null;
|
|
@@ -79,6 +82,51 @@ export function initCanvas(container, onNodeClick) {
|
|
|
79
82
|
ctx.save();
|
|
80
83
|
ctx.translate(-camera.x * camera.scale, -camera.y * camera.scale);
|
|
81
84
|
ctx.scale(camera.scale, camera.scale);
|
|
85
|
+
// Draw type hulls (shaded regions behind same-type nodes)
|
|
86
|
+
if (showTypeHulls) {
|
|
87
|
+
const typeGroups = new Map();
|
|
88
|
+
for (const node of state.nodes) {
|
|
89
|
+
if (filteredNodeIds !== null && !filteredNodeIds.has(node.id))
|
|
90
|
+
continue;
|
|
91
|
+
const group = typeGroups.get(node.type) ?? [];
|
|
92
|
+
group.push(node);
|
|
93
|
+
typeGroups.set(node.type, group);
|
|
94
|
+
}
|
|
95
|
+
for (const [type, nodes] of typeGroups) {
|
|
96
|
+
if (nodes.length < 2)
|
|
97
|
+
continue;
|
|
98
|
+
const color = getColor(type);
|
|
99
|
+
const padding = NODE_RADIUS * 2.5;
|
|
100
|
+
// Compute bounding box
|
|
101
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
102
|
+
for (const n of nodes) {
|
|
103
|
+
if (n.x < minX)
|
|
104
|
+
minX = n.x;
|
|
105
|
+
if (n.y < minY)
|
|
106
|
+
minY = n.y;
|
|
107
|
+
if (n.x > maxX)
|
|
108
|
+
maxX = n.x;
|
|
109
|
+
if (n.y > maxY)
|
|
110
|
+
maxY = n.y;
|
|
111
|
+
}
|
|
112
|
+
ctx.beginPath();
|
|
113
|
+
const rx = (maxX - minX) / 2 + padding;
|
|
114
|
+
const ry = (maxY - minY) / 2 + padding;
|
|
115
|
+
const cx = (minX + maxX) / 2;
|
|
116
|
+
const cy = (minY + maxY) / 2;
|
|
117
|
+
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
118
|
+
ctx.fillStyle = color;
|
|
119
|
+
ctx.globalAlpha = 0.05;
|
|
120
|
+
ctx.fill();
|
|
121
|
+
ctx.strokeStyle = color;
|
|
122
|
+
ctx.globalAlpha = 0.12;
|
|
123
|
+
ctx.lineWidth = 1;
|
|
124
|
+
ctx.setLineDash([4, 4]);
|
|
125
|
+
ctx.stroke();
|
|
126
|
+
ctx.setLineDash([]);
|
|
127
|
+
ctx.globalAlpha = 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
82
130
|
// Draw edges
|
|
83
131
|
for (const edge of state.edges) {
|
|
84
132
|
const source = state.nodeMap.get(edge.sourceId);
|
|
@@ -114,17 +162,19 @@ export function initCanvas(container, onNodeClick) {
|
|
|
114
162
|
// Arrowhead
|
|
115
163
|
drawArrowhead(source.x, source.y, target.x, target.y, highlighted, arrowColor, arrowHighlight);
|
|
116
164
|
// Edge label at midpoint
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
if (showEdgeLabels) {
|
|
166
|
+
const mx = (source.x + target.x) / 2;
|
|
167
|
+
const my = (source.y + target.y) / 2;
|
|
168
|
+
ctx.fillStyle = highlighted
|
|
169
|
+
? edgeLabelHighlight
|
|
170
|
+
: edgeDimmed
|
|
171
|
+
? edgeLabelDim
|
|
172
|
+
: edgeLabel;
|
|
173
|
+
ctx.font = "9px system-ui, sans-serif";
|
|
174
|
+
ctx.textAlign = "center";
|
|
175
|
+
ctx.textBaseline = "bottom";
|
|
176
|
+
ctx.fillText(edge.type, mx, my - 4);
|
|
177
|
+
}
|
|
128
178
|
}
|
|
129
179
|
// Draw nodes
|
|
130
180
|
for (const node of state.nodes) {
|
|
@@ -173,6 +223,87 @@ export function initCanvas(container, onNodeClick) {
|
|
|
173
223
|
}
|
|
174
224
|
ctx.restore();
|
|
175
225
|
ctx.restore();
|
|
226
|
+
// Minimap
|
|
227
|
+
if (showMinimap && state.nodes.length > 1) {
|
|
228
|
+
drawMinimap();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function drawMinimap() {
|
|
232
|
+
if (!state)
|
|
233
|
+
return;
|
|
234
|
+
const mapW = 140;
|
|
235
|
+
const mapH = 100;
|
|
236
|
+
const mapPad = 8;
|
|
237
|
+
const mapX = canvas.clientWidth - mapW - 16;
|
|
238
|
+
const mapY = canvas.clientHeight - mapH - 16;
|
|
239
|
+
// Compute graph bounds
|
|
240
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
241
|
+
for (const n of state.nodes) {
|
|
242
|
+
if (n.x < minX)
|
|
243
|
+
minX = n.x;
|
|
244
|
+
if (n.y < minY)
|
|
245
|
+
minY = n.y;
|
|
246
|
+
if (n.x > maxX)
|
|
247
|
+
maxX = n.x;
|
|
248
|
+
if (n.y > maxY)
|
|
249
|
+
maxY = n.y;
|
|
250
|
+
}
|
|
251
|
+
const gw = maxX - minX || 1;
|
|
252
|
+
const gh = maxY - minY || 1;
|
|
253
|
+
const scale = Math.min((mapW - mapPad * 2) / gw, (mapH - mapPad * 2) / gh);
|
|
254
|
+
const offsetX = mapX + mapPad + ((mapW - mapPad * 2) - gw * scale) / 2;
|
|
255
|
+
const offsetY = mapY + mapPad + ((mapH - mapPad * 2) - gh * scale) / 2;
|
|
256
|
+
ctx.save();
|
|
257
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
258
|
+
// Background
|
|
259
|
+
ctx.fillStyle = cssVar("--bg-surface") || "#1a1a1a";
|
|
260
|
+
ctx.globalAlpha = 0.85;
|
|
261
|
+
ctx.beginPath();
|
|
262
|
+
ctx.roundRect(mapX, mapY, mapW, mapH, 8);
|
|
263
|
+
ctx.fill();
|
|
264
|
+
ctx.strokeStyle = cssVar("--border") || "#2a2a2a";
|
|
265
|
+
ctx.globalAlpha = 1;
|
|
266
|
+
ctx.lineWidth = 1;
|
|
267
|
+
ctx.stroke();
|
|
268
|
+
// Edges
|
|
269
|
+
ctx.globalAlpha = 0.15;
|
|
270
|
+
ctx.strokeStyle = cssVar("--canvas-edge") || "#555";
|
|
271
|
+
ctx.lineWidth = 0.5;
|
|
272
|
+
for (const edge of state.edges) {
|
|
273
|
+
const src = state.nodeMap.get(edge.sourceId);
|
|
274
|
+
const tgt = state.nodeMap.get(edge.targetId);
|
|
275
|
+
if (!src || !tgt || edge.sourceId === edge.targetId)
|
|
276
|
+
continue;
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
ctx.moveTo(offsetX + (src.x - minX) * scale, offsetY + (src.y - minY) * scale);
|
|
279
|
+
ctx.lineTo(offsetX + (tgt.x - minX) * scale, offsetY + (tgt.y - minY) * scale);
|
|
280
|
+
ctx.stroke();
|
|
281
|
+
}
|
|
282
|
+
// Nodes
|
|
283
|
+
ctx.globalAlpha = 0.8;
|
|
284
|
+
for (const node of state.nodes) {
|
|
285
|
+
const nx = offsetX + (node.x - minX) * scale;
|
|
286
|
+
const ny = offsetY + (node.y - minY) * scale;
|
|
287
|
+
ctx.beginPath();
|
|
288
|
+
ctx.arc(nx, ny, 2, 0, Math.PI * 2);
|
|
289
|
+
ctx.fillStyle = getColor(node.type);
|
|
290
|
+
ctx.fill();
|
|
291
|
+
}
|
|
292
|
+
// Viewport rectangle
|
|
293
|
+
const vx1 = camera.x;
|
|
294
|
+
const vy1 = camera.y;
|
|
295
|
+
const vx2 = camera.x + canvas.clientWidth / camera.scale;
|
|
296
|
+
const vy2 = camera.y + canvas.clientHeight / camera.scale;
|
|
297
|
+
const rx = offsetX + (vx1 - minX) * scale;
|
|
298
|
+
const ry = offsetY + (vy1 - minY) * scale;
|
|
299
|
+
const rw = (vx2 - vx1) * scale;
|
|
300
|
+
const rh = (vy2 - vy1) * scale;
|
|
301
|
+
ctx.globalAlpha = 0.3;
|
|
302
|
+
ctx.strokeStyle = cssVar("--accent") || "#d4a27f";
|
|
303
|
+
ctx.lineWidth = 1.5;
|
|
304
|
+
ctx.strokeRect(Math.max(mapX, Math.min(rx, mapX + mapW)), Math.max(mapY, Math.min(ry, mapY + mapH)), Math.min(rw, mapW), Math.min(rh, mapH));
|
|
305
|
+
ctx.globalAlpha = 1;
|
|
306
|
+
ctx.restore();
|
|
176
307
|
}
|
|
177
308
|
function drawArrowhead(sx, sy, tx, ty, highlighted, arrowColor, arrowHighlight) {
|
|
178
309
|
const angle = Math.atan2(ty - sy, tx - sx);
|
|
@@ -195,10 +326,12 @@ export function initCanvas(container, onNodeClick) {
|
|
|
195
326
|
ctx.strokeStyle = highlighted ? edgeHighlight : edgeColor;
|
|
196
327
|
ctx.lineWidth = highlighted ? 2.5 : 1.5;
|
|
197
328
|
ctx.stroke();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
329
|
+
if (showEdgeLabels) {
|
|
330
|
+
ctx.fillStyle = highlighted ? labelHighlight : labelColor;
|
|
331
|
+
ctx.font = "9px system-ui, sans-serif";
|
|
332
|
+
ctx.textAlign = "center";
|
|
333
|
+
ctx.fillText(type, cx, cy - 18);
|
|
334
|
+
}
|
|
202
335
|
}
|
|
203
336
|
// --- Simulation loop ---
|
|
204
337
|
function animatePan() {
|
|
@@ -485,25 +618,112 @@ export function initCanvas(container, onNodeClick) {
|
|
|
485
618
|
render();
|
|
486
619
|
},
|
|
487
620
|
panToNode(nodeId) {
|
|
488
|
-
|
|
621
|
+
this.panToNodes([nodeId]);
|
|
622
|
+
},
|
|
623
|
+
panToNodes(nodeIds) {
|
|
624
|
+
if (!state || nodeIds.length === 0)
|
|
489
625
|
return;
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
626
|
+
const nodes = nodeIds.map((id) => state.nodeMap.get(id)).filter(Boolean);
|
|
627
|
+
if (nodes.length === 0)
|
|
492
628
|
return;
|
|
493
|
-
selectedNodeIds = new Set(
|
|
494
|
-
onNodeClick?.(
|
|
629
|
+
selectedNodeIds = new Set(nodeIds);
|
|
630
|
+
onNodeClick?.(nodeIds);
|
|
495
631
|
const w = canvas.clientWidth;
|
|
496
632
|
const h = canvas.clientHeight;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
633
|
+
if (nodes.length === 1) {
|
|
634
|
+
panStart = { x: camera.x, y: camera.y, time: performance.now() };
|
|
635
|
+
panTarget = {
|
|
636
|
+
x: nodes[0].x - w / (2 * camera.scale),
|
|
637
|
+
y: nodes[0].y - h / (2 * camera.scale),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// Fit all nodes in view with padding
|
|
642
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
643
|
+
for (const n of nodes) {
|
|
644
|
+
if (n.x < minX)
|
|
645
|
+
minX = n.x;
|
|
646
|
+
if (n.y < minY)
|
|
647
|
+
minY = n.y;
|
|
648
|
+
if (n.x > maxX)
|
|
649
|
+
maxX = n.x;
|
|
650
|
+
if (n.y > maxY)
|
|
651
|
+
maxY = n.y;
|
|
652
|
+
}
|
|
653
|
+
const pad = NODE_RADIUS * 4;
|
|
654
|
+
const bw = maxX - minX + pad * 2;
|
|
655
|
+
const bh = maxY - minY + pad * 2;
|
|
656
|
+
const fitScale = Math.min(w / bw, h / bh, camera.scale);
|
|
657
|
+
camera.scale = fitScale;
|
|
658
|
+
const cx = (minX + maxX) / 2;
|
|
659
|
+
const cy = (minY + maxY) / 2;
|
|
660
|
+
panStart = { x: camera.x, y: camera.y, time: performance.now() };
|
|
661
|
+
panTarget = {
|
|
662
|
+
x: cx - w / (2 * camera.scale),
|
|
663
|
+
y: cy - h / (2 * camera.scale),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
502
666
|
animatePan();
|
|
503
667
|
},
|
|
668
|
+
setEdgeLabels(visible) {
|
|
669
|
+
showEdgeLabels = visible;
|
|
670
|
+
render();
|
|
671
|
+
},
|
|
672
|
+
setTypeHulls(visible) {
|
|
673
|
+
showTypeHulls = visible;
|
|
674
|
+
render();
|
|
675
|
+
},
|
|
676
|
+
setMinimap(visible) {
|
|
677
|
+
showMinimap = visible;
|
|
678
|
+
render();
|
|
679
|
+
},
|
|
680
|
+
reheat() {
|
|
681
|
+
alpha = 0.5;
|
|
682
|
+
cancelAnimationFrame(animFrame);
|
|
683
|
+
simulate();
|
|
684
|
+
},
|
|
685
|
+
exportImage(format) {
|
|
686
|
+
if (!state)
|
|
687
|
+
return "";
|
|
688
|
+
// Use the actual canvas pixel dimensions (already scaled by dpr)
|
|
689
|
+
const pw = canvas.width;
|
|
690
|
+
const ph = canvas.height;
|
|
691
|
+
if (format === "png") {
|
|
692
|
+
const exportCanvas = document.createElement("canvas");
|
|
693
|
+
exportCanvas.width = pw;
|
|
694
|
+
exportCanvas.height = ph;
|
|
695
|
+
const ectx = exportCanvas.getContext("2d");
|
|
696
|
+
// Draw background
|
|
697
|
+
ectx.fillStyle = cssVar("--bg") || "#141414";
|
|
698
|
+
ectx.fillRect(0, 0, pw, ph);
|
|
699
|
+
// Copy current canvas pixels 1:1
|
|
700
|
+
ectx.drawImage(canvas, 0, 0);
|
|
701
|
+
// Watermark (scale font to match pixel density)
|
|
702
|
+
drawWatermark(ectx, pw, ph);
|
|
703
|
+
return exportCanvas.toDataURL("image/png");
|
|
704
|
+
}
|
|
705
|
+
// SVG: embed the canvas as a PNG image with text overlay
|
|
706
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
707
|
+
const fontSize = Math.max(16, Math.round(pw / 80));
|
|
708
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${pw}" height="${ph}">
|
|
709
|
+
<image href="${dataUrl}" width="${pw}" height="${ph}"/>
|
|
710
|
+
<text x="${pw - 20}" y="${ph - 16}" text-anchor="end" font-family="system-ui, sans-serif" font-size="${fontSize}" fill="#ffffff" opacity="0.4">backpackontology.com</text>
|
|
711
|
+
</svg>`;
|
|
712
|
+
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
713
|
+
},
|
|
504
714
|
destroy() {
|
|
505
715
|
cancelAnimationFrame(animFrame);
|
|
506
716
|
observer.disconnect();
|
|
507
717
|
},
|
|
508
718
|
};
|
|
719
|
+
function drawWatermark(ectx, w, h) {
|
|
720
|
+
const fontSize = Math.max(16, Math.round(w / 80));
|
|
721
|
+
ectx.save();
|
|
722
|
+
ectx.font = `${fontSize}px system-ui, sans-serif`;
|
|
723
|
+
ectx.fillStyle = "rgba(255, 255, 255, 0.4)";
|
|
724
|
+
ectx.textAlign = "right";
|
|
725
|
+
ectx.textBaseline = "bottom";
|
|
726
|
+
ectx.fillText("backpackontology.com", w - 20, h - 16);
|
|
727
|
+
ectx.restore();
|
|
728
|
+
}
|
|
509
729
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function initEmptyState(container) {
|
|
2
|
+
const el = document.createElement("div");
|
|
3
|
+
el.className = "empty-state";
|
|
4
|
+
el.innerHTML = `
|
|
5
|
+
<div class="empty-state-content">
|
|
6
|
+
<div class="empty-state-icon">
|
|
7
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
8
|
+
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0022 16z"/>
|
|
9
|
+
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
|
10
|
+
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
|
11
|
+
</svg>
|
|
12
|
+
</div>
|
|
13
|
+
<h2 class="empty-state-title">No learning graphs yet</h2>
|
|
14
|
+
<p class="empty-state-desc">Connect Backpack to Claude, then start a conversation. Claude will build your first learning graph automatically.</p>
|
|
15
|
+
<div class="empty-state-setup">
|
|
16
|
+
<div class="empty-state-label">Add Backpack to Claude Code:</div>
|
|
17
|
+
<code class="empty-state-code">claude mcp add backpack-local -s user -- npx backpack-ontology@latest</code>
|
|
18
|
+
</div>
|
|
19
|
+
<p class="empty-state-hint">Press <kbd>?</kbd> for keyboard shortcuts</p>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
container.appendChild(el);
|
|
23
|
+
return {
|
|
24
|
+
show() { el.classList.remove("hidden"); },
|
|
25
|
+
hide() { el.classList.add("hidden"); },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LearningGraphData } from "backpack-ontology";
|
|
2
|
+
export declare function createHistory(): {
|
|
3
|
+
/** Call before mutating the data to snapshot the current state. */
|
|
4
|
+
push(data: LearningGraphData): void;
|
|
5
|
+
undo(currentData: LearningGraphData): LearningGraphData | null;
|
|
6
|
+
redo(currentData: LearningGraphData): LearningGraphData | null;
|
|
7
|
+
canUndo(): boolean;
|
|
8
|
+
canRedo(): boolean;
|
|
9
|
+
clear(): void;
|
|
10
|
+
};
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const MAX_HISTORY = 30;
|
|
2
|
+
export function createHistory() {
|
|
3
|
+
let undoStack = [];
|
|
4
|
+
let redoStack = [];
|
|
5
|
+
return {
|
|
6
|
+
/** Call before mutating the data to snapshot the current state. */
|
|
7
|
+
push(data) {
|
|
8
|
+
undoStack.push(JSON.stringify(data));
|
|
9
|
+
if (undoStack.length > MAX_HISTORY)
|
|
10
|
+
undoStack.shift();
|
|
11
|
+
redoStack = [];
|
|
12
|
+
},
|
|
13
|
+
undo(currentData) {
|
|
14
|
+
if (undoStack.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
redoStack.push(JSON.stringify(currentData));
|
|
17
|
+
return JSON.parse(undoStack.pop());
|
|
18
|
+
},
|
|
19
|
+
redo(currentData) {
|
|
20
|
+
if (redoStack.length === 0)
|
|
21
|
+
return null;
|
|
22
|
+
undoStack.push(JSON.stringify(currentData));
|
|
23
|
+
return JSON.parse(redoStack.pop());
|
|
24
|
+
},
|
|
25
|
+
canUndo() {
|
|
26
|
+
return undoStack.length > 0;
|
|
27
|
+
},
|
|
28
|
+
canRedo() {
|
|
29
|
+
return redoStack.length > 0;
|
|
30
|
+
},
|
|
31
|
+
clear() {
|
|
32
|
+
undoStack = [];
|
|
33
|
+
redoStack = [];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/layout.d.ts
CHANGED
|
@@ -18,7 +18,14 @@ export interface LayoutState {
|
|
|
18
18
|
edges: LayoutEdge[];
|
|
19
19
|
nodeMap: Map<string, LayoutNode>;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
export interface LayoutParams {
|
|
22
|
+
clusterStrength: number;
|
|
23
|
+
spacing: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const DEFAULT_LAYOUT_PARAMS: LayoutParams;
|
|
26
|
+
export declare function setLayoutParams(p: Partial<LayoutParams>): void;
|
|
27
|
+
export declare function getLayoutParams(): LayoutParams;
|
|
28
|
+
/** Create a layout state from ontology data. Nodes start grouped by type. */
|
|
22
29
|
export declare function createLayout(data: LearningGraphData): LayoutState;
|
|
23
30
|
/** Run one tick of the force simulation. Returns new alpha. */
|
|
24
31
|
export declare function tick(state: LayoutState, alpha: number): number;
|