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.
@@ -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)}
@@ -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-Mi0vDG5K.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-z15vEFEy.css">
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
- export function initCanvas(container, onNodeClick, onFocusChange) {
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 = 300;
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
- for (const edge of state.edges) {
138
- const source = state.nodeMap.get(edge.sourceId);
139
- const target = state.nodeMap.get(edge.targetId);
140
- if (!source || !target)
141
- continue;
142
- const sourceMatch = filteredNodeIds === null || filteredNodeIds.has(edge.sourceId);
143
- const targetMatch = filteredNodeIds === null || filteredNodeIds.has(edge.targetId);
144
- const bothMatch = sourceMatch && targetMatch;
145
- // Hide edges where neither endpoint matches the filter
146
- if (filteredNodeIds !== null && !sourceMatch && !targetMatch)
147
- continue;
148
- const isConnected = selectedNodeIds.size > 0 &&
149
- (selectedNodeIds.has(edge.sourceId) || selectedNodeIds.has(edge.targetId));
150
- const highlighted = isConnected || (filteredNodeIds !== null && bothMatch);
151
- const edgeDimmed = filteredNodeIds !== null && !bothMatch;
152
- // Self-loop
153
- if (edge.sourceId === edge.targetId) {
154
- drawSelfLoop(source, edge.type, highlighted, edgeColor, edgeHighlight, edgeLabel, edgeLabelHighlight);
155
- continue;
156
- }
157
- // Line
158
- ctx.beginPath();
159
- ctx.moveTo(source.x, source.y);
160
- ctx.lineTo(target.x, target.y);
161
- ctx.strokeStyle = highlighted
162
- ? edgeHighlight
163
- : edgeDimmed
164
- ? edgeDimColor
165
- : edgeColor;
166
- ctx.lineWidth = highlighted ? 2.5 : 1.5;
167
- ctx.stroke();
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
- ? edgeLabelDim
178
- : edgeLabel;
179
- ctx.font = "9px system-ui, sans-serif";
180
- ctx.textAlign = "center";
181
- ctx.textBaseline = "bottom";
182
- ctx.fillText(edge.type, mx, my - 4);
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, NODE_RADIUS + 3, 0, Math.PI * 2);
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, NODE_RADIUS, 0, Math.PI * 2);
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
- const label = node.label.length > 24 ? node.label.slice(0, 22) + "..." : node.label;
218
- ctx.fillStyle = dimmed ? nodeLabelDim : nodeLabel;
219
- ctx.font = "11px system-ui, sans-serif";
220
- ctx.textAlign = "center";
221
- ctx.textBaseline = "top";
222
- ctx.fillText(label, node.x, node.y + NODE_RADIUS + 4);
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
- ctx.fillStyle = dimmed ? typeBadgeDim : typeBadge;
225
- ctx.font = "9px system-ui, sans-serif";
226
- ctx.textBaseline = "bottom";
227
- ctx.fillText(node.type, node.x, node.y - NODE_RADIUS - 3);
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(0.05, Math.min(10, camera.scale * factor));
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(0.05, Math.min(10, initialPinchScale * ratio));
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(10, camera.scale * 1.3);
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(0.05, camera.scale / 1.3);
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();
@@ -0,0 +1,4 @@
1
+ import defaultConfig from "./default-config.json";
2
+ export type ViewerConfig = typeof defaultConfig;
3
+ export type KeybindingMap = typeof defaultConfig.keybindings;
4
+ export declare function loadViewerConfig(): ViewerConfig;
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
+ }
@@ -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;