backpack-viewer 0.2.14 → 0.2.15
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-Mi0vDG5K.js +21 -0
- package/dist/app/assets/index-z15vEFEy.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +10 -1
- package/dist/canvas.js +81 -2
- package/dist/info-panel.d.ts +1 -1
- package/dist/info-panel.js +32 -11
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +26 -0
- package/dist/main.js +135 -10
- package/dist/shortcuts.js +2 -1
- package/dist/style.css +88 -2
- package/dist/tools-pane.d.ts +3 -0
- package/dist/tools-pane.js +167 -5
- package/package.json +1 -1
- package/dist/app/assets/index-CR8Iepyw.js +0 -21
- package/dist/app/assets/index-FMdnOuXa.css +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const E of e)if(E.type==="childList")for(const t of E.addedNodes)t.tagName==="LINK"&&t.rel==="modulepreload"&&i(t)}).observe(document,{childList:!0,subtree:!0});function u(e){const E={};return e.integrity&&(E.integrity=e.integrity),e.referrerPolicy&&(E.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?E.credentials="include":e.crossOrigin==="anonymous"?E.credentials="omit":E.credentials="same-origin",E}function i(e){if(e.ep)return;e.ep=!0;const E=u(e);fetch(e.href,E)}})();async function Ie(){const s=await fetch("/api/ontologies");return s.ok?s.json():[]}async function Pe(s){const o=await fetch(`/api/ontologies/${encodeURIComponent(s)}`);if(!o.ok)throw new Error(`Failed to load ontology: ${s}`);return o.json()}async function Re(s,o){if(!(await fetch(`/api/ontologies/${encodeURIComponent(s)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).ok)throw new Error(`Failed to save ontology: ${s}`)}async function _e(s,o){if(!(await fetch(`/api/ontologies/${encodeURIComponent(s)}/rename`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:o})})).ok)throw new Error(`Failed to rename ontology: ${s}`)}function Ke(s,o){const u=typeof o=="function"?{onSelect:o}:o,i=document.createElement("h2");i.textContent="Backpack Viewer";const e=document.createElement("input");e.type="text",e.placeholder="Filter...",e.id="filter";const E=document.createElement("ul");E.id="ontology-list";const t=document.createElement("div");t.className="sidebar-footer",t.innerHTML='<a href="mailto:support@backpackontology.com">support@backpackontology.com</a><span>Feedback & support</span>',s.appendChild(i),s.appendChild(e),s.appendChild(E),s.appendChild(t);let l=[],c="";return e.addEventListener("input",()=>{const h=e.value.toLowerCase();for(const v of l){const I=v.dataset.name??"";v.style.display=I.includes(h)?"":"none"}}),{setSummaries(h){E.innerHTML="",l=h.map(v=>{const I=document.createElement("li");I.className="ontology-item",I.dataset.name=v.name;const T=document.createElement("span");T.className="name",T.textContent=v.name;const q=document.createElement("span");if(q.className="stats",q.textContent=`${v.nodeCount} nodes, ${v.edgeCount} edges`,I.appendChild(T),I.appendChild(q),u.onRename){const j=document.createElement("button");j.className="sidebar-edit-btn",j.textContent="✎",j.title="Rename";const Y=u.onRename;j.addEventListener("click",D=>{D.stopPropagation();const _=document.createElement("input");_.type="text",_.className="sidebar-rename-input",_.value=v.name,T.textContent="",T.appendChild(_),j.style.display="none",_.focus(),_.select();const L=()=>{const B=_.value.trim();B&&B!==v.name?Y(v.name,B):(T.textContent=v.name,j.style.display="")};_.addEventListener("blur",L),_.addEventListener("keydown",B=>{B.key==="Enter"&&_.blur(),B.key==="Escape"&&(_.value=v.name,_.blur())})}),I.appendChild(j)}return I.addEventListener("click",()=>u.onSelect(v.name)),E.appendChild(I),I}),c&&this.setActive(c)},setActive(h){c=h;for(const v of l)v.classList.toggle("active",v.dataset.name===h)}}}const Ve={clusterStrength:.05,spacing:1},Je=5e3,Ze=8e3,Qe=.005,Ue=100,je=250,Fe=.9,$e=.01,Be=30,Te=50;let Ne={...Ve};function et(s){s.clusterStrength!==void 0&&(Ne.clusterStrength=s.clusterStrength),s.spacing!==void 0&&(Ne.spacing=s.spacing)}function tt(s,o){for(const u of Object.values(s))if(typeof u=="string")return u;return o}function nt(s,o,u){const i=new Set(o);let e=new Set(o);for(let E=0;E<u;E++){const t=new Set;for(const l of s.edges)e.has(l.sourceId)&&!i.has(l.targetId)&&t.add(l.targetId),e.has(l.targetId)&&!i.has(l.sourceId)&&t.add(l.sourceId);for(const l of t)i.add(l);if(e=t,t.size===0)break}return{nodes:s.nodes.filter(E=>i.has(E.id)),edges:s.edges.filter(E=>i.has(E.sourceId)&&i.has(E.targetId)),metadata:s.metadata}}function Oe(s){const o=new Map,u=[...new Set(s.nodes.map(c=>c.type))],i=Math.sqrt(u.length)*je*.6,e=new Map,E=new Map;for(const c of s.nodes)E.set(c.type,(E.get(c.type)??0)+1);const t=s.nodes.map(c=>{const h=u.indexOf(c.type),v=2*Math.PI*h/Math.max(u.length,1),I=Math.cos(v)*i,T=Math.sin(v)*i,q=e.get(c.type)??0;e.set(c.type,q+1);const j=E.get(c.type)??1,Y=2*Math.PI*q/j,D=Ue*.6,_={id:c.id,x:I+Math.cos(Y)*D,y:T+Math.sin(Y)*D,vx:0,vy:0,label:tt(c.properties,c.id),type:c.type};return o.set(c.id,_),_}),l=s.edges.map(c=>({sourceId:c.sourceId,targetId:c.targetId,type:c.type}));return{nodes:t,edges:l,nodeMap:o}}function ot(s,o){const{nodes:u,edges:i,nodeMap:e}=s;for(let t=0;t<u.length;t++)for(let l=t+1;l<u.length;l++){const c=u[t],h=u[l];let v=h.x-c.x,I=h.y-c.y,T=Math.sqrt(v*v+I*I);T<Be&&(T=Be);const j=(c.type===h.type?Je:Ze*Ne.spacing)*o/(T*T),Y=v/T*j,D=I/T*j;c.vx-=Y,c.vy-=D,h.vx+=Y,h.vy+=D}for(const t of i){const l=e.get(t.sourceId),c=e.get(t.targetId);if(!l||!c)continue;const h=c.x-l.x,v=c.y-l.y,I=Math.sqrt(h*h+v*v);if(I===0)continue;const T=l.type===c.type?Ue*Ne.spacing:je*Ne.spacing,q=Qe*(I-T)*o,j=h/I*q,Y=v/I*q;l.vx+=j,l.vy+=Y,c.vx-=j,c.vy-=Y}for(const t of u)t.vx-=t.x*$e*o,t.vy-=t.y*$e*o;const E=new Map;for(const t of u){const l=E.get(t.type)??{x:0,y:0,count:0};l.x+=t.x,l.y+=t.y,l.count++,E.set(t.type,l)}for(const t of E.values())t.x/=t.count,t.y/=t.count;for(const t of u){const l=E.get(t.type);t.vx+=(l.x-t.x)*Ne.clusterStrength*o,t.vy+=(l.y-t.y)*Ne.clusterStrength*o}for(const t of u){t.vx*=Fe,t.vy*=Fe;const l=Math.sqrt(t.vx*t.vx+t.vy*t.vy);l>Te&&(t.vx=t.vx/l*Te,t.vy=t.vy/l*Te),t.x+=t.vx,t.y+=t.vy}return o*.995}const De=["#d4a27f","#c17856","#b07a5e","#d4956b","#a67c5a","#cc9e7c","#c4866a","#cb8e6c","#b8956e","#a88a70","#d9b08c","#c4a882","#e8b898","#b5927a","#a8886e","#d1a990"],ze=new Map;function de(s){const o=ze.get(s);if(o)return o;let u=0;for(let e=0;e<s.length;e++)u=(u<<5)-u+s.charCodeAt(e)|0;const i=De[Math.abs(u)%De.length];return ze.set(s,i),i}function se(s){return getComputedStyle(document.documentElement).getPropertyValue(s).trim()}const he=20,st=.001;function at(s,o,u){const i=s.querySelector("canvas"),e=i.getContext("2d"),E=window.devicePixelRatio||1;let t={x:0,y:0,scale:1},l=null,c=1,h=0,v=new Set,I=null,T=!0,q=!0,j=!0,Y=null,D=null,_=1,L=null,B=null,M=null,m=null;const k=300;function w(){i.width=i.clientWidth*E,i.height=i.clientHeight*E,x()}const a=new ResizeObserver(w);a.observe(s),w();function y(p,C){return[p/t.scale+t.x,C/t.scale+t.y]}function A(p,C){if(!l)return null;const[S,$]=y(p,C);for(let z=l.nodes.length-1;z>=0;z--){const U=l.nodes[z],O=S-U.x,G=$-U.y;if(O*O+G*G<=he*he)return U}return null}function x(){if(!l){e.clearRect(0,0,i.width,i.height);return}const p=se("--canvas-edge"),C=se("--canvas-edge-highlight"),S=se("--canvas-edge-dim"),$=se("--canvas-edge-label"),z=se("--canvas-edge-label-highlight"),U=se("--canvas-edge-label-dim"),O=se("--canvas-arrow"),G=se("--canvas-arrow-highlight"),oe=se("--canvas-node-label"),J=se("--canvas-node-label-dim"),fe=se("--canvas-type-badge"),ie=se("--canvas-type-badge-dim"),be=se("--canvas-selection-border"),Le=se("--canvas-node-border");if(e.save(),e.setTransform(E,0,0,E,0,0),e.clearRect(0,0,i.clientWidth,i.clientHeight),e.save(),e.translate(-t.x*t.scale,-t.y*t.scale),e.scale(t.scale,t.scale),q){const H=new Map;for(const te of l.nodes){if(I!==null&&!I.has(te.id))continue;const ne=H.get(te.type)??[];ne.push(te),H.set(te.type,ne)}for(const[te,ne]of H){if(ne.length<2)continue;const Ce=de(te),ye=he*2.5;let pe=1/0,ve=1/0,ce=-1/0,Q=-1/0;for(const we of ne)we.x<pe&&(pe=we.x),we.y<ve&&(ve=we.y),we.x>ce&&(ce=we.x),we.y>Q&&(Q=we.y);e.beginPath();const xe=(ce-pe)/2+ye,Ee=(Q-ve)/2+ye,qe=(pe+ce)/2,Ge=(ve+Q)/2;e.ellipse(qe,Ge,xe,Ee,0,0,Math.PI*2),e.fillStyle=Ce,e.globalAlpha=.05,e.fill(),e.strokeStyle=Ce,e.globalAlpha=.12,e.lineWidth=1,e.setLineDash([4,4]),e.stroke(),e.setLineDash([]),e.globalAlpha=1}}for(const H of l.edges){const te=l.nodeMap.get(H.sourceId),ne=l.nodeMap.get(H.targetId);if(!te||!ne)continue;const Ce=I===null||I.has(H.sourceId),ye=I===null||I.has(H.targetId),pe=Ce&&ye;if(I!==null&&!Ce&&!ye)continue;const ce=v.size>0&&(v.has(H.sourceId)||v.has(H.targetId))||I!==null&&pe,Q=I!==null&&!pe;if(H.sourceId===H.targetId){N(te,H.type,ce,p,C,$,z);continue}if(e.beginPath(),e.moveTo(te.x,te.y),e.lineTo(ne.x,ne.y),e.strokeStyle=ce?C:Q?S:p,e.lineWidth=ce?2.5:1.5,e.stroke(),R(te.x,te.y,ne.x,ne.y,ce,O,G),T){const xe=(te.x+ne.x)/2,Ee=(te.y+ne.y)/2;e.fillStyle=ce?z:Q?U:$,e.font="9px system-ui, sans-serif",e.textAlign="center",e.textBaseline="bottom",e.fillText(H.type,xe,Ee-4)}}for(const H of l.nodes){const te=de(H.type),ne=v.has(H.id),Ce=v.size>0&&l.edges.some(ce=>v.has(ce.sourceId)&&ce.targetId===H.id||v.has(ce.targetId)&&ce.sourceId===H.id),ye=I!==null&&!I.has(H.id),pe=ye||v.size>0&&!ne&&!Ce;ne&&(e.save(),e.shadowColor=te,e.shadowBlur=20,e.beginPath(),e.arc(H.x,H.y,he+3,0,Math.PI*2),e.fillStyle=te,e.globalAlpha=.3,e.fill(),e.restore()),e.beginPath(),e.arc(H.x,H.y,he,0,Math.PI*2),e.fillStyle=te,e.globalAlpha=ye?.1:pe?.3:1,e.fill(),e.strokeStyle=ne?be:Le,e.lineWidth=ne?3:1.5,e.stroke();const ve=H.label.length>24?H.label.slice(0,22)+"...":H.label;e.fillStyle=pe?J:oe,e.font="11px system-ui, sans-serif",e.textAlign="center",e.textBaseline="top",e.fillText(ve,H.x,H.y+he+4),e.fillStyle=pe?ie:fe,e.font="9px system-ui, sans-serif",e.textBaseline="bottom",e.fillText(H.type,H.x,H.y-he-3),e.globalAlpha=1}e.restore(),e.restore(),j&&l.nodes.length>1&&P()}function P(){if(!l)return;const p=140,C=100,S=8,$=i.clientWidth-p-16,z=i.clientHeight-C-16;let U=1/0,O=1/0,G=-1/0,oe=-1/0;for(const Q of l.nodes)Q.x<U&&(U=Q.x),Q.y<O&&(O=Q.y),Q.x>G&&(G=Q.x),Q.y>oe&&(oe=Q.y);const J=G-U||1,fe=oe-O||1,ie=Math.min((p-S*2)/J,(C-S*2)/fe),be=$+S+(p-S*2-J*ie)/2,Le=z+S+(C-S*2-fe*ie)/2;e.save(),e.setTransform(E,0,0,E,0,0),e.fillStyle=se("--bg-surface")||"#1a1a1a",e.globalAlpha=.85,e.beginPath(),e.roundRect($,z,p,C,8),e.fill(),e.strokeStyle=se("--border")||"#2a2a2a",e.globalAlpha=1,e.lineWidth=1,e.stroke(),e.globalAlpha=.15,e.strokeStyle=se("--canvas-edge")||"#555",e.lineWidth=.5;for(const Q of l.edges){const xe=l.nodeMap.get(Q.sourceId),Ee=l.nodeMap.get(Q.targetId);!xe||!Ee||Q.sourceId===Q.targetId||(e.beginPath(),e.moveTo(be+(xe.x-U)*ie,Le+(xe.y-O)*ie),e.lineTo(be+(Ee.x-U)*ie,Le+(Ee.y-O)*ie),e.stroke())}e.globalAlpha=.8;for(const Q of l.nodes){const xe=be+(Q.x-U)*ie,Ee=Le+(Q.y-O)*ie;e.beginPath(),e.arc(xe,Ee,2,0,Math.PI*2),e.fillStyle=de(Q.type),e.fill()}const H=t.x,te=t.y,ne=t.x+i.clientWidth/t.scale,Ce=t.y+i.clientHeight/t.scale,ye=be+(H-U)*ie,pe=Le+(te-O)*ie,ve=(ne-H)*ie,ce=(Ce-te)*ie;e.globalAlpha=.3,e.strokeStyle=se("--accent")||"#d4a27f",e.lineWidth=1.5,e.strokeRect(Math.max($,Math.min(ye,$+p)),Math.max(z,Math.min(pe,z+C)),Math.min(ve,p),Math.min(ce,C)),e.globalAlpha=1,e.restore()}function R(p,C,S,$,z,U,O){const G=Math.atan2($-C,S-p),oe=S-Math.cos(G)*he,J=$-Math.sin(G)*he,fe=8;e.beginPath(),e.moveTo(oe,J),e.lineTo(oe-fe*Math.cos(G-.4),J-fe*Math.sin(G-.4)),e.lineTo(oe-fe*Math.cos(G+.4),J-fe*Math.sin(G+.4)),e.closePath(),e.fillStyle=z?O:U,e.fill()}function N(p,C,S,$,z,U,O){const G=p.x+he+15,oe=p.y-he-15;e.beginPath(),e.arc(G,oe,15,0,Math.PI*2),e.strokeStyle=S?z:$,e.lineWidth=S?2.5:1.5,e.stroke(),T&&(e.fillStyle=S?O:U,e.font="9px system-ui, sans-serif",e.textAlign="center",e.fillText(C,G,oe-18))}function r(){if(!M||!m)return;const p=performance.now()-m.time,C=Math.min(p/k,1),S=1-Math.pow(1-C,3);t.x=m.x+(M.x-m.x)*S,t.y=m.y+(M.y-m.y)*S,x(),C<1?requestAnimationFrame(r):(M=null,m=null)}function X(){!l||c<st||(c=ot(l,c),x(),h=requestAnimationFrame(X))}let n=!1,d=!1,f=0,g=0;i.addEventListener("mousedown",p=>{n=!0,d=!1,f=p.clientX,g=p.clientY}),i.addEventListener("mousemove",p=>{if(!n)return;const C=p.clientX-f,S=p.clientY-g;(Math.abs(C)>2||Math.abs(S)>2)&&(d=!0),t.x-=C/t.scale,t.y-=S/t.scale,f=p.clientX,g=p.clientY,x()}),i.addEventListener("mouseup",p=>{if(n=!1,d)return;const C=i.getBoundingClientRect(),S=p.clientX-C.left,$=p.clientY-C.top,z=A(S,$),U=p.ctrlKey||p.metaKey;if(z){U?v.has(z.id)?v.delete(z.id):v.add(z.id):v.size===1&&v.has(z.id)?v.clear():(v.clear(),v.add(z.id));const O=[...v];o==null||o(O.length>0?O:null)}else v.clear(),o==null||o(null);x()}),i.addEventListener("mouseleave",()=>{n=!1}),i.addEventListener("wheel",p=>{p.preventDefault();const C=i.getBoundingClientRect(),S=p.clientX-C.left,$=p.clientY-C.top,[z,U]=y(S,$),O=p.ctrlKey?1-p.deltaY*.01:p.deltaY>0?.9:1.1;t.scale=Math.max(.05,Math.min(10,t.scale*O)),t.x=z-S/t.scale,t.y=U-$/t.scale,x()},{passive:!1});let F=[],W=0,K=1,ae=0,V=0,re=!1;i.addEventListener("touchstart",p=>{p.preventDefault(),F=Array.from(p.touches),F.length===2?(W=Z(F[0],F[1]),K=t.scale):F.length===1&&(f=F[0].clientX,g=F[0].clientY,ae=F[0].clientX,V=F[0].clientY,re=!1)},{passive:!1}),i.addEventListener("touchmove",p=>{p.preventDefault();const C=Array.from(p.touches);if(C.length===2&&F.length===2){const $=Z(C[0],C[1])/W;t.scale=Math.max(.05,Math.min(10,K*$)),x()}else if(C.length===1){const S=C[0].clientX-f,$=C[0].clientY-g;(Math.abs(C[0].clientX-ae)>10||Math.abs(C[0].clientY-V)>10)&&(re=!0),t.x-=S/t.scale,t.y-=$/t.scale,f=C[0].clientX,g=C[0].clientY,x()}F=C},{passive:!1}),i.addEventListener("touchend",p=>{if(p.preventDefault(),re||p.changedTouches.length!==1)return;const C=p.changedTouches[0],S=i.getBoundingClientRect(),$=C.clientX-S.left,z=C.clientY-S.top,U=A($,z);if(U){v.size===1&&v.has(U.id)?v.clear():(v.clear(),v.add(U.id));const O=[...v];o==null||o(O.length>0?O:null)}else v.clear(),o==null||o(null);x()},{passive:!1}),i.addEventListener("gesturestart",p=>p.preventDefault()),i.addEventListener("gesturechange",p=>p.preventDefault());function Z(p,C){const S=p.clientX-C.clientX,$=p.clientY-C.clientY;return Math.sqrt(S*S+$*$)}const ue=document.createElement("div");ue.className="zoom-controls";const me=document.createElement("button");me.className="zoom-btn",me.textContent="+",me.title="Zoom in",me.addEventListener("click",()=>{const p=i.clientWidth/2,C=i.clientHeight/2,[S,$]=y(p,C);t.scale=Math.min(10,t.scale*1.3),t.x=S-p/t.scale,t.y=$-C/t.scale,x()});const ee=document.createElement("button");ee.className="zoom-btn",ee.textContent="−",ee.title="Zoom out",ee.addEventListener("click",()=>{const p=i.clientWidth/2,C=i.clientHeight/2,[S,$]=y(p,C);t.scale=Math.max(.05,t.scale/1.3),t.x=S-p/t.scale,t.y=$-C/t.scale,x()});const ge=document.createElement("button");return ge.className="zoom-btn",ge.textContent="○",ge.title="Reset zoom",ge.addEventListener("click",()=>{if(l){if(t={x:0,y:0,scale:1},l.nodes.length>0){let p=1/0,C=1/0,S=-1/0,$=-1/0;for(const O of l.nodes)O.x<p&&(p=O.x),O.y<C&&(C=O.y),O.x>S&&(S=O.x),O.y>$&&($=O.y);const z=(p+S)/2,U=(C+$)/2;t.x=z-i.clientWidth/2,t.y=U-i.clientHeight/2}x()}}),ue.appendChild(me),ue.appendChild(ge),ue.appendChild(ee),s.appendChild(ue),{loadGraph(p){if(cancelAnimationFrame(h),Y=p,D=null,L=null,B=null,l=Oe(p),c=1,v=new Set,I=null,t={x:0,y:0,scale:1},l.nodes.length>0){let C=1/0,S=1/0,$=-1/0,z=-1/0;for(const J of l.nodes)J.x<C&&(C=J.x),J.y<S&&(S=J.y),J.x>$&&($=J.x),J.y>z&&(z=J.y);const U=(C+$)/2,O=(S+z)/2,G=i.clientWidth,oe=i.clientHeight;t.x=U-G/2,t.y=O-oe/2}X()},setFilteredNodeIds(p){I=p,x()},panToNode(p){this.panToNodes([p])},panToNodes(p){if(!l||p.length===0)return;const C=p.map(z=>l.nodeMap.get(z)).filter(Boolean);if(C.length===0)return;v=new Set(p),o==null||o(p);const S=i.clientWidth,$=i.clientHeight;if(C.length===1)m={x:t.x,y:t.y,time:performance.now()},M={x:C[0].x-S/(2*t.scale),y:C[0].y-$/(2*t.scale)};else{let z=1/0,U=1/0,O=-1/0,G=-1/0;for(const H of C)H.x<z&&(z=H.x),H.y<U&&(U=H.y),H.x>O&&(O=H.x),H.y>G&&(G=H.y);const oe=he*4,J=O-z+oe*2,fe=G-U+oe*2,ie=Math.min(S/J,$/fe,t.scale);t.scale=ie;const be=(z+O)/2,Le=(U+G)/2;m={x:t.x,y:t.y,time:performance.now()},M={x:be-S/(2*t.scale),y:Le-$/(2*t.scale)}}r()},setEdgeLabels(p){T=p,x()},setTypeHulls(p){q=p,x()},setMinimap(p){j=p,x()},reheat(){c=.5,cancelAnimationFrame(h),X()},exportImage(p){if(!l)return"";const C=i.width,S=i.height;if(p==="png"){const O=document.createElement("canvas");O.width=C,O.height=S;const G=O.getContext("2d");return G.fillStyle=se("--bg")||"#141414",G.fillRect(0,0,C,S),G.drawImage(i,0,0),Se(G,C,S),O.toDataURL("image/png")}const $=i.toDataURL("image/png"),z=Math.max(16,Math.round(C/80)),U=`<svg xmlns="http://www.w3.org/2000/svg" width="${C}" height="${S}">
|
|
2
|
+
<image href="${$}" width="${C}" height="${S}"/>
|
|
3
|
+
<text x="${C-20}" y="${S-16}" text-anchor="end" font-family="system-ui, sans-serif" font-size="${z}" fill="#ffffff" opacity="0.4">backpackontology.com</text>
|
|
4
|
+
</svg>`;return"data:image/svg+xml;charset=utf-8,"+encodeURIComponent(U)},enterFocus(p,C){if(!Y||!l)return;D||(L=l,B={...t}),D=p,_=C;const S=nt(Y,p,C);if(cancelAnimationFrame(h),l=Oe(S),c=1,v=new Set(p),I=null,t={x:0,y:0,scale:1},l.nodes.length>0){let $=1/0,z=1/0,U=-1/0,O=-1/0;for(const J of l.nodes)J.x<$&&($=J.x),J.y<z&&(z=J.y),J.x>U&&(U=J.x),J.y>O&&(O=J.y);const G=($+U)/2,oe=(z+O)/2;t.x=G-i.clientWidth/2,t.y=oe-i.clientHeight/2}X(),u==null||u({seedNodeIds:p,hops:C,totalNodes:S.nodes.length})},exitFocus(){!D||!L||(cancelAnimationFrame(h),l=L,t=B??{x:0,y:0,scale:1},D=null,L=null,B=null,v=new Set,I=null,x(),u==null||u(null))},isFocused(){return D!==null},getFocusInfo(){return!D||!l?null:{seedNodeIds:D,hops:_,totalNodes:l.nodes.length}},destroy(){cancelAnimationFrame(h),a.disconnect()}};function Se(p,C,S){const $=Math.max(16,Math.round(C/80));p.save(),p.font=`${$}px system-ui, sans-serif`,p.fillStyle="rgba(255, 255, 255, 0.4)",p.textAlign="right",p.textBaseline="bottom",p.fillText("backpackontology.com",C-20,S-16),p.restore()}}function ke(s){for(const o of Object.values(s.properties))if(typeof o=="string")return o;return s.id}const ct="✎";function it(s,o,u,i){const e=document.createElement("div");e.id="info-panel",e.className="info-panel hidden",s.appendChild(e);let E=!1,t=[],l=-1,c=!1,h=null,v=[];function I(){e.classList.add("hidden"),e.classList.remove("info-panel-maximized"),e.innerHTML="",E=!1,t=[],l=-1}function T(L){!h||!u||(l<t.length-1&&(t=t.slice(0,l+1)),t.push(L),l=t.length-1,c=!0,u(L),c=!1)}function q(){l<=0||!h||!u||(l--,c=!0,u(t[l]),c=!1)}function j(){l>=t.length-1||!h||!u||(l++,c=!0,u(t[l]),c=!1)}function Y(){const L=document.createElement("div");L.className="info-panel-toolbar";const B=document.createElement("button");B.className="info-toolbar-btn",B.textContent="←",B.title="Back",B.disabled=l<=0,B.addEventListener("click",q),L.appendChild(B);const M=document.createElement("button");if(M.className="info-toolbar-btn",M.textContent="→",M.title="Forward",M.disabled=l>=t.length-1,M.addEventListener("click",j),L.appendChild(M),i&&v.length>0){const w=document.createElement("button");w.className="info-toolbar-btn info-focus-btn",w.textContent="◎",w.title="Focus on neighborhood (F)",w.addEventListener("click",()=>{i(v)}),L.appendChild(w)}const m=document.createElement("button");m.className="info-toolbar-btn",m.textContent=E?"⎘":"⛶",m.title=E?"Restore":"Maximize",m.addEventListener("click",()=>{E=!E,e.classList.toggle("info-panel-maximized",E),m.textContent=E?"⎘":"⛶",m.title=E?"Restore":"Maximize"}),L.appendChild(m);const k=document.createElement("button");return k.className="info-toolbar-btn info-close-btn",k.textContent="×",k.title="Close",k.addEventListener("click",I),L.appendChild(k),L}function D(L,B){const M=B.nodes.find(d=>d.id===L);if(!M)return;const m=B.edges.filter(d=>d.sourceId===L||d.targetId===L);e.innerHTML="",e.classList.remove("hidden"),E&&e.classList.add("info-panel-maximized"),e.appendChild(Y());const k=document.createElement("div");k.className="info-header";const w=document.createElement("span");if(w.className="info-type-badge",w.textContent=M.type,w.style.backgroundColor=de(M.type),o){w.classList.add("info-editable");const d=document.createElement("button");d.className="info-inline-edit",d.textContent=ct,d.addEventListener("click",f=>{f.stopPropagation();const g=document.createElement("input");g.type="text",g.className="info-edit-inline-input",g.value=M.type,w.textContent="",w.appendChild(g),g.focus(),g.select();const F=()=>{const W=g.value.trim();W&&W!==M.type?o.onChangeNodeType(L,W):(w.textContent=M.type,w.appendChild(d))};g.addEventListener("blur",F),g.addEventListener("keydown",W=>{W.key==="Enter"&&g.blur(),W.key==="Escape"&&(g.value=M.type,g.blur())})}),w.appendChild(d)}const a=document.createElement("h3");a.className="info-label",a.textContent=ke(M);const y=document.createElement("span");y.className="info-id",y.textContent=M.id,k.appendChild(w),k.appendChild(a),k.appendChild(y),e.appendChild(k);const A=Object.keys(M.properties),x=Me("Properties");if(A.length>0){const d=document.createElement("dl");d.className="info-props";for(const f of A){const g=document.createElement("dt");g.textContent=f;const F=document.createElement("dd");if(o){const W=Ae(M.properties[f]),K=document.createElement("textarea");K.className="info-edit-input",K.value=W,K.rows=1,K.addEventListener("input",()=>He(K)),K.addEventListener("keydown",V=>{V.key==="Enter"&&!V.shiftKey&&(V.preventDefault(),K.blur())}),K.addEventListener("blur",()=>{const V=K.value;V!==W&&o.onUpdateNode(L,{[f]:dt(V)})}),F.appendChild(K),requestAnimationFrame(()=>He(K));const ae=document.createElement("button");ae.className="info-delete-prop",ae.textContent="×",ae.title=`Remove ${f}`,ae.addEventListener("click",()=>{const V={...M.properties};delete V[f],o.onUpdateNode(L,V)}),F.appendChild(ae)}else F.appendChild(lt(M.properties[f]));d.appendChild(g),d.appendChild(F)}x.appendChild(d)}if(o){const d=document.createElement("button");d.className="info-add-btn",d.textContent="+ Add property",d.addEventListener("click",()=>{const f=document.createElement("div");f.className="info-add-row";const g=document.createElement("input");g.type="text",g.className="info-edit-input",g.placeholder="key";const F=document.createElement("input");F.type="text",F.className="info-edit-input",F.placeholder="value";const W=document.createElement("button");W.className="info-add-save",W.textContent="Add",W.addEventListener("click",()=>{g.value&&o.onAddProperty(L,g.value,F.value)}),f.appendChild(g),f.appendChild(F),f.appendChild(W),x.appendChild(f),g.focus()}),x.appendChild(d)}if(e.appendChild(x),m.length>0){const d=Me(`Connections (${m.length})`),f=document.createElement("ul");f.className="info-connections";for(const g of m){const F=g.sourceId===L,W=F?g.targetId:g.sourceId,K=B.nodes.find(ee=>ee.id===W),ae=K?ke(K):W,V=document.createElement("li");if(V.className="info-connection",u&&K&&(V.classList.add("info-connection-link"),V.addEventListener("click",ee=>{ee.target.closest(".info-delete-edge")||T(W)})),K){const ee=document.createElement("span");ee.className="info-target-dot",ee.style.backgroundColor=de(K.type),V.appendChild(ee)}const re=document.createElement("span");re.className="info-arrow",re.textContent=F?"→":"←";const Z=document.createElement("span");Z.className="info-edge-type",Z.textContent=g.type;const ue=document.createElement("span");ue.className="info-target",ue.textContent=ae,V.appendChild(re),V.appendChild(Z),V.appendChild(ue);const me=Object.keys(g.properties);if(me.length>0){const ee=document.createElement("div");ee.className="info-edge-props";for(const ge of me){const Se=document.createElement("span");Se.className="info-edge-prop",Se.textContent=`${ge}: ${Ae(g.properties[ge])}`,ee.appendChild(Se)}V.appendChild(ee)}if(o){const ee=document.createElement("button");ee.className="info-delete-edge",ee.textContent="×",ee.title="Remove connection",ee.addEventListener("click",ge=>{ge.stopPropagation(),o.onDeleteEdge(g.id)}),V.appendChild(ee)}f.appendChild(V)}d.appendChild(f),e.appendChild(d)}const P=Me("Timestamps"),R=document.createElement("dl");R.className="info-props";const N=document.createElement("dt");N.textContent="created";const r=document.createElement("dd");r.textContent=Ye(M.createdAt);const X=document.createElement("dt");X.textContent="updated";const n=document.createElement("dd");if(n.textContent=Ye(M.updatedAt),R.appendChild(N),R.appendChild(r),R.appendChild(X),R.appendChild(n),P.appendChild(R),e.appendChild(P),o){const d=document.createElement("div");d.className="info-section info-danger";const f=document.createElement("button");f.className="info-delete-node",f.textContent="Delete node",f.addEventListener("click",()=>{o.onDeleteNode(L),I()}),d.appendChild(f),e.appendChild(d)}}function _(L,B){const M=new Set(L),m=B.nodes.filter(N=>M.has(N.id));if(m.length===0)return;const k=B.edges.filter(N=>M.has(N.sourceId)&&M.has(N.targetId));e.innerHTML="",e.classList.remove("hidden"),E&&e.classList.add("info-panel-maximized"),e.appendChild(Y());const w=document.createElement("div");w.className="info-header";const a=document.createElement("h3");a.className="info-label",a.textContent=`${m.length} nodes selected`,w.appendChild(a);const y=document.createElement("div");y.style.cssText="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px";const A=new Map;for(const N of m)A.set(N.type,(A.get(N.type)??0)+1);for(const[N,r]of A){const X=document.createElement("span");X.className="info-type-badge",X.style.backgroundColor=de(N),X.textContent=r>1?`${N} (${r})`:N,y.appendChild(X)}w.appendChild(y),e.appendChild(w);const x=Me("Selected Nodes"),P=document.createElement("ul");P.className="info-connections";for(const N of m){const r=document.createElement("li");r.className="info-connection",u&&(r.classList.add("info-connection-link"),r.addEventListener("click",()=>{T(N.id)}));const X=document.createElement("span");X.className="info-target-dot",X.style.backgroundColor=de(N.type);const n=document.createElement("span");n.className="info-target",n.textContent=ke(N);const d=document.createElement("span");d.className="info-edge-type",d.textContent=N.type,r.appendChild(X),r.appendChild(n),r.appendChild(d),P.appendChild(r)}x.appendChild(P),e.appendChild(x);const R=Me(k.length>0?`Connections Between Selected (${k.length})`:"Connections Between Selected");if(k.length===0){const N=document.createElement("p");N.style.cssText="font-size:12px;color:var(--text-dim)",N.textContent="No direct connections between selected nodes",R.appendChild(N)}else{const N=document.createElement("ul");N.className="info-connections";for(const r of k){const X=B.nodes.find(Z=>Z.id===r.sourceId),n=B.nodes.find(Z=>Z.id===r.targetId),d=X?ke(X):r.sourceId,f=n?ke(n):r.targetId,g=document.createElement("li");if(g.className="info-connection",X){const Z=document.createElement("span");Z.className="info-target-dot",Z.style.backgroundColor=de(X.type),g.appendChild(Z)}const F=document.createElement("span");F.className="info-target",F.textContent=d;const W=document.createElement("span");W.className="info-arrow",W.textContent="→";const K=document.createElement("span");K.className="info-edge-type",K.textContent=r.type;const ae=document.createElement("span");if(ae.className="info-arrow",ae.textContent="→",g.appendChild(F),g.appendChild(W),g.appendChild(K),g.appendChild(ae),n){const Z=document.createElement("span");Z.className="info-target-dot",Z.style.backgroundColor=de(n.type),g.appendChild(Z)}const V=document.createElement("span");V.className="info-target",V.textContent=f,g.appendChild(V);const re=Object.keys(r.properties);if(re.length>0){const Z=document.createElement("div");Z.className="info-edge-props";for(const ue of re){const me=document.createElement("span");me.className="info-edge-prop",me.textContent=`${ue}: ${Ae(r.properties[ue])}`,Z.appendChild(me)}g.appendChild(Z)}N.appendChild(g)}R.appendChild(N)}e.appendChild(R)}return{show(L,B){if(h=B,v=L,L.length===1&&!c){const M=L[0];t[l]!==M&&(l<t.length-1&&(t=t.slice(0,l+1)),t.push(M),l=t.length-1)}L.length===1?D(L[0],B):L.length>1&&_(L,B)},hide:I,get visible(){return!e.classList.contains("hidden")}}}function Me(s){const o=document.createElement("div");o.className="info-section";const u=document.createElement("h4");return u.className="info-section-title",u.textContent=s,o.appendChild(u),o}function lt(s){if(Array.isArray(s)){const u=document.createElement("div");u.className="info-array";for(const i of s){const e=document.createElement("span");e.className="info-tag",e.textContent=String(i),u.appendChild(e)}return u}if(s!==null&&typeof s=="object"){const u=document.createElement("pre");return u.className="info-json",u.textContent=JSON.stringify(s,null,2),u}const o=document.createElement("span");return o.className="info-value",o.textContent=String(s??""),o}function Ae(s){return Array.isArray(s)?s.map(String).join(", "):s!==null&&typeof s=="object"?JSON.stringify(s):String(s??"")}function dt(s){const o=s.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 s}return s}function He(s){s.style.height="auto",s.style.height=s.scrollHeight+"px"}function Ye(s){try{return new Date(s).toLocaleString()}catch{return s}}function We(s){for(const o of Object.values(s.properties))if(typeof o=="string")return o;return s.id}function Xe(s,o){const u=o.toLowerCase();if(We(s).toLowerCase().includes(u)||s.type.toLowerCase().includes(u))return!0;for(const i of Object.values(s.properties))if(typeof i=="string"&&i.toLowerCase().includes(u))return!0;return!1}function rt(s){let o=null,u=null,i=null,e=new Set,E=null;const t=document.createElement("div");t.className="search-overlay hidden";const l=document.createElement("div");l.className="search-input-wrap";const c=document.createElement("input");c.className="search-input",c.type="text",c.placeholder="Search nodes...",c.setAttribute("autocomplete","off"),c.setAttribute("spellcheck","false");const h=document.createElement("kbd");h.className="search-kbd",h.textContent="/";const v=document.createElement("button");v.className="chip-toggle",v.setAttribute("aria-label","Toggle filter chips"),v.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;v.addEventListener("click",()=>{I=!I,q.classList.toggle("hidden",!I),v.classList.toggle("active",I)}),l.appendChild(c),l.appendChild(h),l.appendChild(v);const T=document.createElement("ul");T.className="search-results hidden";const q=document.createElement("div");q.className="type-chips hidden",t.appendChild(l),t.appendChild(T),t.appendChild(q),s.appendChild(t);function j(){if(q.innerHTML="",!o)return;const L=new Map;for(const M of o.nodes)L.set(M.type,(L.get(M.type)??0)+1);const B=[...L.keys()].sort();e=new Set;for(const M of B){const m=document.createElement("button");m.className="type-chip",m.dataset.type=M;const k=document.createElement("span");k.className="type-chip-dot",k.style.backgroundColor=de(M);const w=document.createElement("span");w.textContent=`${M} (${L.get(M)})`,m.appendChild(k),m.appendChild(w),m.addEventListener("click",()=>{e.has(M)?(e.delete(M),m.classList.remove("active")):(e.add(M),m.classList.add("active")),D()}),q.appendChild(m)}}function Y(){if(!o)return null;const L=c.value.trim(),B=e.size===0,M=L.length===0;if(M&&B)return null;const m=new Set;for(const k of o.nodes)!B&&!e.has(k.type)||(M||Xe(k,L))&&m.add(k.id);return m}function D(){const L=Y();u==null||u(L),_()}function _(){T.innerHTML="";const L=c.value.trim();if(!o||L.length===0){T.classList.add("hidden");return}const B=e.size===0,M=[];for(const m of o.nodes)if(!(!B&&!e.has(m.type))&&Xe(m,L)&&(M.push(m),M.length>=8))break;if(M.length===0){T.classList.add("hidden");return}for(const m of M){const k=document.createElement("li");k.className="search-result-item";const w=document.createElement("span");w.className="search-result-dot",w.style.backgroundColor=de(m.type);const a=document.createElement("span");a.className="search-result-label";const y=We(m);a.textContent=y.length>36?y.slice(0,34)+"...":y;const A=document.createElement("span");A.className="search-result-type",A.textContent=m.type,k.appendChild(w),k.appendChild(a),k.appendChild(A),k.addEventListener("click",()=>{i==null||i(m.id),c.value="",T.classList.add("hidden"),D()}),T.appendChild(k)}T.classList.remove("hidden")}return c.addEventListener("input",()=>{E&&clearTimeout(E),E=setTimeout(D,150)}),c.addEventListener("keydown",L=>{if(L.key==="Escape")c.value="",c.blur(),T.classList.add("hidden"),D();else if(L.key==="Enter"){const B=T.querySelector(".search-result-item");B==null||B.click()}}),document.addEventListener("click",L=>{t.contains(L.target)||T.classList.add("hidden")}),c.addEventListener("focus",()=>h.classList.add("hidden")),c.addEventListener("blur",()=>{c.value.length===0&&h.classList.remove("hidden")}),{setLearningGraphData(L){o=L,c.value="",T.classList.add("hidden"),o&&o.nodes.length>0?(t.classList.remove("hidden"),j()):t.classList.add("hidden")},onFilterChange(L){u=L},onNodeSelect(L){i=L},clear(){c.value="",T.classList.add("hidden"),e.clear(),I=!1,q.classList.add("hidden"),v.classList.remove("active"),u==null||u(null)},focus(){c.focus()}}}function pt(s,o){let u=null,i=null,e=!0,E=null,t=!0,l=!0,c=!0;const h={types:new Set,nodeIds:new Set};function v(){if(!u)return[];const m=new Set;for(const k of u.nodes)h.types.has(k.type)&&m.add(k.id);for(const k of h.nodeIds)m.add(k);return[...m]}function I(m){if(h.nodeIds.has(m))return!0;const k=u==null?void 0:u.nodes.find(w=>w.id===m);return k?h.types.has(k.type):!1}function T(){return h.types.size===0&&h.nodeIds.size===0}function q(){const m=v();o.onFocusChange(m.length>0?m:null)}const j=document.createElement("button");j.className="tools-pane-toggle hidden",j.title="Graph Inspector",j.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 Y=document.createElement("div");Y.className="tools-pane-content hidden",s.appendChild(j),s.appendChild(Y),j.addEventListener("click",()=>{var m;e=!e,Y.classList.toggle("hidden",e),j.classList.toggle("active",!e),e||(m=o.onOpen)==null||m.call(o)});function D(){if(Y.innerHTML="",!i)return;const m=document.createElement("div");m.className="tools-pane-summary",m.innerHTML=`<span>${i.nodeCount} nodes</span><span class="tools-pane-sep">·</span><span>${i.edgeCount} edges</span><span class="tools-pane-sep">·</span><span>${i.types.length} types</span>`,Y.appendChild(m),i.types.length&&Y.appendChild(L("Node Types",w=>{for(const a of i.types){const y=document.createElement("div");y.className="tools-pane-row tools-pane-clickable",E===a.name&&y.classList.add("active");const A=document.createElement("span");A.className="tools-pane-dot",A.style.backgroundColor=de(a.name);const x=document.createElement("span");x.className="tools-pane-name",x.textContent=a.name;const P=document.createElement("span");P.className="tools-pane-count",P.textContent=String(a.count);const R=document.createElement("button");R.className="tools-pane-edit tools-pane-focus-toggle",h.types.has(a.name)&&R.classList.add("tools-pane-focus-active"),R.textContent="◎",R.title=h.types.has(a.name)?`Remove ${a.name} from focus`:`Add ${a.name} to focus`;const N=document.createElement("button");N.className="tools-pane-edit",N.textContent="✎",N.title=`Rename all ${a.name} nodes`,y.appendChild(A),y.appendChild(x),y.appendChild(P),y.appendChild(R),y.appendChild(N),y.addEventListener("click",r=>{r.target.closest(".tools-pane-edit")||(E===a.name?(E=null,o.onFilterByType(null)):(E=a.name,o.onFilterByType(a.name)),D())}),R.addEventListener("click",r=>{r.stopPropagation(),h.types.has(a.name)?h.types.delete(a.name):h.types.add(a.name),q(),D()}),N.addEventListener("click",r=>{r.stopPropagation(),B(y,a.name,X=>{X&&X!==a.name&&o.onRenameNodeType(a.name,X)})}),w.appendChild(y)}if(h.types.size>0){const a=document.createElement("div");a.className="tools-pane-row tools-pane-clickable tools-pane-focus-clear";const y=document.createElement("span");y.className="tools-pane-name",y.style.color="var(--accent)",y.textContent=`${h.types.size} type${h.types.size>1?"s":""} focused`;const A=document.createElement("span");A.className="tools-pane-badge",A.textContent="clear types",a.appendChild(y),a.appendChild(A),a.addEventListener("click",()=>{h.types.clear(),q(),D()}),w.appendChild(a)}})),i.edgeTypes.length&&Y.appendChild(L("Edge Types",w=>{for(const a of i.edgeTypes){const y=document.createElement("div");y.className="tools-pane-row tools-pane-clickable";const A=document.createElement("span");A.className="tools-pane-name",A.textContent=a.name;const x=document.createElement("span");x.className="tools-pane-count",x.textContent=String(a.count);const P=document.createElement("button");P.className="tools-pane-edit",P.textContent="✎",P.title=`Rename all ${a.name} edges`,y.appendChild(A),y.appendChild(x),y.appendChild(P),P.addEventListener("click",R=>{R.stopPropagation(),B(y,a.name,N=>{N&&N!==a.name&&o.onRenameEdgeType(a.name,N)})}),w.appendChild(y)}})),i.mostConnected.length&&Y.appendChild(L("Most Connected",w=>{for(const a of i.mostConnected){const y=document.createElement("div");y.className="tools-pane-row tools-pane-clickable";const A=document.createElement("span");A.className="tools-pane-dot",A.style.backgroundColor=de(a.type);const x=document.createElement("span");x.className="tools-pane-name",x.textContent=a.label;const P=document.createElement("span");P.className="tools-pane-count",P.textContent=`${a.connections}`;const R=document.createElement("button");R.className="tools-pane-edit tools-pane-focus-toggle",I(a.id)&&R.classList.add("tools-pane-focus-active"),R.textContent="◎",R.title=I(a.id)?`Remove ${a.label} from focus`:`Add ${a.label} to focus`,y.appendChild(A),y.appendChild(x),y.appendChild(P),y.appendChild(R),y.addEventListener("click",N=>{N.target.closest(".tools-pane-edit")||o.onNavigateToNode(a.id)}),R.addEventListener("click",N=>{N.stopPropagation(),h.nodeIds.has(a.id)?h.nodeIds.delete(a.id):h.nodeIds.add(a.id),q(),D()}),w.appendChild(y)}}));const k=[];if(i.orphans.length&&k.push(`${i.orphans.length} orphan${i.orphans.length>1?"s":""}`),i.singletons.length&&k.push(`${i.singletons.length} singleton type${i.singletons.length>1?"s":""}`),i.emptyNodes.length&&k.push(`${i.emptyNodes.length} empty node${i.emptyNodes.length>1?"s":""}`),k.length&&Y.appendChild(L("Quality",w=>{for(const a of i.orphans.slice(0,5)){const y=document.createElement("div");y.className="tools-pane-row tools-pane-clickable tools-pane-issue";const A=document.createElement("span");A.className="tools-pane-dot",A.style.backgroundColor=de(a.type);const x=document.createElement("span");x.className="tools-pane-name",x.textContent=a.label;const P=document.createElement("span");P.className="tools-pane-badge",P.textContent="orphan";const R=document.createElement("button");R.className="tools-pane-edit tools-pane-focus-toggle",I(a.id)&&R.classList.add("tools-pane-focus-active"),R.textContent="◎",R.title=I(a.id)?`Remove ${a.label} from focus`:`Add ${a.label} to focus`,y.appendChild(A),y.appendChild(x),y.appendChild(P),y.appendChild(R),y.addEventListener("click",N=>{N.target.closest(".tools-pane-edit")||o.onNavigateToNode(a.id)}),R.addEventListener("click",N=>{N.stopPropagation(),h.nodeIds.has(a.id)?h.nodeIds.delete(a.id):h.nodeIds.add(a.id),q(),D()}),w.appendChild(y)}if(i.orphans.length>5){const a=document.createElement("div");a.className="tools-pane-more",a.textContent=`+ ${i.orphans.length-5} more orphans`,w.appendChild(a)}for(const a of i.singletons.slice(0,5)){const y=document.createElement("div");y.className="tools-pane-row tools-pane-issue";const A=document.createElement("span");A.className="tools-pane-dot",A.style.backgroundColor=de(a.name);const x=document.createElement("span");x.className="tools-pane-name",x.textContent=a.name;const P=document.createElement("span");P.className="tools-pane-badge",P.textContent="1 node",y.appendChild(A),y.appendChild(x),y.appendChild(P),w.appendChild(y)}})),!T()){const w=v(),a=[];h.types.size>0&&a.push(`${h.types.size} type${h.types.size>1?"s":""}`),h.nodeIds.size>0&&a.push(`${h.nodeIds.size} node${h.nodeIds.size>1?"s":""}`),Y.appendChild(L("Focus",y=>{const A=document.createElement("div");A.className="tools-pane-row";const x=document.createElement("span");x.className="tools-pane-name",x.style.color="var(--accent)",x.textContent=`${a.join(" + ")} (${w.length} total)`;const P=document.createElement("button");P.className="tools-pane-edit tools-pane-focus-active",P.style.opacity="1",P.textContent="×",P.title="Clear all focus",P.addEventListener("click",()=>{h.types.clear(),h.nodeIds.clear(),q(),D()}),A.appendChild(x),A.appendChild(P),y.appendChild(A)}))}Y.appendChild(L("Controls",w=>{const a=document.createElement("div");a.className="tools-pane-row tools-pane-clickable";const y=document.createElement("input");y.type="checkbox",y.checked=t,y.className="tools-pane-checkbox";const A=document.createElement("span");A.className="tools-pane-name",A.textContent="Edge labels",a.appendChild(y),a.appendChild(A),a.addEventListener("click",g=>{g.target!==y&&(y.checked=!y.checked),t=y.checked,o.onToggleEdgeLabels(t)}),w.appendChild(a);const x=document.createElement("div");x.className="tools-pane-row tools-pane-clickable";const P=document.createElement("input");P.type="checkbox",P.checked=l,P.className="tools-pane-checkbox";const R=document.createElement("span");R.className="tools-pane-name",R.textContent="Type regions",x.appendChild(P),x.appendChild(R),x.addEventListener("click",g=>{g.target!==P&&(P.checked=!P.checked),l=P.checked,o.onToggleTypeHulls(l)}),w.appendChild(x);const N=document.createElement("div");N.className="tools-pane-row tools-pane-clickable";const r=document.createElement("input");r.type="checkbox",r.checked=c,r.className="tools-pane-checkbox";const X=document.createElement("span");X.className="tools-pane-name",X.textContent="Minimap",N.appendChild(r),N.appendChild(X),N.addEventListener("click",g=>{g.target!==r&&(r.checked=!r.checked),c=r.checked,o.onToggleMinimap(c)}),w.appendChild(N),w.appendChild(_("Clustering",0,.15,.01,.05,g=>{o.onLayoutChange("clusterStrength",g)})),w.appendChild(_("Spacing",.5,3,.1,1,g=>{o.onLayoutChange("spacing",g)}));const n=document.createElement("div");n.className="tools-pane-export-row";const d=document.createElement("button");d.className="tools-pane-export-btn",d.textContent="Export PNG",d.addEventListener("click",()=>o.onExport("png"));const f=document.createElement("button");f.className="tools-pane-export-btn",f.textContent="Export SVG",f.addEventListener("click",()=>o.onExport("svg")),n.appendChild(d),n.appendChild(f),w.appendChild(n)}))}function _(m,k,w,a,y,A){const x=document.createElement("div");x.className="tools-pane-slider-row";const P=document.createElement("span");P.className="tools-pane-slider-label",P.textContent=m;const R=document.createElement("input");R.type="range",R.className="tools-pane-slider",R.min=String(k),R.max=String(w),R.step=String(a),R.value=String(y);const N=document.createElement("span");return N.className="tools-pane-slider-value",N.textContent=String(y),R.addEventListener("input",()=>{const r=parseFloat(R.value);N.textContent=r%1===0?String(r):r.toFixed(2),A(r)}),x.appendChild(P),x.appendChild(R),x.appendChild(N),x}function L(m,k){const w=document.createElement("div");w.className="tools-pane-section";const a=document.createElement("div");return a.className="tools-pane-heading",a.textContent=m,w.appendChild(a),k(w),w}function B(m,k,w){const a=document.createElement("input");a.className="tools-pane-inline-input",a.value=k,a.type="text";const y=m.innerHTML;m.innerHTML="",m.classList.add("tools-pane-editing"),m.appendChild(a),a.focus(),a.select();function A(){const x=a.value.trim();m.classList.remove("tools-pane-editing"),x&&x!==k?w(x):m.innerHTML=y}a.addEventListener("keydown",x=>{x.key==="Enter"&&(x.preventDefault(),A()),x.key==="Escape"&&(m.innerHTML=y,m.classList.remove("tools-pane-editing"))}),a.addEventListener("blur",A)}function M(m){const k=new Map,w=new Map,a=new Map,y=new Set;for(const r of m.nodes)k.set(r.type,(k.get(r.type)??0)+1);for(const r of m.edges)w.set(r.type,(w.get(r.type)??0)+1),a.set(r.sourceId,(a.get(r.sourceId)??0)+1),a.set(r.targetId,(a.get(r.targetId)??0)+1),y.add(r.sourceId),y.add(r.targetId);const A=r=>ut(r.properties)??r.id,x=m.nodes.filter(r=>!y.has(r.id)).map(r=>({id:r.id,label:A(r),type:r.type})),P=[...k.entries()].filter(([,r])=>r===1).map(([r])=>({name:r})),R=m.nodes.filter(r=>Object.keys(r.properties).length===0).map(r=>({id:r.id,label:r.id,type:r.type})),N=m.nodes.map(r=>({id:r.id,label:A(r),type:r.type,connections:a.get(r.id)??0})).filter(r=>r.connections>0).sort((r,X)=>X.connections-r.connections).slice(0,5);return{nodeCount:m.nodes.length,edgeCount:m.edges.length,types:[...k.entries()].sort((r,X)=>X[1]-r[1]).map(([r,X])=>({name:r,count:X})),edgeTypes:[...w.entries()].sort((r,X)=>X[1]-r[1]).map(([r,X])=>({name:r,count:X})),orphans:x,singletons:P,emptyNodes:R,mostConnected:N}}return{collapse(){e=!0,Y.classList.add("hidden"),j.classList.remove("active")},addToFocusSet(m){for(const k of m)h.nodeIds.add(k);q(),D()},clearFocusSet(){h.types.clear(),h.nodeIds.clear(),q(),D()},setData(m){u=m,E=null,h.types.clear(),h.nodeIds.clear(),u&&u.nodes.length>0?(i=M(u),j.classList.remove("hidden"),D()):(i=null,j.classList.add("hidden"),Y.classList.add("hidden"))}}}function ut(s){for(const o of Object.values(s))if(typeof o=="string")return o;return null}const mt=[{key:"/",alt:"Ctrl+K",description:"Focus search"},{key:"Ctrl+Z",description:"Undo"},{key:"Ctrl+Shift+Z",description:"Redo"},{key:"?",description:"Show this help"},{key:"F",description:"Focus on selected / exit focus"},{key:"Esc",description:"Exit focus / close panel"},{key:"Click",description:"Select node"},{key:"Ctrl+Click",description:"Multi-select nodes"},{key:"Drag",description:"Pan canvas"},{key:"Scroll",description:"Zoom in/out"}];function ht(s){const o=document.createElement("div");o.className="shortcuts-overlay hidden";const u=document.createElement("div");u.className="shortcuts-modal";const i=document.createElement("h3");i.className="shortcuts-title",i.textContent="Keyboard Shortcuts";const e=document.createElement("div");e.className="shortcuts-list";for(const c of mt){const h=document.createElement("div");h.className="shortcuts-row";const v=document.createElement("div");v.className="shortcuts-keys";const I=document.createElement("kbd");if(I.textContent=c.key,v.appendChild(I),c.alt){const q=document.createElement("span");q.className="shortcuts-or",q.textContent="or",v.appendChild(q);const j=document.createElement("kbd");j.textContent=c.alt,v.appendChild(j)}const T=document.createElement("span");T.className="shortcuts-desc",T.textContent=c.description,h.appendChild(v),h.appendChild(T),e.appendChild(h)}const E=document.createElement("button");E.className="shortcuts-close",E.textContent="×",u.appendChild(E),u.appendChild(i),u.appendChild(e),o.appendChild(u),s.appendChild(o);function t(){o.classList.remove("hidden")}function l(){o.classList.add("hidden")}return E.addEventListener("click",l),o.addEventListener("click",c=>{c.target===o&&l()}),{show:t,hide:l}}function ft(s){const o=document.createElement("div");return o.className="empty-state",o.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
|
+
`,s.appendChild(o),{show(){o.classList.remove("hidden")},hide(){o.classList.add("hidden")}}}const gt=30;function yt(){let s=[],o=[];return{push(u){s.push(JSON.stringify(u)),s.length>gt&&s.shift(),o=[]},undo(u){return s.length===0?null:(o.push(JSON.stringify(u)),JSON.parse(s.pop()))},redo(u){return o.length===0?null:(s.push(JSON.stringify(u)),JSON.parse(o.pop()))},canUndo(){return s.length>0},canRedo(){return o.length>0},clear(){s=[],o=[]}}}let le="",b=null;async function Ct(){const s=document.getElementById("canvas-container"),o=window.matchMedia("(prefers-color-scheme: dark)"),i=localStorage.getItem("backpack-theme")??(o.matches?"dark":"light");document.documentElement.setAttribute("data-theme",i);const e=document.createElement("button");e.className="theme-toggle",e.textContent=i==="light"?"☾":"☼",e.title="Toggle light/dark mode",e.addEventListener("click",()=>{const d=document.documentElement.getAttribute("data-theme")==="light"?"dark":"light";document.documentElement.setAttribute("data-theme",d),localStorage.setItem("backpack-theme",d),e.textContent=d==="light"?"☾":"☼"}),s.appendChild(e);const E=yt();async function t(){if(!le||!b)return;b.metadata.updatedAt=new Date().toISOString(),await Re(le,b),c.loadGraph(b),Y.setLearningGraphData(b),D.setData(b);const n=await Ie();a.setSummaries(n)}async function l(n){b=n,await Re(le,b),c.loadGraph(b),Y.setLearningGraphData(b),D.setData(b);const d=await Ie();a.setSummaries(d)}let c;const h=it(s,{onUpdateNode(n,d){if(!b)return;E.push(b);const f=b.nodes.find(g=>g.id===n);f&&(f.properties={...f.properties,...d},f.updatedAt=new Date().toISOString(),t().then(()=>h.show([n],b)))},onChangeNodeType(n,d){if(!b)return;E.push(b);const f=b.nodes.find(g=>g.id===n);f&&(f.type=d,f.updatedAt=new Date().toISOString(),t().then(()=>h.show([n],b)))},onDeleteNode(n){b&&(E.push(b),b.nodes=b.nodes.filter(d=>d.id!==n),b.edges=b.edges.filter(d=>d.sourceId!==n&&d.targetId!==n),t())},onDeleteEdge(n){var f;if(!b)return;E.push(b);const d=(f=b.edges.find(g=>g.id===n))==null?void 0:f.sourceId;b.edges=b.edges.filter(g=>g.id!==n),t().then(()=>{d&&b&&h.show([d],b)})},onAddProperty(n,d,f){if(!b)return;E.push(b);const g=b.nodes.find(F=>F.id===n);g&&(g.properties[d]=f,g.updatedAt=new Date().toISOString(),t().then(()=>h.show([n],b)))}},n=>{c.panToNode(n)},n=>{D.addToFocusSet(n)}),v=window.matchMedia("(max-width: 768px)");let I=[],T=null;function q(n){T&&T.remove(),T=document.createElement("div"),T.className="focus-indicator";const d=document.createElement("span");d.className="focus-indicator-label",d.textContent=`Focused: ${n.totalNodes} nodes`;const f=document.createElement("span");f.className="focus-indicator-hops",f.textContent=`${n.hops}`;const g=document.createElement("button");g.className="focus-indicator-btn",g.textContent="−",g.title="Fewer hops",g.disabled=n.hops===0,g.addEventListener("click",()=>{c.enterFocus(n.seedNodeIds,Math.max(0,n.hops-1))});const F=document.createElement("button");F.className="focus-indicator-btn",F.textContent="+",F.title="More hops",F.disabled=!1,F.addEventListener("click",()=>{c.enterFocus(n.seedNodeIds,n.hops+1)});const W=document.createElement("button");W.className="focus-indicator-btn focus-indicator-exit",W.textContent="×",W.title="Exit focus (Esc)",W.addEventListener("click",()=>D.clearFocusSet()),T.appendChild(d),T.appendChild(g),T.appendChild(f),T.appendChild(F),T.appendChild(W)}function j(){T&&(T.remove(),T=null)}c=at(s,n=>{I=n??[],n&&n.length>0&&b?(h.show(n,b),v.matches&&D.collapse(),x(le,n)):(h.hide(),le&&x(le))},n=>{if(n){q(n);const d=s.querySelector(".canvas-top-left");d&&T&&d.appendChild(T),x(le,n.seedNodeIds)}else j(),le&&x(le)});const Y=rt(s),D=pt(s,{onFilterByType(n){if(b)if(n===null)c.setFilteredNodeIds(null);else{const d=new Set(((b==null?void 0:b.nodes)??[]).filter(f=>f.type===n).map(f=>f.id));c.setFilteredNodeIds(d)}},onNavigateToNode(n){c.panToNode(n),b&&h.show([n],b)},onFocusChange(n){n&&n.length>0?c.enterFocus(n,1):c.isFocused()&&c.exitFocus()},onRenameNodeType(n,d){if(b){E.push(b);for(const f of b.nodes)f.type===n&&(f.type=d,f.updatedAt=new Date().toISOString());t()}},onRenameEdgeType(n,d){if(b){E.push(b);for(const f of b.edges)f.type===n&&(f.type=d);t()}},onToggleEdgeLabels(n){c.setEdgeLabels(n)},onToggleTypeHulls(n){c.setTypeHulls(n)},onToggleMinimap(n){c.setMinimap(n)},onLayoutChange(n,d){et({[n]:d}),c.reheat()},onExport(n){const d=c.exportImage(n);if(!d)return;const f=document.createElement("a");f.download=`${le||"graph"}.${n}`,f.href=d,f.click()},onOpen(){v.matches&&h.hide()}}),_=document.createElement("div");_.className="canvas-top-bar";const L=document.createElement("div");L.className="canvas-top-left";const B=document.createElement("div");B.className="canvas-top-center";const M=document.createElement("div");M.className="canvas-top-right";const m=s.querySelector(".tools-pane-toggle");m&&L.appendChild(m);const k=s.querySelector(".search-overlay");k&&B.appendChild(k);const w=s.querySelector(".zoom-controls");w&&M.appendChild(w),M.appendChild(e),_.appendChild(L),_.appendChild(B),_.appendChild(M),s.appendChild(_),Y.onFilterChange(n=>{c.setFilteredNodeIds(n)}),Y.onNodeSelect(n=>{c.isFocused()&&D.clearFocusSet(),c.panToNode(n),b&&h.show([n],b)});const a=Ke(document.getElementById("sidebar"),{onSelect:n=>R(n),onRename:async(n,d)=>{await _e(n,d),le===n&&(le=d);const f=await Ie();a.setSummaries(f),a.setActive(le),le===d&&(b=await Pe(d),c.loadGraph(b),Y.setLearningGraphData(b),D.setData(b))}}),y=ht(s),A=ft(s);function x(n,d){const f=[];d!=null&&d.length&&f.push("node="+d.map(encodeURIComponent).join(","));const g=c.getFocusInfo();g&&(f.push("focus="+g.seedNodeIds.map(encodeURIComponent).join(",")),f.push("hops="+g.hops));const F="#"+encodeURIComponent(n)+(f.length?"?"+f.join("&"):"");history.replaceState(null,"",F)}function P(){const n=window.location.hash.slice(1);if(!n)return{graph:null,nodes:[],focus:[],hops:1};const[d,f]=n.split("?"),g=d?decodeURIComponent(d):null;let F=[],W=[],K=1;if(f){const ae=new URLSearchParams(f),V=ae.get("node");V&&(F=V.split(",").map(decodeURIComponent));const re=ae.get("focus");re&&(W=re.split(",").map(decodeURIComponent));const Z=ae.get("hops");Z&&(K=Math.max(0,parseInt(Z,10)||1))}return{graph:g,nodes:F,focus:W,hops:K}}async function R(n,d,f,g){if(le=n,a.setActive(n),h.hide(),j(),Y.clear(),E.clear(),b=await Pe(n),c.loadGraph(b),Y.setLearningGraphData(b),D.setData(b),A.hide(),x(n),f!=null&&f.length&&b){const F=f.filter(W=>b.nodes.some(K=>K.id===W));if(F.length){setTimeout(()=>{c.enterFocus(F,g??1)},500);return}}if(d!=null&&d.length&&b){const F=d.filter(W=>b.nodes.some(K=>K.id===W));F.length&&setTimeout(()=>{c.panToNodes(F),b&&h.show(F,b),x(n,F)},500)}}const N=await Ie();a.setSummaries(N);const r=P(),X=r.graph&&N.some(n=>n.name===r.graph)?r.graph:N.length>0?N[0].name:null;X?await R(X,r.nodes.length?r.nodes:void 0,r.focus.length?r.focus:void 0,r.hops):A.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(),Y.focus();else if(n.key==="z"&&(n.metaKey||n.ctrlKey)&&n.shiftKey){if(n.preventDefault(),b){const d=E.redo(b);d&&l(d)}}else if(n.key==="z"&&(n.metaKey||n.ctrlKey)){if(n.preventDefault(),b){const d=E.undo(b);d&&l(d)}}else n.key==="f"||n.key==="F"?c.isFocused()?D.clearFocusSet():I.length>0&&D.addToFocusSet(I):n.key==="?"?y.show():n.key==="Escape"&&(c.isFocused()?D.clearFocusSet():y.hide())}),window.addEventListener("hashchange",()=>{const n=P();if(n.graph&&n.graph!==le)R(n.graph,n.nodes.length?n.nodes:void 0,n.focus.length?n.focus:void 0,n.hops);else if(n.graph&&n.focus.length&&b)c.enterFocus(n.focus,n.hops);else if(n.graph&&n.nodes.length&&b){c.isFocused()&&c.exitFocus();const d=n.nodes.filter(f=>b.nodes.some(g=>g.id===f));d.length&&(c.panToNodes(d),h.show(d,b))}})}Ct();
|
|
@@ -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}.focus-indicator{display:flex;align-items:center;gap:2px;background:var(--bg-surface);border:1px solid rgba(212,162,127,.4);border-radius:8px;padding:4px 6px 4px 10px;box-shadow:0 2px 8px var(--shadow)}.focus-indicator-label{font-size:11px;color:var(--accent);font-weight:500;white-space:nowrap;margin-right:4px}.focus-indicator-hops{font-size:11px;color:var(--text-muted);font-family:monospace;min-width:12px;text-align:center}.focus-indicator-btn{background:none;border:none;color:var(--text-muted);font-size:14px;cursor:pointer;padding:2px 4px;line-height:1;border-radius:4px;transition:color .15s,background .15s}.focus-indicator-btn:hover:not(:disabled){color:var(--text);background:var(--bg-hover)}.focus-indicator-btn:disabled{color:var(--text-dim);cursor:default;opacity:.3}.focus-indicator-exit{font-size:16px;margin-left:2px}.focus-indicator-exit:hover{color:#ef4444!important}.info-focus-btn{font-size:14px}.theme-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.theme-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.zoom-controls{display:flex;gap:4px}.zoom-btn{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.zoom-btn:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}#canvas-container{flex:1;position:relative;overflow:hidden;touch-action:none}#graph-canvas{position:absolute;top:0;left:0;touch-action:none;width:100%;height:100%;cursor:grab}#graph-canvas:active{cursor:grabbing}.search-overlay{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;max-height:calc(100vh - 48px);pointer-events:none}.search-overlay>*{pointer-events:auto}.search-overlay.hidden{display:none}.search-input-wrap{position:relative;display:flex;align-items:center;gap:6px;width:380px;max-width:calc(100vw - 340px)}.search-input{flex:1;min-width:0;padding:10px 36px 10px 16px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text);font-size:14px;outline:none;transition:border-color .15s,box-shadow .15s}.search-input:focus{border-color:#d4a27f66;box-shadow:0 0 0 3px #d4a27f1a}.search-input::placeholder{color:var(--text-dim)}.search-kbd{position:absolute;right:10px;top:50%;transform:translateY(-50%);padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg-surface);color:var(--text-dim);font-size:11px;font-family:monospace;pointer-events:none}.search-kbd.hidden{display:none}.search-results{list-style:none;width:380px;max-width:calc(100vw - 340px);background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:10px;overflow:hidden;box-shadow:0 8px 32px var(--shadow-strong)}.search-results.hidden{display:none}.search-result-item{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;transition:background .1s}.search-result-item:hover{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;font-family:inherit;color:var(--text);flex:1;min-width:0;resize:vertical;overflow:hidden;line-height:1.4;max-height:300px}.info-edit-input:focus{outline:none;border-color:var(--accent)}.info-delete-prop{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;padding:0 2px;flex-shrink:0;opacity:0;transition:opacity .1s,color .1s}.info-props dd:hover .info-delete-prop{opacity:1}.info-delete-prop:hover{color:#ef4444}.info-add-btn{background:none;border:1px dashed var(--border);border-radius:4px;padding:6px 10px;font-size:12px;color:var(--text-dim);cursor:pointer;width:100%;margin-top:8px;transition:border-color .15s,color .15s}.info-add-btn:hover{border-color:var(--accent);color:var(--text)}.info-add-row{display:flex;gap:4px;margin-top:6px}.info-add-save{background:var(--accent);border:none;border-radius:4px;padding:3px 10px;font-size:12px;color:var(--badge-text);cursor:pointer;flex-shrink:0}.info-add-save:hover{background:var(--accent-hover)}.info-delete-edge{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;margin-left:auto;padding:0 2px;opacity:0;transition:opacity .1s,color .1s}.info-connection:hover .info-delete-edge{opacity:1}.info-delete-edge:hover{color:#ef4444}.info-danger{margin-top:8px;padding-top:12px;border-top:1px solid var(--border)}.info-delete-node{background:none;border:1px solid rgba(239,68,68,.3);border-radius:6px;padding:6px 12px;font-size:12px;color:#ef4444;cursor:pointer;width:100%;transition:background .15s}.info-delete-node:hover{background:#ef44441a}.tools-pane-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.tools-pane-toggle.hidden{display:none}.tools-pane-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.tools-pane-toggle.active{color:var(--accent);border-color:#d4a27f66}.tools-pane-content{position:absolute;top:56px;left:16px;bottom:16px;z-index:20;width:200px;overflow-y:auto;background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:12px;box-shadow:0 8px 32px var(--shadow)}.tools-pane-content.hidden{display:none}.tools-pane-section{margin-bottom:12px}.tools-pane-section:last-child{margin-bottom:0}.tools-pane-heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);margin-bottom:6px}.tools-pane-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:12px}.tools-pane-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.tools-pane-name{flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tools-pane-count{color:var(--text-dim);font-size:11px;flex-shrink:0}.tools-pane-summary{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-muted);padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--border)}.tools-pane-sep{color:var(--text-dim)}.tools-pane-clickable{cursor:pointer;border-radius:4px;padding:3px 4px;margin:0 -4px;transition:background .1s}.tools-pane-clickable:hover{background:var(--bg-hover)}.tools-pane-clickable.active{background:var(--bg-hover);outline:1px solid var(--border)}.tools-pane-badge{font-size:9px;color:var(--accent);flex-shrink:0;opacity:.8}.tools-pane-issue .tools-pane-name{color:var(--text-muted)}.tools-pane-more{font-size:10px;color:var(--text-dim);padding:4px 0 0}.tools-pane-edit{background:none;border:none;color:var(--text-dim);font-size:11px;cursor:pointer;padding:0 2px;opacity:0;transition:opacity .1s,color .1s;flex-shrink:0}.tools-pane-row:hover .tools-pane-edit{opacity:1}.tools-pane-edit:hover{color:var(--accent)}.tools-pane-focus-toggle{opacity:.4;font-size:11px}.tools-pane-focus-active{opacity:1!important;color:var(--accent)!important}.tools-pane-focus-clear{margin-top:4px;border-top:1px solid var(--border);padding-top:6px}.tools-pane-editing{background:none!important}.tools-pane-inline-input{width:100%;background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:12px;padding:2px 6px;outline:none}.tools-pane-slider-row{display:flex;align-items:center;gap:6px;padding:4px 0}.tools-pane-slider-label{font-size:11px;color:var(--text-muted);white-space:nowrap;min-width:56px}.tools-pane-slider{flex:1;min-width:0;height:4px;accent-color:var(--accent);cursor:pointer}.tools-pane-slider-value{font-size:10px;color:var(--text-dim);min-width:28px;text-align:right;font-family:monospace}.tools-pane-checkbox{width:14px;height:14px;accent-color:var(--accent);cursor:pointer;flex-shrink:0}.tools-pane-export-row{display:flex;gap:4px;margin-top:6px}.tools-pane-export-btn{flex:1;padding:4px 8px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s}.tools-pane-export-btn:hover{color:var(--text);border-color:var(--text-muted)}.tools-pane-empty{font-size:11px;color:var(--text-dim);text-align:center;padding:8px 0}.empty-state{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;z-index:5;pointer-events:none}.empty-state.hidden{display:none}.empty-state-content{text-align:center;max-width:420px;padding:40px 24px}.empty-state-icon{color:var(--text-dim);margin-bottom:16px}.empty-state-title{font-size:18px;font-weight:600;color:var(--text);margin-bottom:8px}.empty-state-desc{font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:20px}.empty-state-setup{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:16px}.empty-state-label{font-size:11px;color:var(--text-dim);margin-bottom:8px}.empty-state-code{display:block;font-size:12px;color:var(--accent);font-family:monospace;word-break:break-all}.empty-state-hint{font-size:11px;color:var(--text-dim)}.empty-state-hint kbd{padding:1px 5px;border:1px solid var(--border);border-radius:3px;background:var(--bg-surface);font-family:monospace;font-size:11px}.shortcuts-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.shortcuts-overlay.hidden{display:none}.shortcuts-modal{background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;padding:24px;min-width:300px;max-width:400px;box-shadow:0 16px 48px var(--shadow);position:relative}.shortcuts-close{position:absolute;top:12px;right:14px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;padding:0 4px;line-height:1}.shortcuts-close:hover{color:var(--text)}.shortcuts-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:16px}.shortcuts-list{display:flex;flex-direction:column;gap:8px}.shortcuts-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.shortcuts-keys{display:flex;align-items:center;gap:4px}.shortcuts-keys kbd{padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text);font-size:11px;font-family:monospace}.shortcuts-or{font-size:10px;color:var(--text-dim)}.shortcuts-desc{font-size:12px;color:var(--text-muted)}@media(max-width:768px){#app{flex-direction:column}#sidebar{width:100%;min-width:0;max-height:35vh;border-right:none;border-bottom:1px solid var(--border)}.info-panel{top:auto;bottom:72px;right:8px;left:8px;width:auto;max-height:calc(100% - 200px);overflow-y:auto}.info-panel.info-panel-maximized{bottom:0;left:0;right:0}.canvas-top-bar{top:8px;left:8px;right:8px}.tools-pane-content{top:48px;left:8px;bottom:80px;width:160px;max-width:calc(100vw - 24px)}.tools-pane-edit{opacity:.6}}
|
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-Mi0vDG5K.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-z15vEFEy.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app">
|
package/dist/canvas.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { LearningGraphData } from "backpack-ontology";
|
|
2
|
-
export
|
|
2
|
+
export interface FocusInfo {
|
|
3
|
+
seedNodeIds: string[];
|
|
4
|
+
hops: number;
|
|
5
|
+
totalNodes: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function initCanvas(container: HTMLElement, onNodeClick?: (nodeIds: string[] | null) => void, onFocusChange?: (focus: FocusInfo | null) => void): {
|
|
3
8
|
loadGraph(data: LearningGraphData): void;
|
|
4
9
|
setFilteredNodeIds(ids: Set<string> | null): void;
|
|
5
10
|
panToNode(nodeId: string): void;
|
|
@@ -9,5 +14,9 @@ export declare function initCanvas(container: HTMLElement, onNodeClick?: (nodeId
|
|
|
9
14
|
setMinimap(visible: boolean): void;
|
|
10
15
|
reheat(): void;
|
|
11
16
|
exportImage(format: "png" | "svg"): string;
|
|
17
|
+
enterFocus(seedNodeIds: string[], hops: number): void;
|
|
18
|
+
exitFocus(): void;
|
|
19
|
+
isFocused(): boolean;
|
|
20
|
+
getFocusInfo(): FocusInfo | null;
|
|
12
21
|
destroy(): void;
|
|
13
22
|
};
|
package/dist/canvas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLayout, tick } from "./layout";
|
|
1
|
+
import { createLayout, extractSubgraph, tick } from "./layout";
|
|
2
2
|
import { getColor } from "./colors";
|
|
3
3
|
/** Read a CSS custom property from :root. */
|
|
4
4
|
function cssVar(name) {
|
|
@@ -6,7 +6,7 @@ function cssVar(name) {
|
|
|
6
6
|
}
|
|
7
7
|
const NODE_RADIUS = 20;
|
|
8
8
|
const ALPHA_MIN = 0.001;
|
|
9
|
-
export function initCanvas(container, onNodeClick) {
|
|
9
|
+
export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
10
10
|
const canvas = container.querySelector("canvas");
|
|
11
11
|
const ctx = canvas.getContext("2d");
|
|
12
12
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -19,6 +19,12 @@ export function initCanvas(container, onNodeClick) {
|
|
|
19
19
|
let showEdgeLabels = true;
|
|
20
20
|
let showTypeHulls = true;
|
|
21
21
|
let showMinimap = true;
|
|
22
|
+
// Focus mode state
|
|
23
|
+
let lastLoadedData = null;
|
|
24
|
+
let focusSeedIds = null;
|
|
25
|
+
let focusHops = 1;
|
|
26
|
+
let savedFullState = null;
|
|
27
|
+
let savedFullCamera = null;
|
|
22
28
|
// Pan animation state
|
|
23
29
|
let panTarget = null;
|
|
24
30
|
let panStart = null;
|
|
@@ -586,6 +592,11 @@ export function initCanvas(container, onNodeClick) {
|
|
|
586
592
|
return {
|
|
587
593
|
loadGraph(data) {
|
|
588
594
|
cancelAnimationFrame(animFrame);
|
|
595
|
+
lastLoadedData = data;
|
|
596
|
+
// Exit any active focus when full graph reloads
|
|
597
|
+
focusSeedIds = null;
|
|
598
|
+
savedFullState = null;
|
|
599
|
+
savedFullCamera = null;
|
|
589
600
|
state = createLayout(data);
|
|
590
601
|
alpha = 1;
|
|
591
602
|
selectedNodeIds = new Set();
|
|
@@ -711,6 +722,74 @@ export function initCanvas(container, onNodeClick) {
|
|
|
711
722
|
</svg>`;
|
|
712
723
|
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
713
724
|
},
|
|
725
|
+
enterFocus(seedNodeIds, hops) {
|
|
726
|
+
if (!lastLoadedData || !state)
|
|
727
|
+
return;
|
|
728
|
+
// Save current full-graph state
|
|
729
|
+
if (!focusSeedIds) {
|
|
730
|
+
savedFullState = state;
|
|
731
|
+
savedFullCamera = { ...camera };
|
|
732
|
+
}
|
|
733
|
+
focusSeedIds = seedNodeIds;
|
|
734
|
+
focusHops = hops;
|
|
735
|
+
const subgraph = extractSubgraph(lastLoadedData, seedNodeIds, hops);
|
|
736
|
+
cancelAnimationFrame(animFrame);
|
|
737
|
+
state = createLayout(subgraph);
|
|
738
|
+
alpha = 1;
|
|
739
|
+
selectedNodeIds = new Set(seedNodeIds);
|
|
740
|
+
filteredNodeIds = null;
|
|
741
|
+
// Center camera
|
|
742
|
+
camera = { x: 0, y: 0, scale: 1 };
|
|
743
|
+
if (state.nodes.length > 0) {
|
|
744
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
745
|
+
for (const n of state.nodes) {
|
|
746
|
+
if (n.x < minX)
|
|
747
|
+
minX = n.x;
|
|
748
|
+
if (n.y < minY)
|
|
749
|
+
minY = n.y;
|
|
750
|
+
if (n.x > maxX)
|
|
751
|
+
maxX = n.x;
|
|
752
|
+
if (n.y > maxY)
|
|
753
|
+
maxY = n.y;
|
|
754
|
+
}
|
|
755
|
+
const cx = (minX + maxX) / 2;
|
|
756
|
+
const cy = (minY + maxY) / 2;
|
|
757
|
+
camera.x = cx - canvas.clientWidth / 2;
|
|
758
|
+
camera.y = cy - canvas.clientHeight / 2;
|
|
759
|
+
}
|
|
760
|
+
simulate();
|
|
761
|
+
onFocusChange?.({
|
|
762
|
+
seedNodeIds,
|
|
763
|
+
hops,
|
|
764
|
+
totalNodes: subgraph.nodes.length,
|
|
765
|
+
});
|
|
766
|
+
},
|
|
767
|
+
exitFocus() {
|
|
768
|
+
if (!focusSeedIds || !savedFullState)
|
|
769
|
+
return;
|
|
770
|
+
cancelAnimationFrame(animFrame);
|
|
771
|
+
state = savedFullState;
|
|
772
|
+
camera = savedFullCamera ?? { x: 0, y: 0, scale: 1 };
|
|
773
|
+
focusSeedIds = null;
|
|
774
|
+
savedFullState = null;
|
|
775
|
+
savedFullCamera = null;
|
|
776
|
+
selectedNodeIds = new Set();
|
|
777
|
+
filteredNodeIds = null;
|
|
778
|
+
render();
|
|
779
|
+
onFocusChange?.(null);
|
|
780
|
+
},
|
|
781
|
+
isFocused() {
|
|
782
|
+
return focusSeedIds !== null;
|
|
783
|
+
},
|
|
784
|
+
getFocusInfo() {
|
|
785
|
+
if (!focusSeedIds || !state)
|
|
786
|
+
return null;
|
|
787
|
+
return {
|
|
788
|
+
seedNodeIds: focusSeedIds,
|
|
789
|
+
hops: focusHops,
|
|
790
|
+
totalNodes: state.nodes.length,
|
|
791
|
+
};
|
|
792
|
+
},
|
|
714
793
|
destroy() {
|
|
715
794
|
cancelAnimationFrame(animFrame);
|
|
716
795
|
observer.disconnect();
|
package/dist/info-panel.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface EditCallbacks {
|
|
|
6
6
|
onDeleteEdge(edgeId: string): void;
|
|
7
7
|
onAddProperty(nodeId: string, key: string, value: string): void;
|
|
8
8
|
}
|
|
9
|
-
export declare function initInfoPanel(container: HTMLElement, callbacks?: EditCallbacks, onNavigateToNode?: (nodeId: string) => void): {
|
|
9
|
+
export declare function initInfoPanel(container: HTMLElement, callbacks?: EditCallbacks, onNavigateToNode?: (nodeId: string) => void, onFocus?: (nodeIds: string[]) => void): {
|
|
10
10
|
show(nodeIds: string[], data: LearningGraphData): void;
|
|
11
11
|
hide: () => void;
|
|
12
12
|
readonly visible: boolean;
|
package/dist/info-panel.js
CHANGED
|
@@ -8,7 +8,7 @@ function nodeLabel(node) {
|
|
|
8
8
|
return node.id;
|
|
9
9
|
}
|
|
10
10
|
const EDIT_ICON = '\u270E'; // pencil
|
|
11
|
-
export function initInfoPanel(container, callbacks, onNavigateToNode) {
|
|
11
|
+
export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
12
12
|
const panel = document.createElement("div");
|
|
13
13
|
panel.id = "info-panel";
|
|
14
14
|
panel.className = "info-panel hidden";
|
|
@@ -19,6 +19,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode) {
|
|
|
19
19
|
let historyIndex = -1;
|
|
20
20
|
let navigatingHistory = false;
|
|
21
21
|
let lastData = null;
|
|
22
|
+
let currentNodeIds = [];
|
|
22
23
|
function hide() {
|
|
23
24
|
panel.classList.add("hidden");
|
|
24
25
|
panel.classList.remove("info-panel-maximized");
|
|
@@ -76,6 +77,17 @@ export function initInfoPanel(container, callbacks, onNavigateToNode) {
|
|
|
76
77
|
fwdBtn.disabled = historyIndex >= history.length - 1;
|
|
77
78
|
fwdBtn.addEventListener("click", goForward);
|
|
78
79
|
toolbar.appendChild(fwdBtn);
|
|
80
|
+
// Focus
|
|
81
|
+
if (onFocus && currentNodeIds.length > 0) {
|
|
82
|
+
const focusBtn = document.createElement("button");
|
|
83
|
+
focusBtn.className = "info-toolbar-btn info-focus-btn";
|
|
84
|
+
focusBtn.textContent = "\u25CE"; // bullseye
|
|
85
|
+
focusBtn.title = "Focus on neighborhood (F)";
|
|
86
|
+
focusBtn.addEventListener("click", () => {
|
|
87
|
+
onFocus(currentNodeIds);
|
|
88
|
+
});
|
|
89
|
+
toolbar.appendChild(focusBtn);
|
|
90
|
+
}
|
|
79
91
|
// Maximize/restore
|
|
80
92
|
const maxBtn = document.createElement("button");
|
|
81
93
|
maxBtn.className = "info-toolbar-btn";
|
|
@@ -174,22 +186,26 @@ export function initInfoPanel(container, callbacks, onNavigateToNode) {
|
|
|
174
186
|
const dd = document.createElement("dd");
|
|
175
187
|
if (callbacks) {
|
|
176
188
|
const valueStr = formatValue(node.properties[key]);
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
189
|
+
const textarea = document.createElement("textarea");
|
|
190
|
+
textarea.className = "info-edit-input";
|
|
191
|
+
textarea.value = valueStr;
|
|
192
|
+
textarea.rows = 1;
|
|
193
|
+
textarea.addEventListener("input", () => autoResize(textarea));
|
|
194
|
+
textarea.addEventListener("keydown", (e) => {
|
|
195
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
textarea.blur();
|
|
184
198
|
}
|
|
185
199
|
});
|
|
186
|
-
|
|
187
|
-
const newVal =
|
|
200
|
+
textarea.addEventListener("blur", () => {
|
|
201
|
+
const newVal = textarea.value;
|
|
188
202
|
if (newVal !== valueStr) {
|
|
189
203
|
callbacks.onUpdateNode(nodeId, { [key]: tryParseValue(newVal) });
|
|
190
204
|
}
|
|
191
205
|
});
|
|
192
|
-
dd.appendChild(
|
|
206
|
+
dd.appendChild(textarea);
|
|
207
|
+
// Auto-size after append
|
|
208
|
+
requestAnimationFrame(() => autoResize(textarea));
|
|
193
209
|
// Delete property button
|
|
194
210
|
const delProp = document.createElement("button");
|
|
195
211
|
delProp.className = "info-delete-prop";
|
|
@@ -484,6 +500,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode) {
|
|
|
484
500
|
return {
|
|
485
501
|
show(nodeIds, data) {
|
|
486
502
|
lastData = data;
|
|
503
|
+
currentNodeIds = nodeIds;
|
|
487
504
|
// Track history for single-node views
|
|
488
505
|
if (nodeIds.length === 1 && !navigatingHistory) {
|
|
489
506
|
const nodeId = nodeIds[0];
|
|
@@ -568,6 +585,10 @@ function tryParseValue(str) {
|
|
|
568
585
|
}
|
|
569
586
|
return str;
|
|
570
587
|
}
|
|
588
|
+
function autoResize(textarea) {
|
|
589
|
+
textarea.style.height = "auto";
|
|
590
|
+
textarea.style.height = textarea.scrollHeight + "px";
|
|
591
|
+
}
|
|
571
592
|
function formatTimestamp(iso) {
|
|
572
593
|
try {
|
|
573
594
|
const d = new Date(iso);
|
package/dist/layout.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface LayoutParams {
|
|
|
25
25
|
export declare const DEFAULT_LAYOUT_PARAMS: LayoutParams;
|
|
26
26
|
export declare function setLayoutParams(p: Partial<LayoutParams>): void;
|
|
27
27
|
export declare function getLayoutParams(): LayoutParams;
|
|
28
|
+
/** Extract the N-hop neighborhood of seed nodes as a new subgraph. */
|
|
29
|
+
export declare function extractSubgraph(data: LearningGraphData, seedIds: string[], hops: number): LearningGraphData;
|
|
28
30
|
/** Create a layout state from ontology data. Nodes start grouped by type. */
|
|
29
31
|
export declare function createLayout(data: LearningGraphData): LayoutState;
|
|
30
32
|
/** Run one tick of the force simulation. Returns new alpha. */
|
package/dist/layout.js
CHANGED
|
@@ -30,6 +30,32 @@ function nodeLabel(properties, id) {
|
|
|
30
30
|
}
|
|
31
31
|
return id;
|
|
32
32
|
}
|
|
33
|
+
/** Extract the N-hop neighborhood of seed nodes as a new subgraph. */
|
|
34
|
+
export function extractSubgraph(data, seedIds, hops) {
|
|
35
|
+
const visited = new Set(seedIds);
|
|
36
|
+
let frontier = new Set(seedIds);
|
|
37
|
+
for (let h = 0; h < hops; h++) {
|
|
38
|
+
const next = new Set();
|
|
39
|
+
for (const edge of data.edges) {
|
|
40
|
+
if (frontier.has(edge.sourceId) && !visited.has(edge.targetId)) {
|
|
41
|
+
next.add(edge.targetId);
|
|
42
|
+
}
|
|
43
|
+
if (frontier.has(edge.targetId) && !visited.has(edge.sourceId)) {
|
|
44
|
+
next.add(edge.sourceId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const id of next)
|
|
48
|
+
visited.add(id);
|
|
49
|
+
frontier = next;
|
|
50
|
+
if (next.size === 0)
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
nodes: data.nodes.filter((n) => visited.has(n.id)),
|
|
55
|
+
edges: data.edges.filter((e) => visited.has(e.sourceId) && visited.has(e.targetId)),
|
|
56
|
+
metadata: data.metadata,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
33
59
|
/** Create a layout state from ontology data. Nodes start grouped by type. */
|
|
34
60
|
export function createLayout(data) {
|
|
35
61
|
const nodeMap = new Map();
|