backpack-viewer 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/colors.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Deterministic type → color mapping.
3
+ * Earth-tone accent palette on a neutral gray UI.
4
+ * These are the only warm colors in the interface —
5
+ * everything else is grayscale.
6
+ */
7
+ const PALETTE = [
8
+ "#d4a27f", // warm tan
9
+ "#c17856", // terracotta
10
+ "#b07a5e", // sienna
11
+ "#d4956b", // burnt amber
12
+ "#a67c5a", // walnut
13
+ "#cc9e7c", // copper
14
+ "#c4866a", // clay
15
+ "#cb8e6c", // apricot
16
+ "#b8956e", // wheat
17
+ "#a88a70", // driftwood
18
+ "#d9b08c", // caramel
19
+ "#c4a882", // sand
20
+ "#e8b898", // peach
21
+ "#b5927a", // dusty rose
22
+ "#a8886e", // muted brown
23
+ "#d1a990", // blush tan
24
+ ];
25
+ const cache = new Map();
26
+ export function getColor(type) {
27
+ const cached = cache.get(type);
28
+ if (cached)
29
+ return cached;
30
+ let hash = 0;
31
+ for (let i = 0; i < type.length; i++) {
32
+ hash = ((hash << 5) - hash + type.charCodeAt(i)) | 0;
33
+ }
34
+ const color = PALETTE[Math.abs(hash) % PALETTE.length];
35
+ cache.set(type, color);
36
+ return color;
37
+ }
@@ -0,0 +1,13 @@
1
+ import type { OntologyData } from "backpack-ontology";
2
+ export interface EditCallbacks {
3
+ onUpdateNode(nodeId: string, properties: Record<string, unknown>): void;
4
+ onChangeNodeType(nodeId: string, newType: string): void;
5
+ onDeleteNode(nodeId: string): void;
6
+ onDeleteEdge(edgeId: string): void;
7
+ onAddProperty(nodeId: string, key: string, value: string): void;
8
+ }
9
+ export declare function initInfoPanel(container: HTMLElement, callbacks?: EditCallbacks, onNavigateToNode?: (nodeId: string) => void): {
10
+ show(nodeIds: string[], data: OntologyData): void;
11
+ hide: () => void;
12
+ readonly visible: boolean;
13
+ };
@@ -0,0 +1,579 @@
1
+ import { getColor } from "./colors";
2
+ /** Extract a display label from a node — first string property, fallback to id. */
3
+ function nodeLabel(node) {
4
+ for (const value of Object.values(node.properties)) {
5
+ if (typeof value === "string")
6
+ return value;
7
+ }
8
+ return node.id;
9
+ }
10
+ const EDIT_ICON = '\u270E'; // pencil
11
+ export function initInfoPanel(container, callbacks, onNavigateToNode) {
12
+ const panel = document.createElement("div");
13
+ panel.id = "info-panel";
14
+ panel.className = "info-panel hidden";
15
+ container.appendChild(panel);
16
+ // --- State ---
17
+ let maximized = false;
18
+ let history = [];
19
+ let historyIndex = -1;
20
+ let navigatingHistory = false;
21
+ let lastData = null;
22
+ function hide() {
23
+ panel.classList.add("hidden");
24
+ panel.classList.remove("info-panel-maximized");
25
+ panel.innerHTML = "";
26
+ maximized = false;
27
+ history = [];
28
+ historyIndex = -1;
29
+ }
30
+ function navigateTo(nodeId) {
31
+ if (!lastData || !onNavigateToNode)
32
+ return;
33
+ // Push to history before navigating
34
+ if (historyIndex < history.length - 1) {
35
+ history = history.slice(0, historyIndex + 1);
36
+ }
37
+ history.push(nodeId);
38
+ historyIndex = history.length - 1;
39
+ // Set flag so the show() call from canvas doesn't double-push
40
+ navigatingHistory = true;
41
+ onNavigateToNode(nodeId);
42
+ navigatingHistory = false;
43
+ }
44
+ function goBack() {
45
+ if (historyIndex <= 0 || !lastData || !onNavigateToNode)
46
+ return;
47
+ historyIndex--;
48
+ navigatingHistory = true;
49
+ onNavigateToNode(history[historyIndex]);
50
+ navigatingHistory = false;
51
+ }
52
+ function goForward() {
53
+ if (historyIndex >= history.length - 1 || !lastData || !onNavigateToNode)
54
+ return;
55
+ historyIndex++;
56
+ navigatingHistory = true;
57
+ onNavigateToNode(history[historyIndex]);
58
+ navigatingHistory = false;
59
+ }
60
+ function createToolbar() {
61
+ const toolbar = document.createElement("div");
62
+ toolbar.className = "info-panel-toolbar";
63
+ // Back
64
+ const backBtn = document.createElement("button");
65
+ backBtn.className = "info-toolbar-btn";
66
+ backBtn.textContent = "\u2190";
67
+ backBtn.title = "Back";
68
+ backBtn.disabled = historyIndex <= 0;
69
+ backBtn.addEventListener("click", goBack);
70
+ toolbar.appendChild(backBtn);
71
+ // Forward
72
+ const fwdBtn = document.createElement("button");
73
+ fwdBtn.className = "info-toolbar-btn";
74
+ fwdBtn.textContent = "\u2192";
75
+ fwdBtn.title = "Forward";
76
+ fwdBtn.disabled = historyIndex >= history.length - 1;
77
+ fwdBtn.addEventListener("click", goForward);
78
+ toolbar.appendChild(fwdBtn);
79
+ // Maximize/restore
80
+ const maxBtn = document.createElement("button");
81
+ maxBtn.className = "info-toolbar-btn";
82
+ maxBtn.textContent = maximized ? "\u2398" : "\u26F6";
83
+ maxBtn.title = maximized ? "Restore" : "Maximize";
84
+ maxBtn.addEventListener("click", () => {
85
+ maximized = !maximized;
86
+ panel.classList.toggle("info-panel-maximized", maximized);
87
+ maxBtn.textContent = maximized ? "\u2398" : "\u26F6";
88
+ maxBtn.title = maximized ? "Restore" : "Maximize";
89
+ });
90
+ toolbar.appendChild(maxBtn);
91
+ // Close
92
+ const closeBtn = document.createElement("button");
93
+ closeBtn.className = "info-toolbar-btn info-close-btn";
94
+ closeBtn.textContent = "\u00d7";
95
+ closeBtn.title = "Close";
96
+ closeBtn.addEventListener("click", hide);
97
+ toolbar.appendChild(closeBtn);
98
+ return toolbar;
99
+ }
100
+ function showSingle(nodeId, data) {
101
+ const node = data.nodes.find((n) => n.id === nodeId);
102
+ if (!node)
103
+ return;
104
+ const connectedEdges = data.edges.filter((e) => e.sourceId === nodeId || e.targetId === nodeId);
105
+ panel.innerHTML = "";
106
+ panel.classList.remove("hidden");
107
+ if (maximized)
108
+ panel.classList.add("info-panel-maximized");
109
+ // Toolbar (back, forward, maximize, close)
110
+ panel.appendChild(createToolbar());
111
+ // Header: type badge + label
112
+ const header = document.createElement("div");
113
+ header.className = "info-header";
114
+ const typeBadge = document.createElement("span");
115
+ typeBadge.className = "info-type-badge";
116
+ typeBadge.textContent = node.type;
117
+ typeBadge.style.backgroundColor = getColor(node.type);
118
+ if (callbacks) {
119
+ typeBadge.classList.add("info-editable");
120
+ const typeEditBtn = document.createElement("button");
121
+ typeEditBtn.className = "info-inline-edit";
122
+ typeEditBtn.textContent = EDIT_ICON;
123
+ typeEditBtn.addEventListener("click", (e) => {
124
+ e.stopPropagation();
125
+ const input = document.createElement("input");
126
+ input.type = "text";
127
+ input.className = "info-edit-inline-input";
128
+ input.value = node.type;
129
+ typeBadge.textContent = "";
130
+ typeBadge.appendChild(input);
131
+ input.focus();
132
+ input.select();
133
+ const finish = () => {
134
+ const val = input.value.trim();
135
+ if (val && val !== node.type) {
136
+ callbacks.onChangeNodeType(nodeId, val);
137
+ }
138
+ else {
139
+ typeBadge.textContent = node.type;
140
+ typeBadge.appendChild(typeEditBtn);
141
+ }
142
+ };
143
+ input.addEventListener("blur", finish);
144
+ input.addEventListener("keydown", (ke) => {
145
+ if (ke.key === "Enter")
146
+ input.blur();
147
+ if (ke.key === "Escape") {
148
+ input.value = node.type;
149
+ input.blur();
150
+ }
151
+ });
152
+ });
153
+ typeBadge.appendChild(typeEditBtn);
154
+ }
155
+ const label = document.createElement("h3");
156
+ label.className = "info-label";
157
+ label.textContent = nodeLabel(node);
158
+ const nodeIdEl = document.createElement("span");
159
+ nodeIdEl.className = "info-id";
160
+ nodeIdEl.textContent = node.id;
161
+ header.appendChild(typeBadge);
162
+ header.appendChild(label);
163
+ header.appendChild(nodeIdEl);
164
+ panel.appendChild(header);
165
+ // Properties section (editable)
166
+ const propKeys = Object.keys(node.properties);
167
+ const propSection = createSection("Properties");
168
+ if (propKeys.length > 0) {
169
+ const table = document.createElement("dl");
170
+ table.className = "info-props";
171
+ for (const key of propKeys) {
172
+ const dt = document.createElement("dt");
173
+ dt.textContent = key;
174
+ const dd = document.createElement("dd");
175
+ if (callbacks) {
176
+ const valueStr = formatValue(node.properties[key]);
177
+ const input = document.createElement("input");
178
+ input.type = "text";
179
+ input.className = "info-edit-input";
180
+ input.value = valueStr;
181
+ input.addEventListener("keydown", (e) => {
182
+ if (e.key === "Enter") {
183
+ input.blur();
184
+ }
185
+ });
186
+ input.addEventListener("blur", () => {
187
+ const newVal = input.value;
188
+ if (newVal !== valueStr) {
189
+ callbacks.onUpdateNode(nodeId, { [key]: tryParseValue(newVal) });
190
+ }
191
+ });
192
+ dd.appendChild(input);
193
+ // Delete property button
194
+ const delProp = document.createElement("button");
195
+ delProp.className = "info-delete-prop";
196
+ delProp.textContent = "\u00d7";
197
+ delProp.title = `Remove ${key}`;
198
+ delProp.addEventListener("click", () => {
199
+ const updated = { ...node.properties };
200
+ delete updated[key];
201
+ callbacks.onUpdateNode(nodeId, updated);
202
+ });
203
+ dd.appendChild(delProp);
204
+ }
205
+ else {
206
+ dd.appendChild(renderValue(node.properties[key]));
207
+ }
208
+ table.appendChild(dt);
209
+ table.appendChild(dd);
210
+ }
211
+ propSection.appendChild(table);
212
+ }
213
+ // Add property button
214
+ if (callbacks) {
215
+ const addBtn = document.createElement("button");
216
+ addBtn.className = "info-add-btn";
217
+ addBtn.textContent = "+ Add property";
218
+ addBtn.addEventListener("click", () => {
219
+ const row = document.createElement("div");
220
+ row.className = "info-add-row";
221
+ const keyInput = document.createElement("input");
222
+ keyInput.type = "text";
223
+ keyInput.className = "info-edit-input";
224
+ keyInput.placeholder = "key";
225
+ const valInput = document.createElement("input");
226
+ valInput.type = "text";
227
+ valInput.className = "info-edit-input";
228
+ valInput.placeholder = "value";
229
+ const saveBtn = document.createElement("button");
230
+ saveBtn.className = "info-add-save";
231
+ saveBtn.textContent = "Add";
232
+ saveBtn.addEventListener("click", () => {
233
+ if (keyInput.value) {
234
+ callbacks.onAddProperty(nodeId, keyInput.value, valInput.value);
235
+ }
236
+ });
237
+ row.appendChild(keyInput);
238
+ row.appendChild(valInput);
239
+ row.appendChild(saveBtn);
240
+ propSection.appendChild(row);
241
+ keyInput.focus();
242
+ });
243
+ propSection.appendChild(addBtn);
244
+ }
245
+ panel.appendChild(propSection);
246
+ // Connections section
247
+ if (connectedEdges.length > 0) {
248
+ const section = createSection(`Connections (${connectedEdges.length})`);
249
+ const list = document.createElement("ul");
250
+ list.className = "info-connections";
251
+ for (const edge of connectedEdges) {
252
+ const isOutgoing = edge.sourceId === nodeId;
253
+ const otherId = isOutgoing ? edge.targetId : edge.sourceId;
254
+ const otherNode = data.nodes.find((n) => n.id === otherId);
255
+ const otherLabel = otherNode ? nodeLabel(otherNode) : otherId;
256
+ const li = document.createElement("li");
257
+ li.className = "info-connection";
258
+ // Make clickable if we can navigate
259
+ if (onNavigateToNode && otherNode) {
260
+ li.classList.add("info-connection-link");
261
+ li.addEventListener("click", (e) => {
262
+ // Don't navigate if clicking the delete edge button
263
+ if (e.target.closest(".info-delete-edge"))
264
+ return;
265
+ navigateTo(otherId);
266
+ });
267
+ }
268
+ if (otherNode) {
269
+ const dot = document.createElement("span");
270
+ dot.className = "info-target-dot";
271
+ dot.style.backgroundColor = getColor(otherNode.type);
272
+ li.appendChild(dot);
273
+ }
274
+ const arrow = document.createElement("span");
275
+ arrow.className = "info-arrow";
276
+ arrow.textContent = isOutgoing ? "\u2192" : "\u2190";
277
+ const edgeType = document.createElement("span");
278
+ edgeType.className = "info-edge-type";
279
+ edgeType.textContent = edge.type;
280
+ const target = document.createElement("span");
281
+ target.className = "info-target";
282
+ target.textContent = otherLabel;
283
+ li.appendChild(arrow);
284
+ li.appendChild(edgeType);
285
+ li.appendChild(target);
286
+ // Edge properties (if any)
287
+ const edgePropKeys = Object.keys(edge.properties);
288
+ if (edgePropKeys.length > 0) {
289
+ const edgeProps = document.createElement("div");
290
+ edgeProps.className = "info-edge-props";
291
+ for (const key of edgePropKeys) {
292
+ const prop = document.createElement("span");
293
+ prop.className = "info-edge-prop";
294
+ prop.textContent = `${key}: ${formatValue(edge.properties[key])}`;
295
+ edgeProps.appendChild(prop);
296
+ }
297
+ li.appendChild(edgeProps);
298
+ }
299
+ // Delete edge button
300
+ if (callbacks) {
301
+ const delEdge = document.createElement("button");
302
+ delEdge.className = "info-delete-edge";
303
+ delEdge.textContent = "\u00d7";
304
+ delEdge.title = "Remove connection";
305
+ delEdge.addEventListener("click", (e) => {
306
+ e.stopPropagation();
307
+ callbacks.onDeleteEdge(edge.id);
308
+ });
309
+ li.appendChild(delEdge);
310
+ }
311
+ list.appendChild(li);
312
+ }
313
+ section.appendChild(list);
314
+ panel.appendChild(section);
315
+ }
316
+ // Timestamps
317
+ const tsSection = createSection("Timestamps");
318
+ const timestamps = document.createElement("dl");
319
+ timestamps.className = "info-props";
320
+ const createdDt = document.createElement("dt");
321
+ createdDt.textContent = "created";
322
+ const createdDd = document.createElement("dd");
323
+ createdDd.textContent = formatTimestamp(node.createdAt);
324
+ const updatedDt = document.createElement("dt");
325
+ updatedDt.textContent = "updated";
326
+ const updatedDd = document.createElement("dd");
327
+ updatedDd.textContent = formatTimestamp(node.updatedAt);
328
+ timestamps.appendChild(createdDt);
329
+ timestamps.appendChild(createdDd);
330
+ timestamps.appendChild(updatedDt);
331
+ timestamps.appendChild(updatedDd);
332
+ tsSection.appendChild(timestamps);
333
+ panel.appendChild(tsSection);
334
+ // Delete node button
335
+ if (callbacks) {
336
+ const deleteSection = document.createElement("div");
337
+ deleteSection.className = "info-section info-danger";
338
+ const deleteBtn = document.createElement("button");
339
+ deleteBtn.className = "info-delete-node";
340
+ deleteBtn.textContent = "Delete node";
341
+ deleteBtn.addEventListener("click", () => {
342
+ callbacks.onDeleteNode(nodeId);
343
+ hide();
344
+ });
345
+ deleteSection.appendChild(deleteBtn);
346
+ panel.appendChild(deleteSection);
347
+ }
348
+ }
349
+ function showMulti(nodeIds, data) {
350
+ const selectedSet = new Set(nodeIds);
351
+ const nodes = data.nodes.filter((n) => selectedSet.has(n.id));
352
+ if (nodes.length === 0)
353
+ return;
354
+ const sharedEdges = data.edges.filter((e) => selectedSet.has(e.sourceId) && selectedSet.has(e.targetId));
355
+ panel.innerHTML = "";
356
+ panel.classList.remove("hidden");
357
+ if (maximized)
358
+ panel.classList.add("info-panel-maximized");
359
+ // Toolbar
360
+ panel.appendChild(createToolbar());
361
+ const header = document.createElement("div");
362
+ header.className = "info-header";
363
+ const label = document.createElement("h3");
364
+ label.className = "info-label";
365
+ label.textContent = `${nodes.length} nodes selected`;
366
+ header.appendChild(label);
367
+ const badgeRow = document.createElement("div");
368
+ badgeRow.style.cssText = "display:flex;flex-wrap:wrap;gap:4px;margin-top:6px";
369
+ const typeCounts = new Map();
370
+ for (const node of nodes) {
371
+ typeCounts.set(node.type, (typeCounts.get(node.type) ?? 0) + 1);
372
+ }
373
+ for (const [type, count] of typeCounts) {
374
+ const badge = document.createElement("span");
375
+ badge.className = "info-type-badge";
376
+ badge.style.backgroundColor = getColor(type);
377
+ badge.textContent = count > 1 ? `${type} (${count})` : type;
378
+ badgeRow.appendChild(badge);
379
+ }
380
+ header.appendChild(badgeRow);
381
+ panel.appendChild(header);
382
+ const nodesSection = createSection("Selected Nodes");
383
+ const nodesList = document.createElement("ul");
384
+ nodesList.className = "info-connections";
385
+ for (const node of nodes) {
386
+ const li = document.createElement("li");
387
+ li.className = "info-connection";
388
+ // Make clickable to navigate to single node
389
+ if (onNavigateToNode) {
390
+ li.classList.add("info-connection-link");
391
+ li.addEventListener("click", () => {
392
+ navigateTo(node.id);
393
+ });
394
+ }
395
+ const dot = document.createElement("span");
396
+ dot.className = "info-target-dot";
397
+ dot.style.backgroundColor = getColor(node.type);
398
+ const name = document.createElement("span");
399
+ name.className = "info-target";
400
+ name.textContent = nodeLabel(node);
401
+ const type = document.createElement("span");
402
+ type.className = "info-edge-type";
403
+ type.textContent = node.type;
404
+ li.appendChild(dot);
405
+ li.appendChild(name);
406
+ li.appendChild(type);
407
+ nodesList.appendChild(li);
408
+ }
409
+ nodesSection.appendChild(nodesList);
410
+ panel.appendChild(nodesSection);
411
+ const connSection = createSection(sharedEdges.length > 0
412
+ ? `Connections Between Selected (${sharedEdges.length})`
413
+ : "Connections Between Selected");
414
+ if (sharedEdges.length === 0) {
415
+ const empty = document.createElement("p");
416
+ empty.style.cssText = "font-size:12px;color:var(--text-dim)";
417
+ empty.textContent = "No direct connections between selected nodes";
418
+ connSection.appendChild(empty);
419
+ }
420
+ else {
421
+ const list = document.createElement("ul");
422
+ list.className = "info-connections";
423
+ for (const edge of sharedEdges) {
424
+ const sourceNode = data.nodes.find((n) => n.id === edge.sourceId);
425
+ const targetNode = data.nodes.find((n) => n.id === edge.targetId);
426
+ const sourceLabel = sourceNode
427
+ ? nodeLabel(sourceNode)
428
+ : edge.sourceId;
429
+ const targetLabel = targetNode
430
+ ? nodeLabel(targetNode)
431
+ : edge.targetId;
432
+ const li = document.createElement("li");
433
+ li.className = "info-connection";
434
+ if (sourceNode) {
435
+ const dot = document.createElement("span");
436
+ dot.className = "info-target-dot";
437
+ dot.style.backgroundColor = getColor(sourceNode.type);
438
+ li.appendChild(dot);
439
+ }
440
+ const source = document.createElement("span");
441
+ source.className = "info-target";
442
+ source.textContent = sourceLabel;
443
+ const arrow = document.createElement("span");
444
+ arrow.className = "info-arrow";
445
+ arrow.textContent = "\u2192";
446
+ const edgeType = document.createElement("span");
447
+ edgeType.className = "info-edge-type";
448
+ edgeType.textContent = edge.type;
449
+ const arrow2 = document.createElement("span");
450
+ arrow2.className = "info-arrow";
451
+ arrow2.textContent = "\u2192";
452
+ li.appendChild(source);
453
+ li.appendChild(arrow);
454
+ li.appendChild(edgeType);
455
+ li.appendChild(arrow2);
456
+ if (targetNode) {
457
+ const dot2 = document.createElement("span");
458
+ dot2.className = "info-target-dot";
459
+ dot2.style.backgroundColor = getColor(targetNode.type);
460
+ li.appendChild(dot2);
461
+ }
462
+ const target = document.createElement("span");
463
+ target.className = "info-target";
464
+ target.textContent = targetLabel;
465
+ li.appendChild(target);
466
+ const edgePropKeys = Object.keys(edge.properties);
467
+ if (edgePropKeys.length > 0) {
468
+ const edgeProps = document.createElement("div");
469
+ edgeProps.className = "info-edge-props";
470
+ for (const key of edgePropKeys) {
471
+ const prop = document.createElement("span");
472
+ prop.className = "info-edge-prop";
473
+ prop.textContent = `${key}: ${formatValue(edge.properties[key])}`;
474
+ edgeProps.appendChild(prop);
475
+ }
476
+ li.appendChild(edgeProps);
477
+ }
478
+ list.appendChild(li);
479
+ }
480
+ connSection.appendChild(list);
481
+ }
482
+ panel.appendChild(connSection);
483
+ }
484
+ return {
485
+ show(nodeIds, data) {
486
+ lastData = data;
487
+ // Track history for single-node views
488
+ if (nodeIds.length === 1 && !navigatingHistory) {
489
+ const nodeId = nodeIds[0];
490
+ // Don't push duplicate consecutive entries
491
+ if (history[historyIndex] !== nodeId) {
492
+ if (historyIndex < history.length - 1) {
493
+ history = history.slice(0, historyIndex + 1);
494
+ }
495
+ history.push(nodeId);
496
+ historyIndex = history.length - 1;
497
+ }
498
+ }
499
+ if (nodeIds.length === 1) {
500
+ showSingle(nodeIds[0], data);
501
+ }
502
+ else if (nodeIds.length > 1) {
503
+ showMulti(nodeIds, data);
504
+ }
505
+ },
506
+ hide,
507
+ get visible() {
508
+ return !panel.classList.contains("hidden");
509
+ },
510
+ };
511
+ }
512
+ function createSection(title) {
513
+ const section = document.createElement("div");
514
+ section.className = "info-section";
515
+ const heading = document.createElement("h4");
516
+ heading.className = "info-section-title";
517
+ heading.textContent = title;
518
+ section.appendChild(heading);
519
+ return section;
520
+ }
521
+ function renderValue(value) {
522
+ if (Array.isArray(value)) {
523
+ const container = document.createElement("div");
524
+ container.className = "info-array";
525
+ for (const item of value) {
526
+ const tag = document.createElement("span");
527
+ tag.className = "info-tag";
528
+ tag.textContent = String(item);
529
+ container.appendChild(tag);
530
+ }
531
+ return container;
532
+ }
533
+ if (value !== null && typeof value === "object") {
534
+ const pre = document.createElement("pre");
535
+ pre.className = "info-json";
536
+ pre.textContent = JSON.stringify(value, null, 2);
537
+ return pre;
538
+ }
539
+ const span = document.createElement("span");
540
+ span.className = "info-value";
541
+ span.textContent = String(value ?? "");
542
+ return span;
543
+ }
544
+ function formatValue(value) {
545
+ if (Array.isArray(value))
546
+ return value.map(String).join(", ");
547
+ if (value !== null && typeof value === "object")
548
+ return JSON.stringify(value);
549
+ return String(value ?? "");
550
+ }
551
+ /** Try to parse a string as JSON (number, boolean, array), fall back to string. */
552
+ function tryParseValue(str) {
553
+ const trimmed = str.trim();
554
+ if (trimmed === "true")
555
+ return true;
556
+ if (trimmed === "false")
557
+ return false;
558
+ if (trimmed !== "" && !isNaN(Number(trimmed)))
559
+ return Number(trimmed);
560
+ if ((trimmed.startsWith("[") && trimmed.endsWith("]")) ||
561
+ (trimmed.startsWith("{") && trimmed.endsWith("}"))) {
562
+ try {
563
+ return JSON.parse(trimmed);
564
+ }
565
+ catch {
566
+ return str;
567
+ }
568
+ }
569
+ return str;
570
+ }
571
+ function formatTimestamp(iso) {
572
+ try {
573
+ const d = new Date(iso);
574
+ return d.toLocaleString();
575
+ }
576
+ catch {
577
+ return iso;
578
+ }
579
+ }
@@ -0,0 +1,24 @@
1
+ import type { OntologyData } from "backpack-ontology";
2
+ export interface LayoutNode {
3
+ id: string;
4
+ x: number;
5
+ y: number;
6
+ vx: number;
7
+ vy: number;
8
+ label: string;
9
+ type: string;
10
+ }
11
+ export interface LayoutEdge {
12
+ sourceId: string;
13
+ targetId: string;
14
+ type: string;
15
+ }
16
+ export interface LayoutState {
17
+ nodes: LayoutNode[];
18
+ edges: LayoutEdge[];
19
+ nodeMap: Map<string, LayoutNode>;
20
+ }
21
+ /** Create a layout state from ontology data. Nodes start in a circle. */
22
+ export declare function createLayout(data: OntologyData): LayoutState;
23
+ /** Run one tick of the force simulation. Returns new alpha. */
24
+ export declare function tick(state: LayoutState, alpha: number): number;