backpack-viewer 0.5.1 → 0.7.0
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/bin/serve.js +159 -396
- package/dist/app/assets/index-D-H7agBH.js +12 -0
- package/dist/app/assets/index-DE73ngo-.css +1 -0
- package/dist/app/assets/index-DFW3OKgJ.js +6 -0
- package/dist/app/assets/layout-worker-4xak23M6.js +1 -0
- package/dist/app/index.html +2 -2
- package/dist/bridge.d.ts +22 -0
- package/dist/bridge.js +41 -0
- package/dist/canvas.d.ts +15 -0
- package/dist/canvas.js +352 -12
- package/dist/config.js +10 -0
- package/dist/copy-prompt.d.ts +17 -0
- package/dist/copy-prompt.js +81 -0
- package/dist/default-config.json +6 -1
- package/dist/dom-utils.d.ts +46 -0
- package/dist/dom-utils.js +57 -0
- package/dist/empty-state.js +63 -31
- package/dist/extensions/api.d.ts +15 -0
- package/dist/extensions/api.js +185 -0
- package/dist/extensions/chat/backpack-extension.json +23 -0
- package/dist/extensions/chat/src/index.js +32 -0
- package/dist/extensions/chat/src/panel.js +306 -0
- package/dist/extensions/chat/src/providers/anthropic.js +158 -0
- package/dist/extensions/chat/src/providers/types.js +15 -0
- package/dist/extensions/chat/src/tools.js +281 -0
- package/dist/extensions/chat/style.css +147 -0
- package/dist/extensions/event-bus.d.ts +12 -0
- package/dist/extensions/event-bus.js +30 -0
- package/dist/extensions/loader.d.ts +32 -0
- package/dist/extensions/loader.js +71 -0
- package/dist/extensions/manifest.d.ts +54 -0
- package/dist/extensions/manifest.js +116 -0
- package/dist/extensions/panel-mount.d.ts +26 -0
- package/dist/extensions/panel-mount.js +377 -0
- package/dist/extensions/taskbar.d.ts +29 -0
- package/dist/extensions/taskbar.js +64 -0
- package/dist/extensions/types.d.ts +182 -0
- package/dist/extensions/types.js +8 -0
- package/dist/info-panel.d.ts +2 -1
- package/dist/info-panel.js +78 -87
- package/dist/keybindings.d.ts +1 -1
- package/dist/keybindings.js +1 -0
- package/dist/layout-worker.d.ts +4 -1
- package/dist/layout-worker.js +51 -1
- package/dist/layout.d.ts +8 -0
- package/dist/layout.js +8 -1
- package/dist/main.js +216 -35
- package/dist/search.js +1 -1
- package/dist/server-api-routes.d.ts +56 -0
- package/dist/server-api-routes.js +442 -0
- package/dist/server-extensions.d.ts +126 -0
- package/dist/server-extensions.js +272 -0
- package/dist/server-viewer-state.d.ts +18 -0
- package/dist/server-viewer-state.js +33 -0
- package/dist/shortcuts.js +6 -2
- package/dist/sidebar.js +19 -7
- package/dist/style.css +356 -74
- package/dist/tools-pane.js +31 -14
- package/package.json +4 -3
- package/dist/app/assets/index-B3z5bBGl.css +0 -1
- package/dist/app/assets/index-CKYlU1zT.js +0 -35
- package/dist/app/assets/layout-worker-BZXiBoiC.js +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { LearningGraphData } from "backpack-ontology";
|
|
2
|
+
/**
|
|
3
|
+
* Versioned extension API contract.
|
|
4
|
+
*
|
|
5
|
+
* Bumping the major version means breaking changes for installed
|
|
6
|
+
* extensions — extensions declare which version they target in their
|
|
7
|
+
* manifest and the loader rejects mismatches.
|
|
8
|
+
*/
|
|
9
|
+
export declare const VIEWER_API_VERSION: "1";
|
|
10
|
+
export type ViewerApiVersion = "1";
|
|
11
|
+
/**
|
|
12
|
+
* Events an extension can subscribe to via `viewer.on(event, cb)`.
|
|
13
|
+
*
|
|
14
|
+
* - `graph-changed`: the active graph data was mutated (any source)
|
|
15
|
+
* - `graph-switched`: the user switched to a different active graph
|
|
16
|
+
* - `selection-changed`: the set of selected node ids changed
|
|
17
|
+
* - `focus-changed`: focus mode was entered, exited, or modified
|
|
18
|
+
*/
|
|
19
|
+
export type ViewerEvent = "graph-changed" | "graph-switched" | "selection-changed" | "focus-changed";
|
|
20
|
+
/** Snapshot of the viewer's current focus state. */
|
|
21
|
+
export interface ViewerFocusSnapshot {
|
|
22
|
+
seedNodeIds: string[];
|
|
23
|
+
hops: number;
|
|
24
|
+
totalNodes: number;
|
|
25
|
+
}
|
|
26
|
+
/** Public extension API surface. */
|
|
27
|
+
export interface ViewerExtensionAPI {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly viewerApiVersion: ViewerApiVersion;
|
|
30
|
+
getGraph(): LearningGraphData | null;
|
|
31
|
+
getGraphName(): string;
|
|
32
|
+
getSelection(): string[];
|
|
33
|
+
getFocus(): ViewerFocusSnapshot | null;
|
|
34
|
+
on(event: ViewerEvent, callback: () => void): () => void;
|
|
35
|
+
addNode(type: string, properties: Record<string, unknown>): Promise<string>;
|
|
36
|
+
updateNode(nodeId: string, properties: Record<string, unknown>): Promise<void>;
|
|
37
|
+
removeNode(nodeId: string): Promise<void>;
|
|
38
|
+
addEdge(sourceId: string, targetId: string, type: string): Promise<string>;
|
|
39
|
+
removeEdge(edgeId: string): Promise<void>;
|
|
40
|
+
panToNode(nodeId: string): void;
|
|
41
|
+
focusNodes(nodeIds: string[], hops: number): void;
|
|
42
|
+
exitFocus(): void;
|
|
43
|
+
registerTaskbarIcon(opts: TaskbarIconOptions): () => void;
|
|
44
|
+
mountPanel(element: HTMLElement, opts?: MountPanelOptions): MountedPanel;
|
|
45
|
+
settings: ExtensionSettingsAPI;
|
|
46
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
47
|
+
}
|
|
48
|
+
export type TaskbarPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
49
|
+
export interface TaskbarIconOptions {
|
|
50
|
+
/** Visible button text and accessible label */
|
|
51
|
+
label: string;
|
|
52
|
+
/** Optional leading symbol shown before the label (no SVG in v1) */
|
|
53
|
+
iconText?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Where to place the icon. Top slots nest into the viewer's existing
|
|
56
|
+
* top bar (alongside zoom/theme controls); bottom slots float in the
|
|
57
|
+
* canvas corners. Default: "bottom-right".
|
|
58
|
+
*
|
|
59
|
+
* The bottom-center area is reserved for the viewer's path bar — no
|
|
60
|
+
* slot lives there to avoid overlap.
|
|
61
|
+
*/
|
|
62
|
+
position?: TaskbarPosition;
|
|
63
|
+
/** Click handler — usually toggles a panel */
|
|
64
|
+
onClick: () => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A custom button rendered in a panel's header, to the left of the
|
|
68
|
+
* built-in fullscreen + close controls. Each button shares the same
|
|
69
|
+
* visual style as the built-ins so extension chrome reads as one row
|
|
70
|
+
* of controls.
|
|
71
|
+
*/
|
|
72
|
+
export interface PanelHeaderButton {
|
|
73
|
+
label: string;
|
|
74
|
+
/** Optional short text shown as the button content (defaults to label). */
|
|
75
|
+
iconText?: string;
|
|
76
|
+
onClick: () => void;
|
|
77
|
+
disabled?: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface MountPanelOptions {
|
|
80
|
+
/** Panel header title */
|
|
81
|
+
title?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Initial position relative to the canvas container (CSS pixels).
|
|
84
|
+
* Defaults to a sensible right-side spot. If a persisted position
|
|
85
|
+
* exists for this panel's persistence key, that wins over this
|
|
86
|
+
* default. The user can drag the panel by its title bar from there.
|
|
87
|
+
*/
|
|
88
|
+
defaultPosition?: {
|
|
89
|
+
left: number;
|
|
90
|
+
top: number;
|
|
91
|
+
};
|
|
92
|
+
/** Custom buttons rendered before the built-in fullscreen + close. */
|
|
93
|
+
headerButtons?: PanelHeaderButton[];
|
|
94
|
+
/** Show the built-in fullscreen toggle button (default true). */
|
|
95
|
+
showFullscreenButton?: boolean;
|
|
96
|
+
/** Show the built-in close X button (default true). */
|
|
97
|
+
showCloseButton?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* If true, clicking the X button hides the panel via setVisible(false)
|
|
100
|
+
* instead of removing it from the DOM. Used by long-lived built-in
|
|
101
|
+
* panels (like info-panel) which are mounted once and reused for
|
|
102
|
+
* the lifetime of the viewer. Extensions typically leave this unset.
|
|
103
|
+
*/
|
|
104
|
+
hideOnClose?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Persistence key suffix. Position + fullscreen state are stored in
|
|
107
|
+
* localStorage under `backpack-viewer:panel:<key>`. Defaults to the
|
|
108
|
+
* extension/panel name passed to mount(). Set this if you need
|
|
109
|
+
* multiple panels for the same name.
|
|
110
|
+
*/
|
|
111
|
+
persistKey?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Called once after the panel is closed for any reason — user
|
|
114
|
+
* clicked the X button, the extension called `close()` itself, or
|
|
115
|
+
* the panel was destroyed because the viewer is reloading. For
|
|
116
|
+
* `hideOnClose` panels, this fires when the X is clicked too. The
|
|
117
|
+
* callback fires at most once per close.
|
|
118
|
+
*/
|
|
119
|
+
onClose?: () => void;
|
|
120
|
+
/** Called whenever fullscreen state changes (button or programmatic). */
|
|
121
|
+
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
122
|
+
}
|
|
123
|
+
export interface MountedPanel {
|
|
124
|
+
/** Remove the panel from the DOM (ignores hideOnClose). */
|
|
125
|
+
close(): void;
|
|
126
|
+
/** Toggle fullscreen state. */
|
|
127
|
+
setFullscreen(fullscreen: boolean): void;
|
|
128
|
+
/** Whether the panel is currently fullscreen. */
|
|
129
|
+
isFullscreen(): boolean;
|
|
130
|
+
/** Update the title shown in the header. */
|
|
131
|
+
setTitle(title: string): void;
|
|
132
|
+
/** Replace the custom header buttons. */
|
|
133
|
+
setHeaderButtons(buttons: PanelHeaderButton[]): void;
|
|
134
|
+
/** Show or hide the panel (without destroying it). */
|
|
135
|
+
setVisible(visible: boolean): void;
|
|
136
|
+
/** Whether the panel is currently visible. */
|
|
137
|
+
isVisible(): boolean;
|
|
138
|
+
/** Bring the panel to the front of the panel z-tier. */
|
|
139
|
+
bringToFront(): void;
|
|
140
|
+
/** The body element the extension owns (not the chrome). */
|
|
141
|
+
element: HTMLElement;
|
|
142
|
+
}
|
|
143
|
+
export interface ExtensionSettingsAPI {
|
|
144
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
145
|
+
set(key: string, value: unknown): Promise<void>;
|
|
146
|
+
remove(key: string): Promise<void>;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Internal interface — what the extension API factory needs from main.ts
|
|
150
|
+
* to construct a per-extension API instance. main.ts creates a single
|
|
151
|
+
* "host" object and passes it to the API factory for each loaded
|
|
152
|
+
* extension. The host is the abstraction barrier between extension code
|
|
153
|
+
* and viewer internals.
|
|
154
|
+
*/
|
|
155
|
+
export interface ViewerHost {
|
|
156
|
+
getGraph(): LearningGraphData | null;
|
|
157
|
+
getGraphName(): string;
|
|
158
|
+
getSelection(): string[];
|
|
159
|
+
getFocus(): ViewerFocusSnapshot | null;
|
|
160
|
+
/** Save current graph state. Caller is expected to mutate getGraph() in place first. */
|
|
161
|
+
saveCurrentGraph(): Promise<void>;
|
|
162
|
+
/** Push current graph onto undo stack. */
|
|
163
|
+
snapshotForUndo(): void;
|
|
164
|
+
/** Drive the canvas. */
|
|
165
|
+
panToNode(nodeId: string): void;
|
|
166
|
+
focusNodes(nodeIds: string[], hops: number): void;
|
|
167
|
+
exitFocus(): void;
|
|
168
|
+
/**
|
|
169
|
+
* Taskbar slot containers (one per supported position). The host
|
|
170
|
+
* creates these in main.ts; the extension API factory routes
|
|
171
|
+
* `registerTaskbarIcon` calls into the right one based on the
|
|
172
|
+
* extension's chosen position.
|
|
173
|
+
*/
|
|
174
|
+
taskbarSlots: {
|
|
175
|
+
topLeft: HTMLElement;
|
|
176
|
+
topRight: HTMLElement;
|
|
177
|
+
bottomLeft: HTMLElement;
|
|
178
|
+
bottomRight: HTMLElement;
|
|
179
|
+
};
|
|
180
|
+
/** Subscribe to events emitted by the host. */
|
|
181
|
+
subscribe(event: ViewerEvent, cb: () => void): () => void;
|
|
182
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned extension API contract.
|
|
3
|
+
*
|
|
4
|
+
* Bumping the major version means breaking changes for installed
|
|
5
|
+
* extensions — extensions declare which version they target in their
|
|
6
|
+
* manifest and the loader rejects mismatches.
|
|
7
|
+
*/
|
|
8
|
+
export const VIEWER_API_VERSION = "1";
|
package/dist/info-panel.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LearningGraphData } from "backpack-ontology";
|
|
2
|
+
import type { PanelMount } from "./extensions/panel-mount";
|
|
2
3
|
export interface EditCallbacks {
|
|
3
4
|
onUpdateNode(nodeId: string, properties: Record<string, unknown>): void;
|
|
4
5
|
onChangeNodeType(nodeId: string, newType: string): void;
|
|
@@ -6,7 +7,7 @@ export interface EditCallbacks {
|
|
|
6
7
|
onDeleteEdge(edgeId: string): void;
|
|
7
8
|
onAddProperty(nodeId: string, key: string, value: string): void;
|
|
8
9
|
}
|
|
9
|
-
export declare function initInfoPanel(container: HTMLElement, callbacks?: EditCallbacks, onNavigateToNode?: (nodeId: string) => void, onFocus?: (nodeIds: string[]) => void): {
|
|
10
|
+
export declare function initInfoPanel(container: HTMLElement, panelMount: PanelMount, callbacks?: EditCallbacks, onNavigateToNode?: (nodeId: string) => void, onFocus?: (nodeIds: string[]) => void): {
|
|
10
11
|
show(nodeIds: string[], data: LearningGraphData): void;
|
|
11
12
|
hide: () => void;
|
|
12
13
|
goBack: () => void;
|
package/dist/info-panel.js
CHANGED
|
@@ -8,13 +8,17 @@ function nodeLabel(node) {
|
|
|
8
8
|
return node.id;
|
|
9
9
|
}
|
|
10
10
|
const EDIT_ICON = '\u270E'; // pencil
|
|
11
|
-
export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
export function initInfoPanel(container, panelMount, callbacks, onNavigateToNode, onFocus) {
|
|
12
|
+
// The body element is owned by info-panel and refilled on every
|
|
13
|
+
// show. Panel-mount wraps it with the standard chrome (title +
|
|
14
|
+
// header buttons + fullscreen + close). info-panel never destroys
|
|
15
|
+
// the panel — it uses setVisible(true/false) instead.
|
|
16
|
+
const bodyEl = document.createElement("div");
|
|
17
|
+
bodyEl.className = "info-panel-content";
|
|
18
|
+
container; // unused — kept in signature for backwards compat
|
|
19
|
+
// (the canvas container is no longer the direct parent; panel-mount
|
|
20
|
+
// handles DOM placement.)
|
|
16
21
|
// --- State ---
|
|
17
|
-
let maximized = false;
|
|
18
22
|
let history = [];
|
|
19
23
|
let historyIndex = -1;
|
|
20
24
|
let navigatingHistory = false;
|
|
@@ -23,11 +27,23 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
23
27
|
let focusDisabled = false;
|
|
24
28
|
let connectionNodeIds = []; // other-end node IDs for each connection
|
|
25
29
|
let activeConnectionIndex = -1;
|
|
30
|
+
// Mount the panel once with placeholder chrome. show*() updates the
|
|
31
|
+
// title + header buttons each time a node is selected.
|
|
32
|
+
const handle = panelMount.mount("info", bodyEl, {
|
|
33
|
+
title: "Node info",
|
|
34
|
+
persistKey: "info-panel",
|
|
35
|
+
hideOnClose: true,
|
|
36
|
+
onClose: () => {
|
|
37
|
+
// Reset history when the user explicitly closes — re-opening on
|
|
38
|
+
// a fresh node selection should start a new history chain.
|
|
39
|
+
history = [];
|
|
40
|
+
historyIndex = -1;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
// Start hidden — info-panel only appears when a node is selected.
|
|
44
|
+
handle.setVisible(false);
|
|
26
45
|
function hide() {
|
|
27
|
-
|
|
28
|
-
panel.classList.remove("info-panel-maximized");
|
|
29
|
-
panel.innerHTML = "";
|
|
30
|
-
maximized = false;
|
|
46
|
+
handle.setVisible(false);
|
|
31
47
|
history = [];
|
|
32
48
|
historyIndex = -1;
|
|
33
49
|
}
|
|
@@ -65,60 +81,38 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
65
81
|
showSingle(nodeId, lastData);
|
|
66
82
|
navigatingHistory = false;
|
|
67
83
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Build the header-button list reflecting current state. Called
|
|
86
|
+
* after every show() so the back/forward/focus enabled state stays
|
|
87
|
+
* in sync.
|
|
88
|
+
*/
|
|
89
|
+
function refreshHeaderButtons() {
|
|
90
|
+
const buttons = [
|
|
91
|
+
{
|
|
92
|
+
label: "Back",
|
|
93
|
+
iconText: "\u2190",
|
|
94
|
+
onClick: goBack,
|
|
95
|
+
disabled: historyIndex <= 0,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: "Forward",
|
|
99
|
+
iconText: "\u2192",
|
|
100
|
+
onClick: goForward,
|
|
101
|
+
disabled: historyIndex >= history.length - 1,
|
|
102
|
+
},
|
|
103
|
+
];
|
|
88
104
|
if (onFocus && currentNodeIds.length > 0) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!focusDisabled)
|
|
98
|
-
onFocus(currentNodeIds);
|
|
105
|
+
buttons.push({
|
|
106
|
+
label: "Focus",
|
|
107
|
+
iconText: "\u25CE",
|
|
108
|
+
onClick: () => {
|
|
109
|
+
if (!focusDisabled)
|
|
110
|
+
onFocus(currentNodeIds);
|
|
111
|
+
},
|
|
112
|
+
disabled: focusDisabled,
|
|
99
113
|
});
|
|
100
|
-
toolbar.appendChild(focusBtn);
|
|
101
114
|
}
|
|
102
|
-
|
|
103
|
-
const maxBtn = document.createElement("button");
|
|
104
|
-
maxBtn.className = "info-toolbar-btn";
|
|
105
|
-
maxBtn.textContent = maximized ? "\u2398" : "\u26F6";
|
|
106
|
-
maxBtn.title = maximized ? "Restore" : "Maximize";
|
|
107
|
-
maxBtn.addEventListener("click", () => {
|
|
108
|
-
maximized = !maximized;
|
|
109
|
-
panel.classList.toggle("info-panel-maximized", maximized);
|
|
110
|
-
maxBtn.textContent = maximized ? "\u2398" : "\u26F6";
|
|
111
|
-
maxBtn.title = maximized ? "Restore" : "Maximize";
|
|
112
|
-
});
|
|
113
|
-
toolbar.appendChild(maxBtn);
|
|
114
|
-
// Close
|
|
115
|
-
const closeBtn = document.createElement("button");
|
|
116
|
-
closeBtn.className = "info-toolbar-btn info-close-btn";
|
|
117
|
-
closeBtn.textContent = "\u00d7";
|
|
118
|
-
closeBtn.title = "Close";
|
|
119
|
-
closeBtn.addEventListener("click", hide);
|
|
120
|
-
toolbar.appendChild(closeBtn);
|
|
121
|
-
return toolbar;
|
|
115
|
+
handle.setHeaderButtons(buttons);
|
|
122
116
|
}
|
|
123
117
|
function showSingle(nodeId, data) {
|
|
124
118
|
const node = data.nodes.find((n) => n.id === nodeId);
|
|
@@ -128,15 +122,15 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
128
122
|
// Store connection targets for keyboard cycling
|
|
129
123
|
connectionNodeIds = connectedEdges.map((e) => e.sourceId === nodeId ? e.targetId : e.sourceId);
|
|
130
124
|
activeConnectionIndex = -1;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
bodyEl.replaceChildren();
|
|
126
|
+
// Update chrome with the node label as title + refresh header buttons
|
|
127
|
+
handle.setTitle(nodeLabel(node));
|
|
128
|
+
refreshHeaderButtons();
|
|
129
|
+
handle.setVisible(true);
|
|
130
|
+
// Pinned header area (type badge + node identity); the toolbar
|
|
131
|
+
// moved out of here into the panel chrome.
|
|
136
132
|
const pinnedHeader = document.createElement("div");
|
|
137
133
|
pinnedHeader.className = "info-panel-header";
|
|
138
|
-
// Toolbar (back, forward, maximize, close)
|
|
139
|
-
pinnedHeader.appendChild(createToolbar());
|
|
140
134
|
// Header: type badge + label
|
|
141
135
|
const header = document.createElement("div");
|
|
142
136
|
header.className = "info-header";
|
|
@@ -191,7 +185,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
191
185
|
header.appendChild(label);
|
|
192
186
|
header.appendChild(nodeIdEl);
|
|
193
187
|
pinnedHeader.appendChild(header);
|
|
194
|
-
|
|
188
|
+
bodyEl.appendChild(pinnedHeader);
|
|
195
189
|
// Scrollable body for properties, connections, timestamps
|
|
196
190
|
const body = document.createElement("div");
|
|
197
191
|
body.className = "info-panel-body";
|
|
@@ -382,7 +376,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
382
376
|
deleteSection.appendChild(deleteBtn);
|
|
383
377
|
body.appendChild(deleteSection);
|
|
384
378
|
}
|
|
385
|
-
|
|
379
|
+
bodyEl.appendChild(body);
|
|
386
380
|
}
|
|
387
381
|
function showMulti(nodeIds, data) {
|
|
388
382
|
const selectedSet = new Set(nodeIds);
|
|
@@ -390,12 +384,11 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
390
384
|
if (nodes.length === 0)
|
|
391
385
|
return;
|
|
392
386
|
const sharedEdges = data.edges.filter((e) => selectedSet.has(e.sourceId) && selectedSet.has(e.targetId));
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
panel.appendChild(createToolbar());
|
|
387
|
+
bodyEl.replaceChildren();
|
|
388
|
+
// Update chrome with multi-selection title + refresh header buttons
|
|
389
|
+
handle.setTitle(`${nodes.length} nodes selected`);
|
|
390
|
+
refreshHeaderButtons();
|
|
391
|
+
handle.setVisible(true);
|
|
399
392
|
const header = document.createElement("div");
|
|
400
393
|
header.className = "info-header";
|
|
401
394
|
const label = document.createElement("h3");
|
|
@@ -416,7 +409,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
416
409
|
badgeRow.appendChild(badge);
|
|
417
410
|
}
|
|
418
411
|
header.appendChild(badgeRow);
|
|
419
|
-
|
|
412
|
+
bodyEl.appendChild(header);
|
|
420
413
|
const nodesSection = createSection("Selected Nodes");
|
|
421
414
|
const nodesList = document.createElement("ul");
|
|
422
415
|
nodesList.className = "info-connections";
|
|
@@ -445,7 +438,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
445
438
|
nodesList.appendChild(li);
|
|
446
439
|
}
|
|
447
440
|
nodesSection.appendChild(nodesList);
|
|
448
|
-
|
|
441
|
+
bodyEl.appendChild(nodesSection);
|
|
449
442
|
const connSection = createSection(sharedEdges.length > 0
|
|
450
443
|
? `Connections Between Selected (${sharedEdges.length})`
|
|
451
444
|
: "Connections Between Selected");
|
|
@@ -517,7 +510,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
517
510
|
}
|
|
518
511
|
connSection.appendChild(list);
|
|
519
512
|
}
|
|
520
|
-
|
|
513
|
+
bodyEl.appendChild(connSection);
|
|
521
514
|
}
|
|
522
515
|
return {
|
|
523
516
|
show(nodeIds, data) {
|
|
@@ -558,8 +551,8 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
558
551
|
if (activeConnectionIndex < 0)
|
|
559
552
|
activeConnectionIndex = connectionNodeIds.length - 1;
|
|
560
553
|
}
|
|
561
|
-
// Highlight active row in the panel
|
|
562
|
-
const items =
|
|
554
|
+
// Highlight active row in the panel body
|
|
555
|
+
const items = bodyEl.querySelectorAll(".info-connection");
|
|
563
556
|
items.forEach((el, i) => {
|
|
564
557
|
el.classList.toggle("info-connection-active", i === activeConnectionIndex);
|
|
565
558
|
});
|
|
@@ -570,14 +563,12 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
570
563
|
},
|
|
571
564
|
setFocusDisabled(disabled) {
|
|
572
565
|
focusDisabled = disabled;
|
|
573
|
-
|
|
574
|
-
if
|
|
575
|
-
|
|
576
|
-
btn.style.opacity = disabled ? "0.3" : "";
|
|
577
|
-
}
|
|
566
|
+
// Refresh the chrome's header buttons to pick up the new
|
|
567
|
+
// disabled state on the focus button (if currently shown).
|
|
568
|
+
refreshHeaderButtons();
|
|
578
569
|
},
|
|
579
570
|
get visible() {
|
|
580
|
-
return
|
|
571
|
+
return handle.isVisible();
|
|
581
572
|
},
|
|
582
573
|
};
|
|
583
574
|
}
|
package/dist/keybindings.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease" | "toggleSidebar" | "walkMode" | "walkIsolate";
|
|
1
|
+
export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease" | "toggleSidebar" | "walkMode" | "walkIsolate" | "resetPins";
|
|
2
2
|
export type KeybindingMap = Record<KeybindingAction, string>;
|
|
3
3
|
/** Parse a binding string like "ctrl+shift+z" and check if it matches a KeyboardEvent. */
|
|
4
4
|
export declare function matchKey(e: KeyboardEvent, binding: string): boolean;
|
package/dist/keybindings.js
CHANGED
package/dist/layout-worker.d.ts
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* Protocol:
|
|
8
8
|
* Main → Worker:
|
|
9
9
|
* { type: 'start', nodes, edges, params } — begin simulation
|
|
10
|
-
* { type: 'stop' } — halt simulation
|
|
10
|
+
* { type: 'stop' } — halt simulation (pauses)
|
|
11
|
+
* { type: 'resume', alpha? } — resume after a stop
|
|
11
12
|
* { type: 'params', params } — update layout params + reheat
|
|
13
|
+
* { type: 'pin', updates: [{id, x, y}] } — mark node(s) as pinned at given pos
|
|
14
|
+
* { type: 'unpin', ids: string[] | 'all' } — release pins; 'all' clears every pin
|
|
12
15
|
*
|
|
13
16
|
* Worker → Main:
|
|
14
17
|
* { type: 'tick', positions: Float64Array, alpha } — position update per tick
|
package/dist/layout-worker.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* Protocol:
|
|
8
8
|
* Main → Worker:
|
|
9
9
|
* { type: 'start', nodes, edges, params } — begin simulation
|
|
10
|
-
* { type: 'stop' } — halt simulation
|
|
10
|
+
* { type: 'stop' } — halt simulation (pauses)
|
|
11
|
+
* { type: 'resume', alpha? } — resume after a stop
|
|
11
12
|
* { type: 'params', params } — update layout params + reheat
|
|
13
|
+
* { type: 'pin', updates: [{id, x, y}] } — mark node(s) as pinned at given pos
|
|
14
|
+
* { type: 'unpin', ids: string[] | 'all' } — release pins; 'all' clears every pin
|
|
12
15
|
*
|
|
13
16
|
* Worker → Main:
|
|
14
17
|
* { type: 'tick', positions: Float64Array, alpha } — position update per tick
|
|
@@ -66,6 +69,53 @@ self.onmessage = (e) => {
|
|
|
66
69
|
if (msg.type === "stop") {
|
|
67
70
|
running = false;
|
|
68
71
|
}
|
|
72
|
+
if (msg.type === "resume") {
|
|
73
|
+
if (!running && state) {
|
|
74
|
+
alpha = Math.max(alpha, typeof msg.alpha === "number" ? msg.alpha : 0.5);
|
|
75
|
+
running = true;
|
|
76
|
+
runLoop();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (msg.type === "pin" && state) {
|
|
80
|
+
// updates: [{ id, x, y }] — apply to the worker's state copy. Pinned
|
|
81
|
+
// nodes are treated as fixed points by tick() in layout.ts.
|
|
82
|
+
const updates = msg.updates;
|
|
83
|
+
for (const u of updates) {
|
|
84
|
+
const node = state.nodeMap.get(u.id);
|
|
85
|
+
if (node) {
|
|
86
|
+
node.x = u.x;
|
|
87
|
+
node.y = u.y;
|
|
88
|
+
node.vx = 0;
|
|
89
|
+
node.vy = 0;
|
|
90
|
+
node.pinned = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Nudge simulation so neighbors react to the new pin location
|
|
94
|
+
alpha = Math.max(alpha, 0.3);
|
|
95
|
+
if (!running) {
|
|
96
|
+
running = true;
|
|
97
|
+
runLoop();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (msg.type === "unpin" && state) {
|
|
101
|
+
const ids = msg.ids;
|
|
102
|
+
if (ids === "all") {
|
|
103
|
+
for (const node of state.nodes)
|
|
104
|
+
node.pinned = false;
|
|
105
|
+
}
|
|
106
|
+
else if (Array.isArray(ids)) {
|
|
107
|
+
for (const id of ids) {
|
|
108
|
+
const node = state.nodeMap.get(id);
|
|
109
|
+
if (node)
|
|
110
|
+
node.pinned = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
alpha = Math.max(alpha, 0.5);
|
|
114
|
+
if (!running) {
|
|
115
|
+
running = true;
|
|
116
|
+
runLoop();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
69
119
|
if (msg.type === "params") {
|
|
70
120
|
setLayoutParams(msg.params);
|
|
71
121
|
// Reheat simulation
|
package/dist/layout.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface LayoutNode {
|
|
|
7
7
|
vy: number;
|
|
8
8
|
label: string;
|
|
9
9
|
type: string;
|
|
10
|
+
/**
|
|
11
|
+
* When true, the simulation treats this node as a fixed point:
|
|
12
|
+
* it still contributes forces to neighbors (its x/y is read for
|
|
13
|
+
* repulsion and attraction calculations) but its own position is
|
|
14
|
+
* not updated by the integration step. Set by the viewer's drag
|
|
15
|
+
* handler to temporarily pin a node at a user-chosen location.
|
|
16
|
+
*/
|
|
17
|
+
pinned?: boolean;
|
|
10
18
|
}
|
|
11
19
|
export interface LayoutEdge {
|
|
12
20
|
sourceId: string;
|
package/dist/layout.js
CHANGED
|
@@ -229,8 +229,15 @@ export function tick(state, alpha) {
|
|
|
229
229
|
node.vx += (c.x - node.x) * params.clusterStrength * alpha;
|
|
230
230
|
node.vy += (c.y - node.y) * params.clusterStrength * alpha;
|
|
231
231
|
}
|
|
232
|
-
// Integrate — update positions, apply damping, clamp velocity
|
|
232
|
+
// Integrate — update positions, apply damping, clamp velocity.
|
|
233
|
+
// Pinned nodes keep their x/y and have velocity zeroed so they
|
|
234
|
+
// don't drift when released later with pending momentum.
|
|
233
235
|
for (const node of nodes) {
|
|
236
|
+
if (node.pinned) {
|
|
237
|
+
node.vx = 0;
|
|
238
|
+
node.vy = 0;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
234
241
|
node.vx *= DAMPING;
|
|
235
242
|
node.vy *= DAMPING;
|
|
236
243
|
const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy);
|