backpack-viewer 0.2.16 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -1
- package/bin/serve.js +155 -0
- package/dist/api.d.ts +27 -0
- package/dist/api.js +71 -0
- package/dist/app/assets/index-CTM-vKgB.js +21 -0
- package/dist/app/assets/index-CjzMJjZ-.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +22 -1
- package/dist/canvas.js +143 -65
- package/dist/config.d.ts +4 -0
- package/dist/config.js +32 -0
- package/dist/default-config.json +66 -0
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +119 -0
- package/dist/info-panel.d.ts +4 -0
- package/dist/info-panel.js +66 -11
- package/dist/keybindings.d.ts +6 -0
- package/dist/keybindings.js +67 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +18 -8
- package/dist/main.js +175 -42
- package/dist/search.d.ts +5 -1
- package/dist/search.js +46 -85
- package/dist/shortcuts.d.ts +3 -1
- package/dist/shortcuts.js +51 -19
- package/dist/sidebar.d.ts +8 -0
- package/dist/sidebar.js +106 -1
- package/dist/style.css +328 -6
- package/dist/tools-pane.d.ts +10 -0
- package/dist/tools-pane.js +407 -148
- package/package.json +1 -1
- package/dist/app/assets/index-Mi0vDG5K.js +0 -21
- package/dist/app/assets/index-z15vEFEy.css +0 -1
|
@@ -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.sidebar-collapsed{width:0;min-width:0;padding:0;overflow:hidden;border-right:none}.sidebar-heading-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.sidebar-heading-row h2{margin-bottom:0}.sidebar-collapse-btn{background:none;border:1px solid var(--border);border-radius:6px;color:var(--text-dim);cursor:pointer;padding:4px 6px;line-height:1;transition:color .15s,border-color .15s}.sidebar-collapse-btn:hover{color:var(--text);border-color:var(--text-muted)}#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-branch{font-size:10px;color:var(--accent);opacity:.7;display:block;margin-top:2px}.sidebar-branch:hover{opacity:1}.branch-picker{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:4px;margin-top:4px;box-shadow:0 4px 16px var(--shadow);z-index:50}.branch-picker-item{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;font-size:12px;color:var(--text);border-radius:4px;cursor:pointer}.branch-picker-item:hover{background:var(--bg-hover)}.branch-picker-active{color:var(--accent);font-weight:600;cursor:default}.branch-picker-active:hover{background:none}.branch-picker-delete{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:14px;padding:0 4px}.branch-picker-delete:hover{color:var(--danger, #e55)}.branch-picker-create{color:var(--accent);font-size:11px;border-top:1px solid var(--border);margin-top:4px;padding-top:8px}.sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);text-align:center}.sidebar-footer a{display:block;font-size:12px;font-weight:500;color:var(--accent);text-decoration:none;margin-bottom:4px}.sidebar-footer a:hover{color:var(--accent-hover)}.sidebar-footer span{display:block;font-size:10px;color:var(--text-dim)}.canvas-top-bar{position:absolute;top:16px;left:16px;right:16px;z-index:30;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none}.canvas-top-left,.canvas-top-center,.canvas-top-right{pointer-events:auto;display:flex;align-items:center;gap:4px}.canvas-top-left{flex-shrink:0;min-width:var(--tools-width, 264px)}.canvas-top-center{flex:1;justify-content:center}.focus-indicator{display:flex;align-items:center;gap:2px;background:var(--bg-surface);border:1px solid rgba(212,162,127,.4);border-radius:8px;padding:4px 6px 4px 10px;box-shadow:0 2px 8px var(--shadow)}.focus-indicator-label{font-size:11px;color:var(--accent);font-weight:500;white-space:nowrap;margin-right:4px}.focus-indicator-hops{font-size:11px;color:var(--text-muted);font-family:monospace;min-width:12px;text-align:center}.focus-indicator-btn{background:none;border:none;color:var(--text-muted);font-size:14px;cursor:pointer;padding:2px 4px;line-height:1;border-radius:4px;transition:color .15s,background .15s}.focus-indicator-btn:hover:not(:disabled){color:var(--text);background:var(--bg-hover)}.focus-indicator-btn:disabled{color:var(--text-dim);cursor:default;opacity:.3}.focus-indicator-exit{font-size:16px;margin-left:2px}.focus-indicator-exit:hover{color:#ef4444!important}.info-focus-btn{font-size:14px}.theme-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.theme-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.zoom-controls{display:flex;gap:4px}.zoom-btn{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.zoom-btn:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}#canvas-container{flex:1;position:relative;overflow:hidden;touch-action:none}#graph-canvas{position:absolute;top:0;left:0;touch-action:none;width:100%;height:100%;cursor:grab}#graph-canvas:active{cursor:grabbing}.search-overlay{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;max-height:calc(100vh - 48px);pointer-events:none}.search-overlay>*{pointer-events:auto}.search-overlay.hidden{display:none}.search-input-wrap{position:relative;display:flex;align-items:center;gap:6px;width:380px;max-width:calc(100vw - 340px)}.search-input{flex:1;min-width:0;padding:10px 36px 10px 16px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text);font-size:14px;outline:none;transition:border-color .15s,box-shadow .15s}.search-input:focus{border-color:#d4a27f66;box-shadow:0 0 0 3px #d4a27f1a}.search-input::placeholder{color:var(--text-dim)}.search-kbd{position:absolute;right:10px;top:50%;transform:translateY(-50%);padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg-surface);color:var(--text-dim);font-size:11px;font-family:monospace;pointer-events:none}.search-kbd.hidden{display:none}.search-results{list-style:none;width:380px;max-width:calc(100vw - 340px);background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:10px;overflow:hidden;box-shadow:0 8px 32px var(--shadow-strong)}.search-results.hidden{display:none}.search-result-item{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;transition:background .1s}.search-result-item:hover,.search-result-active{background:var(--bg-hover)}.search-result-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.search-result-label{font-size:13px;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-result-type{font-size:11px;color:var(--text-dim);flex-shrink:0}.chip-toggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:1px solid var(--glass-border);border-radius:10px;background:var(--glass-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);color:var(--text-dim);cursor:pointer;transition:border-color .15s,color .15s;pointer-events:auto}.chip-toggle:hover{border-color:#d4a27f4d;color:var(--text)}.chip-toggle.active{border-color:#d4a27f66;color:var(--accent)}.type-chips{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;max-width:500px;max-height:200px;overflow-y:auto;padding:4px;border-radius:10px}.type-chips.hidden{display:none}.type-chip{display:flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid transparent;border-radius:12px;background:var(--chip-bg);color:var(--text-dim);font-size:11px;cursor:pointer;transition:all .15s;white-space:nowrap}.type-chip.active{background:var(--chip-bg-active);color:var(--text-muted);border-color:var(--chip-border-active)}.type-chip:hover{background:var(--chip-bg-hover)}.type-chip-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.type-chip:not(.active) .type-chip-dot{opacity:.3}.info-panel{position:absolute;top:56px;right:16px;bottom:16px;width:360px;background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;padding:0;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-header{flex-shrink:0;padding:20px 20px 12px;position:relative}.info-panel-body{flex:1;overflow-y:auto;min-height:0;padding:0 20px 20px}.info-panel.hidden{display:none}.info-panel.info-panel-maximized{top:0;right:0;bottom:0;left:0;width:auto;max-height:none;border-radius:0;z-index:40}.info-panel-toolbar{position:absolute;top:12px;right:14px;display:flex;align-items:center;gap:2px;z-index:1}.info-toolbar-btn{background:none;border:none;color:var(--text-muted);font-size:16px;cursor:pointer;padding:4px 6px;line-height:1;border-radius:4px;transition:color .15s,background .15s}.info-toolbar-btn:hover:not(:disabled){color:var(--text);background:var(--bg-hover)}.info-toolbar-btn:disabled{color:var(--text-dim);cursor:default;opacity:.3}.info-close-btn{font-size:20px}.info-connection-link{cursor:pointer;transition:background .15s}.info-connection-link:hover{background:var(--bg-active)}.info-connection-link .info-target{color:var(--accent);text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color .15s}.info-connection-link:hover .info-target{text-decoration-color:var(--accent)}.info-header{margin-bottom:16px}.info-type-badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;color:var(--badge-text);margin-bottom:8px}.info-label{font-size:18px;font-weight:600;color:var(--text-strong);margin-bottom:4px;word-break:break-word}.info-id{display:block;font-size:11px;color:var(--text-dim);font-family:monospace}.info-section{margin-bottom:16px}.info-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border)}.info-props{display:grid;grid-template-columns:auto 1fr;gap:4px 12px}.info-props dt{font-size:12px;color:var(--text-muted);padding-top:2px}.info-props dd{font-size:12px;color:var(--text);word-break:break-word;display:flex;align-items:center;gap:4px}.info-value{white-space:pre-wrap}.info-array{display:flex;flex-wrap:wrap;gap:4px}.info-tag{display:inline-block;padding:2px 8px;background:var(--bg-hover);border-radius:4px;font-size:11px;color:var(--text-muted)}.info-json{font-size:11px;font-family:monospace;color:var(--text-muted);background:var(--bg-inset);padding:6px 8px;border-radius:4px;overflow-x:auto;white-space:pre}.info-connections{list-style:none;display:flex;flex-direction:column;gap:6px}.info-connection{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--bg-elevated);border-radius:6px;font-size:12px;flex-wrap:wrap}.info-connection-active{outline:1.5px solid var(--accent);background:var(--bg-hover)}.info-target-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.info-arrow{color:var(--text-dim);font-size:14px;flex-shrink:0}.info-edge-type{color:var(--text-muted);font-size:11px;font-weight:500}.info-target{color:var(--text);font-weight:500}.info-edge-props{width:100%;padding-top:4px;padding-left:20px}.info-edge-prop{display:block;font-size:11px;color:var(--text-dim)}.info-editable{cursor:default;position:relative}.info-inline-edit{background:none;border:none;color:var(--badge-text);opacity:0;font-size:10px;cursor:pointer;margin-left:4px;transition:opacity .15s}.info-editable:hover .info-inline-edit{opacity:.8}.info-inline-edit:hover{opacity:1!important}.info-edit-inline-input{background:transparent;border:none;border-bottom:1px solid var(--accent);color:var(--badge-text);font:inherit;font-size:inherit;outline:none;width:100%;padding:0}.info-edit-input{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:3px 6px;font-size:12px;font-family:inherit;color:var(--text);flex:1;min-width:0;resize:vertical;overflow:hidden;line-height:1.4;max-height:300px}.info-edit-input:focus{outline:none;border-color:var(--accent)}.info-delete-prop{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;padding:0 2px;flex-shrink:0;opacity:0;transition:opacity .1s,color .1s}.info-props dd:hover .info-delete-prop{opacity:1}.info-delete-prop:hover{color:#ef4444}.info-add-btn{background:none;border:1px dashed var(--border);border-radius:4px;padding:6px 10px;font-size:12px;color:var(--text-dim);cursor:pointer;width:100%;margin-top:8px;transition:border-color .15s,color .15s}.info-add-btn:hover{border-color:var(--accent);color:var(--text)}.info-add-row{display:flex;gap:4px;margin-top:6px}.info-add-save{background:var(--accent);border:none;border-radius:4px;padding:3px 10px;font-size:12px;color:var(--badge-text);cursor:pointer;flex-shrink:0}.info-add-save:hover{background:var(--accent-hover)}.info-delete-edge{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;margin-left:auto;padding:0 2px;opacity:0;transition:opacity .1s,color .1s}.info-connection:hover .info-delete-edge{opacity:1}.info-delete-edge:hover{color:#ef4444}.info-danger{margin-top:8px;padding-top:12px;border-top:1px solid var(--border)}.info-delete-node{background:none;border:1px solid rgba(239,68,68,.3);border-radius:6px;padding:6px 12px;font-size:12px;color:#ef4444;cursor:pointer;width:100%;transition:background .15s}.info-delete-node:hover{background:#ef44441a}.tools-pane-toggle{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;color:var(--text-muted);font-size:18px;cursor:pointer;padding:6px 10px;line-height:1;transition:color .15s,border-color .15s,background .15s;box-shadow:0 2px 8px var(--shadow)}.tools-pane-toggle.hidden{display:none}.tools-pane-toggle:hover{color:var(--text);border-color:var(--text-muted);background:var(--bg-hover)}.tools-pane-toggle.active{color:var(--accent);border-color:#d4a27f66}.tools-pane-content{position:absolute;top:56px;left:16px;bottom:16px;z-index:20;width:var(--tools-width, 264px);box-sizing:border-box;overflow:hidden;display:flex;flex-direction:column;background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:12px;box-shadow:0 8px 32px var(--shadow)}.tools-pane-content.hidden{display:none}.tools-pane-section{margin-bottom:12px}.tools-pane-section:last-child{margin-bottom:0}.tools-pane-heading{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);margin-bottom:6px}.tools-pane-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:12px}.tools-pane-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.tools-pane-name{flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tools-pane-count{color:var(--text-dim);font-size:11px;flex-shrink:0}.tools-pane-summary{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-muted);padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid var(--border)}.tools-pane-sep{color:var(--text-dim)}.tools-pane-clickable{cursor:pointer;border-radius:4px;padding:3px 4px;margin:0 -4px;transition:background .1s}.tools-pane-clickable:hover{background:var(--bg-hover)}.tools-pane-clickable.active{background:var(--bg-hover);outline:1px solid var(--border)}.tools-pane-badge{font-size:9px;color:var(--accent);flex-shrink:0;opacity:.8}.tools-pane-issue .tools-pane-name{color:var(--text-muted)}.tools-pane-more{font-size:10px;color:var(--text-dim);padding:4px 0 0}.tools-pane-edit{background:none;border:none;color:var(--text-dim);font-size:11px;cursor:pointer;padding:0 2px;opacity:0;transition:opacity .1s,color .1s;flex-shrink:0}.tools-pane-row:hover .tools-pane-edit{opacity:1}.tools-pane-edit:hover{color:var(--accent)}.tools-pane-focus-toggle{opacity:.4;font-size:11px}.tools-pane-focus-active{opacity:1!important;color:var(--accent)!important}.tools-pane-focus-clear{margin-top:4px;border-top:1px solid var(--border);padding-top:6px}.tools-pane-editing{background:none!important}.tools-pane-inline-input{width:100%;background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:12px;padding:2px 6px;outline:none}.tools-pane-slider-row{display:flex;align-items:center;gap:6px;padding:4px 0}.tools-pane-slider-label{font-size:11px;color:var(--text-muted);white-space:nowrap;min-width:56px}.tools-pane-slider{flex:1;min-width:0;height:4px;accent-color:var(--accent);cursor:pointer}.tools-pane-slider-value{font-size:10px;color:var(--text-dim);min-width:28px;text-align:right;font-family:monospace}.tools-pane-checkbox{width:14px;height:14px;accent-color:var(--accent);cursor:pointer;flex-shrink:0}.tools-pane-export-row{display:flex;gap:4px;margin-top:6px}.tools-pane-export-btn{flex:1;padding:4px 8px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s}.tools-pane-export-btn:hover{color:var(--text);border-color:var(--text-muted)}.tools-pane-empty{font-size:11px;color:var(--text-dim);text-align:center;padding:8px 0}.tools-pane-tabs{display:flex;gap:2px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}.tools-pane-tab{flex:1;padding:4px 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;background:none;border:1px solid transparent;border-radius:5px;color:var(--text-dim);cursor:pointer;transition:color .15s,background .15s,border-color .15s}.tools-pane-tab:hover{color:var(--text-muted);background:var(--bg-hover)}.tools-pane-tab-active{color:var(--text);background:var(--bg-hover);border-color:var(--border)}.tools-pane-tab-content{flex:1;overflow-y:auto;overflow-x:hidden;min-height:0}.tools-pane-search{width:100%;padding:4px 8px;font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);outline:none;margin-bottom:8px;box-sizing:border-box}.tools-pane-search:focus{border-color:var(--accent)}.tools-pane-search::placeholder{color:var(--text-dim)}.tools-pane-empty-msg{font-size:11px;color:var(--text-dim);text-align:center;padding:16px 0}.empty-state{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;z-index:5;pointer-events:none}.empty-state.hidden{display:none}.empty-state-content{text-align:center;max-width:420px;padding:40px 24px}.empty-state-icon{color:var(--text-dim);margin-bottom:16px}.empty-state-title{font-size:18px;font-weight:600;color:var(--text);margin-bottom:8px}.empty-state-desc{font-size:13px;color:var(--text-muted);line-height:1.5;margin-bottom:20px}.empty-state-setup{background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:16px}.empty-state-label{font-size:11px;color:var(--text-dim);margin-bottom:8px}.empty-state-code{display:block;font-size:12px;color:var(--accent);font-family:monospace;word-break:break-all}.empty-state-hint{font-size:11px;color:var(--text-dim)}.empty-state-hint kbd{padding:1px 5px;border:1px solid var(--border);border-radius:3px;background:var(--bg-surface);font-family:monospace;font-size:11px}.shortcuts-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.shortcuts-overlay.hidden{display:none}.shortcuts-modal{background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;padding:24px;min-width:300px;max-width:400px;box-shadow:0 16px 48px var(--shadow);position:relative}.shortcuts-close{position:absolute;top:12px;right:14px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;padding:0 4px;line-height:1}.shortcuts-close:hover{color:var(--text)}.shortcuts-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:16px}.shortcuts-list{display:flex;flex-direction:column;gap:8px}.shortcuts-row{display:flex;align-items:center;justify-content:space-between;gap:12px}.shortcuts-keys{display:flex;align-items:center;gap:4px}.shortcuts-keys kbd{padding:2px 7px;border:1px solid var(--border);border-radius:4px;background:var(--bg);color:var(--text);font-size:11px;font-family:monospace}.shortcuts-or{font-size:10px;color:var(--text-dim)}.shortcuts-desc{font-size:12px;color:var(--text-muted)}@media(max-width:768px){#app{flex-direction:column}#sidebar{width:100%;min-width:0;max-height:35vh;border-right:none;border-bottom:1px solid var(--border)}.info-panel{top:auto;bottom:72px;right:8px;left:8px;width:auto;max-height:calc(100% - 200px);overflow-y:auto}.info-panel.info-panel-maximized{bottom:0;left:0;right:0}.canvas-top-bar{top:8px;left:8px;right:8px}.tools-pane-content{top:48px;left:8px;bottom:80px;width:160px;max-width:calc(100vw - 24px)}.tools-pane-edit{opacity:.6}}.bp-dialog-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.bp-dialog{background:var(--bg-surface);border:1px solid var(--border);border-radius:12px;padding:20px;min-width:280px;max-width:400px;box-shadow:0 16px 48px #0000004d}.bp-dialog-title{font-size:14px;font-weight:600;color:var(--text);margin-bottom:12px}.bp-dialog-message{font-size:13px;color:var(--text-muted);margin-bottom:16px;line-height:1.5}.bp-dialog-input{width:100%;padding:8px 12px;font-size:13px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);outline:none;margin-bottom:16px}.bp-dialog-input:focus{border-color:var(--accent)}.bp-dialog-buttons{display:flex;justify-content:flex-end;gap:8px}.bp-dialog-btn{padding:6px 16px;font-size:12px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text-muted);cursor:pointer;transition:all .15s}.bp-dialog-btn:hover{background:var(--bg-hover);color:var(--text)}.bp-dialog-btn-accent{background:var(--accent);color:#fff;border-color:var(--accent)}.bp-dialog-btn-accent:hover{opacity:.9;color:#fff;background:var(--accent)}.bp-dialog-btn-danger{background:#e55;color:#fff;border-color:#e55}.bp-dialog-btn-danger:hover{opacity:.9;color:#fff;background:#e55}.bp-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%) translateY(20px);background:var(--bg-surface);border:1px solid var(--border);color:var(--text);padding:8px 20px;border-radius:8px;font-size:12px;z-index:1001;opacity:0;transition:opacity .3s,transform .3s;box-shadow:0 4px 16px var(--shadow)}.bp-toast-visible{opacity:1;transform:translate(-50%) translateY(0)}
|
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-CTM-vKgB.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CjzMJjZ-.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app">
|
package/dist/canvas.d.ts
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
import type { LearningGraphData } from "backpack-ontology";
|
|
2
|
+
export interface CanvasConfig {
|
|
3
|
+
lod?: {
|
|
4
|
+
hideBadges?: number;
|
|
5
|
+
hideLabels?: number;
|
|
6
|
+
hideEdgeLabels?: number;
|
|
7
|
+
smallNodes?: number;
|
|
8
|
+
hideArrows?: number;
|
|
9
|
+
};
|
|
10
|
+
navigation?: {
|
|
11
|
+
zoomFactor?: number;
|
|
12
|
+
zoomMin?: number;
|
|
13
|
+
zoomMax?: number;
|
|
14
|
+
panAnimationMs?: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
2
17
|
export interface FocusInfo {
|
|
3
18
|
seedNodeIds: string[];
|
|
4
19
|
hops: number;
|
|
5
20
|
totalNodes: number;
|
|
6
21
|
}
|
|
7
|
-
export declare function initCanvas(container: HTMLElement, onNodeClick?: (nodeIds: string[] | null) => void, onFocusChange?: (focus: FocusInfo | null) => void): {
|
|
22
|
+
export declare function initCanvas(container: HTMLElement, onNodeClick?: (nodeIds: string[] | null) => void, onFocusChange?: (focus: FocusInfo | null) => void, config?: CanvasConfig): {
|
|
8
23
|
loadGraph(data: LearningGraphData): void;
|
|
9
24
|
setFilteredNodeIds(ids: Set<string> | null): void;
|
|
10
25
|
panToNode(nodeId: string): void;
|
|
11
26
|
panToNodes(nodeIds: string[]): void;
|
|
27
|
+
setEdges(visible: boolean): void;
|
|
12
28
|
setEdgeLabels(visible: boolean): void;
|
|
13
29
|
setTypeHulls(visible: boolean): void;
|
|
14
30
|
setMinimap(visible: boolean): void;
|
|
31
|
+
centerView(): void;
|
|
32
|
+
panBy(dx: number, dy: number): void;
|
|
33
|
+
zoomBy(factor: number): void;
|
|
15
34
|
reheat(): void;
|
|
16
35
|
exportImage(format: "png" | "svg"): string;
|
|
17
36
|
enterFocus(seedNodeIds: string[], hops: number): void;
|
|
18
37
|
exitFocus(): void;
|
|
19
38
|
isFocused(): boolean;
|
|
20
39
|
getFocusInfo(): FocusInfo | null;
|
|
40
|
+
/** Get all node IDs in the current layout (subgraph if focused, full graph otherwise). Seed nodes first. */
|
|
41
|
+
getNodeIds(): string[];
|
|
21
42
|
destroy(): void;
|
|
22
43
|
};
|
package/dist/canvas.js
CHANGED
|
@@ -6,7 +6,18 @@ function cssVar(name) {
|
|
|
6
6
|
}
|
|
7
7
|
const NODE_RADIUS = 20;
|
|
8
8
|
const ALPHA_MIN = 0.001;
|
|
9
|
-
|
|
9
|
+
// Defaults — overridden per-instance via config
|
|
10
|
+
const LOD_DEFAULTS = { hideBadges: 0.4, hideLabels: 0.25, hideEdgeLabels: 0.35, smallNodes: 0.2, hideArrows: 0.15 };
|
|
11
|
+
const NAV_DEFAULTS = { zoomFactor: 1.3, zoomMin: 0.05, zoomMax: 10, panAnimationMs: 300 };
|
|
12
|
+
/** Check if a point is within the visible viewport (with padding). */
|
|
13
|
+
function isInViewport(x, y, camera, canvasW, canvasH, pad = 100) {
|
|
14
|
+
const sx = (x - camera.x) * camera.scale;
|
|
15
|
+
const sy = (y - camera.y) * camera.scale;
|
|
16
|
+
return sx >= -pad && sx <= canvasW + pad && sy >= -pad && sy <= canvasH + pad;
|
|
17
|
+
}
|
|
18
|
+
export function initCanvas(container, onNodeClick, onFocusChange, config) {
|
|
19
|
+
const lod = { ...LOD_DEFAULTS, ...(config?.lod ?? {}) };
|
|
20
|
+
const nav = { ...NAV_DEFAULTS, ...(config?.navigation ?? {}) };
|
|
10
21
|
const canvas = container.querySelector("canvas");
|
|
11
22
|
const ctx = canvas.getContext("2d");
|
|
12
23
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -16,6 +27,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
16
27
|
let animFrame = 0;
|
|
17
28
|
let selectedNodeIds = new Set();
|
|
18
29
|
let filteredNodeIds = null; // null = no filter (show all)
|
|
30
|
+
let showEdges = true;
|
|
19
31
|
let showEdgeLabels = true;
|
|
20
32
|
let showTypeHulls = true;
|
|
21
33
|
let showMinimap = true;
|
|
@@ -28,7 +40,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
28
40
|
// Pan animation state
|
|
29
41
|
let panTarget = null;
|
|
30
42
|
let panStart = null;
|
|
31
|
-
const PAN_DURATION =
|
|
43
|
+
const PAN_DURATION = nav.panAnimationMs;
|
|
32
44
|
// --- Sizing ---
|
|
33
45
|
function resize() {
|
|
34
46
|
canvas.width = canvas.clientWidth * dpr;
|
|
@@ -89,7 +101,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
89
101
|
ctx.translate(-camera.x * camera.scale, -camera.y * camera.scale);
|
|
90
102
|
ctx.scale(camera.scale, camera.scale);
|
|
91
103
|
// Draw type hulls (shaded regions behind same-type nodes)
|
|
92
|
-
if (showTypeHulls) {
|
|
104
|
+
if (showTypeHulls && camera.scale >= lod.smallNodes) {
|
|
93
105
|
const typeGroups = new Map();
|
|
94
106
|
for (const node of state.nodes) {
|
|
95
107
|
if (filteredNodeIds !== null && !filteredNodeIds.has(node.id))
|
|
@@ -134,56 +146,66 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
134
146
|
}
|
|
135
147
|
}
|
|
136
148
|
// Draw edges
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Arrowhead
|
|
169
|
-
drawArrowhead(source.x, source.y, target.x, target.y, highlighted, arrowColor, arrowHighlight);
|
|
170
|
-
// Edge label at midpoint
|
|
171
|
-
if (showEdgeLabels) {
|
|
172
|
-
const mx = (source.x + target.x) / 2;
|
|
173
|
-
const my = (source.y + target.y) / 2;
|
|
174
|
-
ctx.fillStyle = highlighted
|
|
175
|
-
? edgeLabelHighlight
|
|
149
|
+
if (showEdges)
|
|
150
|
+
for (const edge of state.edges) {
|
|
151
|
+
const source = state.nodeMap.get(edge.sourceId);
|
|
152
|
+
const target = state.nodeMap.get(edge.targetId);
|
|
153
|
+
if (!source || !target)
|
|
154
|
+
continue;
|
|
155
|
+
// Viewport culling — skip if both endpoints are off-screen
|
|
156
|
+
if (!isInViewport(source.x, source.y, camera, canvas.clientWidth, canvas.clientHeight, 200) &&
|
|
157
|
+
!isInViewport(target.x, target.y, camera, canvas.clientWidth, canvas.clientHeight, 200))
|
|
158
|
+
continue;
|
|
159
|
+
const sourceMatch = filteredNodeIds === null || filteredNodeIds.has(edge.sourceId);
|
|
160
|
+
const targetMatch = filteredNodeIds === null || filteredNodeIds.has(edge.targetId);
|
|
161
|
+
const bothMatch = sourceMatch && targetMatch;
|
|
162
|
+
// Hide edges where neither endpoint matches the filter
|
|
163
|
+
if (filteredNodeIds !== null && !sourceMatch && !targetMatch)
|
|
164
|
+
continue;
|
|
165
|
+
const isConnected = selectedNodeIds.size > 0 &&
|
|
166
|
+
(selectedNodeIds.has(edge.sourceId) || selectedNodeIds.has(edge.targetId));
|
|
167
|
+
const highlighted = isConnected || (filteredNodeIds !== null && bothMatch);
|
|
168
|
+
const edgeDimmed = filteredNodeIds !== null && !bothMatch;
|
|
169
|
+
// Self-loop
|
|
170
|
+
if (edge.sourceId === edge.targetId) {
|
|
171
|
+
drawSelfLoop(source, edge.type, highlighted, edgeColor, edgeHighlight, edgeLabel, edgeLabelHighlight);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Line
|
|
175
|
+
ctx.beginPath();
|
|
176
|
+
ctx.moveTo(source.x, source.y);
|
|
177
|
+
ctx.lineTo(target.x, target.y);
|
|
178
|
+
ctx.strokeStyle = highlighted
|
|
179
|
+
? edgeHighlight
|
|
176
180
|
: edgeDimmed
|
|
177
|
-
?
|
|
178
|
-
:
|
|
179
|
-
ctx.
|
|
180
|
-
ctx.
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
? edgeDimColor
|
|
182
|
+
: edgeColor;
|
|
183
|
+
ctx.lineWidth = camera.scale < lod.hideArrows ? 1 : highlighted ? 2.5 : 1.5;
|
|
184
|
+
ctx.stroke();
|
|
185
|
+
// Arrowhead
|
|
186
|
+
if (camera.scale >= lod.hideArrows) {
|
|
187
|
+
drawArrowhead(source.x, source.y, target.x, target.y, highlighted, arrowColor, arrowHighlight);
|
|
188
|
+
}
|
|
189
|
+
// Edge label at midpoint
|
|
190
|
+
if (showEdgeLabels && camera.scale >= lod.hideEdgeLabels) {
|
|
191
|
+
const mx = (source.x + target.x) / 2;
|
|
192
|
+
const my = (source.y + target.y) / 2;
|
|
193
|
+
ctx.fillStyle = highlighted
|
|
194
|
+
? edgeLabelHighlight
|
|
195
|
+
: edgeDimmed
|
|
196
|
+
? edgeLabelDim
|
|
197
|
+
: edgeLabel;
|
|
198
|
+
ctx.font = "9px system-ui, sans-serif";
|
|
199
|
+
ctx.textAlign = "center";
|
|
200
|
+
ctx.textBaseline = "bottom";
|
|
201
|
+
ctx.fillText(edge.type, mx, my - 4);
|
|
202
|
+
}
|
|
183
203
|
}
|
|
184
|
-
}
|
|
185
204
|
// Draw nodes
|
|
186
205
|
for (const node of state.nodes) {
|
|
206
|
+
// Viewport culling
|
|
207
|
+
if (!isInViewport(node.x, node.y, camera, canvas.clientWidth, canvas.clientHeight))
|
|
208
|
+
continue;
|
|
187
209
|
const color = getColor(node.type);
|
|
188
210
|
const isSelected = selectedNodeIds.has(node.id);
|
|
189
211
|
const isNeighbor = selectedNodeIds.size > 0 &&
|
|
@@ -192,13 +214,14 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
192
214
|
const filteredOut = filteredNodeIds !== null && !filteredNodeIds.has(node.id);
|
|
193
215
|
const dimmed = filteredOut ||
|
|
194
216
|
(selectedNodeIds.size > 0 && !isSelected && !isNeighbor);
|
|
217
|
+
const r = camera.scale < lod.smallNodes ? NODE_RADIUS * 0.5 : NODE_RADIUS;
|
|
195
218
|
// Glow for selected node
|
|
196
219
|
if (isSelected) {
|
|
197
220
|
ctx.save();
|
|
198
221
|
ctx.shadowColor = color;
|
|
199
222
|
ctx.shadowBlur = 20;
|
|
200
223
|
ctx.beginPath();
|
|
201
|
-
ctx.arc(node.x, node.y,
|
|
224
|
+
ctx.arc(node.x, node.y, r + 3, 0, Math.PI * 2);
|
|
202
225
|
ctx.fillStyle = color;
|
|
203
226
|
ctx.globalAlpha = 0.3;
|
|
204
227
|
ctx.fill();
|
|
@@ -206,7 +229,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
206
229
|
}
|
|
207
230
|
// Circle
|
|
208
231
|
ctx.beginPath();
|
|
209
|
-
ctx.arc(node.x, node.y,
|
|
232
|
+
ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
|
|
210
233
|
ctx.fillStyle = color;
|
|
211
234
|
ctx.globalAlpha = filteredOut ? 0.1 : dimmed ? 0.3 : 1;
|
|
212
235
|
ctx.fill();
|
|
@@ -214,17 +237,21 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
214
237
|
ctx.lineWidth = isSelected ? 3 : 1.5;
|
|
215
238
|
ctx.stroke();
|
|
216
239
|
// Label below
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
if (camera.scale >= lod.hideLabels) {
|
|
241
|
+
const label = node.label.length > 24 ? node.label.slice(0, 22) + "..." : node.label;
|
|
242
|
+
ctx.fillStyle = dimmed ? nodeLabelDim : nodeLabel;
|
|
243
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
244
|
+
ctx.textAlign = "center";
|
|
245
|
+
ctx.textBaseline = "top";
|
|
246
|
+
ctx.fillText(label, node.x, node.y + r + 4);
|
|
247
|
+
}
|
|
223
248
|
// Type badge above
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
if (camera.scale >= lod.hideBadges) {
|
|
250
|
+
ctx.fillStyle = dimmed ? typeBadgeDim : typeBadge;
|
|
251
|
+
ctx.font = "9px system-ui, sans-serif";
|
|
252
|
+
ctx.textBaseline = "bottom";
|
|
253
|
+
ctx.fillText(node.type, node.x, node.y - r - 3);
|
|
254
|
+
}
|
|
228
255
|
ctx.globalAlpha = 1;
|
|
229
256
|
}
|
|
230
257
|
ctx.restore();
|
|
@@ -443,7 +470,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
443
470
|
: e.deltaY > 0
|
|
444
471
|
? 0.9
|
|
445
472
|
: 1.1;
|
|
446
|
-
camera.scale = Math.max(
|
|
473
|
+
camera.scale = Math.max(nav.zoomMin, Math.min(nav.zoomMax, camera.scale * factor));
|
|
447
474
|
camera.x = wx - mx / camera.scale;
|
|
448
475
|
camera.y = wy - my / camera.scale;
|
|
449
476
|
render();
|
|
@@ -476,7 +503,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
476
503
|
if (current.length === 2 && touches.length === 2) {
|
|
477
504
|
const dist = touchDistance(current[0], current[1]);
|
|
478
505
|
const ratio = dist / initialPinchDist;
|
|
479
|
-
camera.scale = Math.max(
|
|
506
|
+
camera.scale = Math.max(nav.zoomMin, Math.min(nav.zoomMax, initialPinchScale * ratio));
|
|
480
507
|
render();
|
|
481
508
|
}
|
|
482
509
|
else if (current.length === 1) {
|
|
@@ -539,7 +566,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
539
566
|
const cx = canvas.clientWidth / 2;
|
|
540
567
|
const cy = canvas.clientHeight / 2;
|
|
541
568
|
const [wx, wy] = screenToWorld(cx, cy);
|
|
542
|
-
camera.scale = Math.min(
|
|
569
|
+
camera.scale = Math.min(nav.zoomMax, camera.scale * nav.zoomFactor);
|
|
543
570
|
camera.x = wx - cx / camera.scale;
|
|
544
571
|
camera.y = wy - cy / camera.scale;
|
|
545
572
|
render();
|
|
@@ -552,7 +579,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
552
579
|
const cx = canvas.clientWidth / 2;
|
|
553
580
|
const cy = canvas.clientHeight / 2;
|
|
554
581
|
const [wx, wy] = screenToWorld(cx, cy);
|
|
555
|
-
camera.scale = Math.max(
|
|
582
|
+
camera.scale = Math.max(nav.zoomMin, camera.scale / nav.zoomFactor);
|
|
556
583
|
camera.x = wx - cx / camera.scale;
|
|
557
584
|
camera.y = wy - cy / camera.scale;
|
|
558
585
|
render();
|
|
@@ -676,6 +703,10 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
676
703
|
}
|
|
677
704
|
animatePan();
|
|
678
705
|
},
|
|
706
|
+
setEdges(visible) {
|
|
707
|
+
showEdges = visible;
|
|
708
|
+
render();
|
|
709
|
+
},
|
|
679
710
|
setEdgeLabels(visible) {
|
|
680
711
|
showEdgeLabels = visible;
|
|
681
712
|
render();
|
|
@@ -688,6 +719,41 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
688
719
|
showMinimap = visible;
|
|
689
720
|
render();
|
|
690
721
|
},
|
|
722
|
+
centerView() {
|
|
723
|
+
if (!state)
|
|
724
|
+
return;
|
|
725
|
+
camera = { x: 0, y: 0, scale: 1 };
|
|
726
|
+
if (state.nodes.length > 0) {
|
|
727
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
728
|
+
for (const n of state.nodes) {
|
|
729
|
+
if (n.x < minX)
|
|
730
|
+
minX = n.x;
|
|
731
|
+
if (n.y < minY)
|
|
732
|
+
minY = n.y;
|
|
733
|
+
if (n.x > maxX)
|
|
734
|
+
maxX = n.x;
|
|
735
|
+
if (n.y > maxY)
|
|
736
|
+
maxY = n.y;
|
|
737
|
+
}
|
|
738
|
+
camera.x = (minX + maxX) / 2 - canvas.clientWidth / 2;
|
|
739
|
+
camera.y = (minY + maxY) / 2 - canvas.clientHeight / 2;
|
|
740
|
+
}
|
|
741
|
+
render();
|
|
742
|
+
},
|
|
743
|
+
panBy(dx, dy) {
|
|
744
|
+
camera.x += dx / camera.scale;
|
|
745
|
+
camera.y += dy / camera.scale;
|
|
746
|
+
render();
|
|
747
|
+
},
|
|
748
|
+
zoomBy(factor) {
|
|
749
|
+
const cx = canvas.clientWidth / 2;
|
|
750
|
+
const cy = canvas.clientHeight / 2;
|
|
751
|
+
const [wx, wy] = screenToWorld(cx, cy);
|
|
752
|
+
camera.scale = Math.max(nav.zoomMin, Math.min(nav.zoomMax, camera.scale * factor));
|
|
753
|
+
camera.x = wx - cx / camera.scale;
|
|
754
|
+
camera.y = wy - cy / camera.scale;
|
|
755
|
+
render();
|
|
756
|
+
},
|
|
691
757
|
reheat() {
|
|
692
758
|
alpha = 0.5;
|
|
693
759
|
cancelAnimationFrame(animFrame);
|
|
@@ -790,6 +856,18 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
790
856
|
totalNodes: state.nodes.length,
|
|
791
857
|
};
|
|
792
858
|
},
|
|
859
|
+
/** Get all node IDs in the current layout (subgraph if focused, full graph otherwise). Seed nodes first. */
|
|
860
|
+
getNodeIds() {
|
|
861
|
+
if (!state)
|
|
862
|
+
return [];
|
|
863
|
+
if (focusSeedIds) {
|
|
864
|
+
const seedSet = new Set(focusSeedIds);
|
|
865
|
+
const seeds = state.nodes.filter((n) => seedSet.has(n.id)).map((n) => n.id);
|
|
866
|
+
const rest = state.nodes.filter((n) => !seedSet.has(n.id)).map((n) => n.id);
|
|
867
|
+
return [...seeds, ...rest];
|
|
868
|
+
}
|
|
869
|
+
return state.nodes.map((n) => n.id);
|
|
870
|
+
},
|
|
793
871
|
destroy() {
|
|
794
872
|
cancelAnimationFrame(animFrame);
|
|
795
873
|
observer.disconnect();
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import defaultConfig from "./default-config.json" with { type: "json" };
|
|
5
|
+
function viewerConfigDir() {
|
|
6
|
+
if (process.env.BACKPACK_DIR) {
|
|
7
|
+
return path.join(process.env.BACKPACK_DIR, "config");
|
|
8
|
+
}
|
|
9
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
10
|
+
return path.join(xdgConfig, "backpack");
|
|
11
|
+
}
|
|
12
|
+
function viewerConfigFile() {
|
|
13
|
+
return path.join(viewerConfigDir(), "viewer.json");
|
|
14
|
+
}
|
|
15
|
+
export function loadViewerConfig() {
|
|
16
|
+
const filePath = viewerConfigFile();
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
19
|
+
const user = JSON.parse(raw);
|
|
20
|
+
return {
|
|
21
|
+
keybindings: { ...defaultConfig.keybindings, ...(user.keybindings ?? {}) },
|
|
22
|
+
display: { ...defaultConfig.display, ...(user.display ?? {}) },
|
|
23
|
+
layout: { ...defaultConfig.layout, ...(user.layout ?? {}) },
|
|
24
|
+
navigation: { ...defaultConfig.navigation, ...(user.navigation ?? {}) },
|
|
25
|
+
lod: { ...defaultConfig.lod, ...(user.lod ?? {}) },
|
|
26
|
+
limits: { ...defaultConfig.limits, ...(user.limits ?? {}) },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return defaultConfig;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keybindings": {
|
|
3
|
+
"search": "/",
|
|
4
|
+
"searchAlt": "ctrl+k",
|
|
5
|
+
"undo": "ctrl+z",
|
|
6
|
+
"redo": "ctrl+shift+z",
|
|
7
|
+
"help": "?",
|
|
8
|
+
"escape": "Escape",
|
|
9
|
+
"focus": "f",
|
|
10
|
+
"toggleEdges": "e",
|
|
11
|
+
"center": "c",
|
|
12
|
+
"nextNode": ".",
|
|
13
|
+
"prevNode": ",",
|
|
14
|
+
"nextConnection": ">",
|
|
15
|
+
"prevConnection": "<",
|
|
16
|
+
"historyBack": "(",
|
|
17
|
+
"historyForward": ")",
|
|
18
|
+
"hopsIncrease": "=",
|
|
19
|
+
"hopsDecrease": "-",
|
|
20
|
+
"panLeft": "h",
|
|
21
|
+
"panDown": "j",
|
|
22
|
+
"panUp": "k",
|
|
23
|
+
"panRight": "l",
|
|
24
|
+
"panFastLeft": "H",
|
|
25
|
+
"zoomOut": "J",
|
|
26
|
+
"zoomIn": "K",
|
|
27
|
+
"panFastRight": "L",
|
|
28
|
+
"spacingDecrease": "[",
|
|
29
|
+
"spacingIncrease": "]",
|
|
30
|
+
"clusteringDecrease": "{",
|
|
31
|
+
"clusteringIncrease": "}",
|
|
32
|
+
"toggleSidebar": "Tab"
|
|
33
|
+
},
|
|
34
|
+
"display": {
|
|
35
|
+
"edges": true,
|
|
36
|
+
"edgeLabels": true,
|
|
37
|
+
"typeHulls": true,
|
|
38
|
+
"minimap": true,
|
|
39
|
+
"theme": "system"
|
|
40
|
+
},
|
|
41
|
+
"layout": {
|
|
42
|
+
"spacing": 1.5,
|
|
43
|
+
"clustering": 0.08
|
|
44
|
+
},
|
|
45
|
+
"navigation": {
|
|
46
|
+
"panSpeed": 60,
|
|
47
|
+
"panFastMultiplier": 3,
|
|
48
|
+
"zoomFactor": 1.3,
|
|
49
|
+
"zoomMin": 0.05,
|
|
50
|
+
"zoomMax": 10,
|
|
51
|
+
"panAnimationMs": 300
|
|
52
|
+
},
|
|
53
|
+
"lod": {
|
|
54
|
+
"hideBadges": 0.4,
|
|
55
|
+
"hideLabels": 0.25,
|
|
56
|
+
"hideEdgeLabels": 0.35,
|
|
57
|
+
"smallNodes": 0.2,
|
|
58
|
+
"hideArrows": 0.15
|
|
59
|
+
},
|
|
60
|
+
"limits": {
|
|
61
|
+
"maxSearchResults": 8,
|
|
62
|
+
"maxQualityItems": 5,
|
|
63
|
+
"maxMostConnected": 5,
|
|
64
|
+
"searchDebounceMs": 150
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Lightweight inline dialog system — replaces native alert/confirm/prompt. */
|
|
2
|
+
/** Show a confirmation dialog. Returns a promise that resolves to true/false. */
|
|
3
|
+
export declare function showConfirm(title: string, message: string): Promise<boolean>;
|
|
4
|
+
/** Show a prompt dialog with an input field. Returns null if cancelled. */
|
|
5
|
+
export declare function showPrompt(title: string, placeholder?: string, defaultValue?: string): Promise<string | null>;
|
|
6
|
+
/** Show a danger confirmation (for destructive actions). */
|
|
7
|
+
export declare function showDangerConfirm(title: string, message: string): Promise<boolean>;
|
|
8
|
+
/** Show a brief toast notification. */
|
|
9
|
+
export declare function showToast(message: string, durationMs?: number): void;
|