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.
Files changed (62) hide show
  1. package/bin/serve.js +159 -396
  2. package/dist/app/assets/index-D-H7agBH.js +12 -0
  3. package/dist/app/assets/index-DE73ngo-.css +1 -0
  4. package/dist/app/assets/index-DFW3OKgJ.js +6 -0
  5. package/dist/app/assets/layout-worker-4xak23M6.js +1 -0
  6. package/dist/app/index.html +2 -2
  7. package/dist/bridge.d.ts +22 -0
  8. package/dist/bridge.js +41 -0
  9. package/dist/canvas.d.ts +15 -0
  10. package/dist/canvas.js +352 -12
  11. package/dist/config.js +10 -0
  12. package/dist/copy-prompt.d.ts +17 -0
  13. package/dist/copy-prompt.js +81 -0
  14. package/dist/default-config.json +6 -1
  15. package/dist/dom-utils.d.ts +46 -0
  16. package/dist/dom-utils.js +57 -0
  17. package/dist/empty-state.js +63 -31
  18. package/dist/extensions/api.d.ts +15 -0
  19. package/dist/extensions/api.js +185 -0
  20. package/dist/extensions/chat/backpack-extension.json +23 -0
  21. package/dist/extensions/chat/src/index.js +32 -0
  22. package/dist/extensions/chat/src/panel.js +306 -0
  23. package/dist/extensions/chat/src/providers/anthropic.js +158 -0
  24. package/dist/extensions/chat/src/providers/types.js +15 -0
  25. package/dist/extensions/chat/src/tools.js +281 -0
  26. package/dist/extensions/chat/style.css +147 -0
  27. package/dist/extensions/event-bus.d.ts +12 -0
  28. package/dist/extensions/event-bus.js +30 -0
  29. package/dist/extensions/loader.d.ts +32 -0
  30. package/dist/extensions/loader.js +71 -0
  31. package/dist/extensions/manifest.d.ts +54 -0
  32. package/dist/extensions/manifest.js +116 -0
  33. package/dist/extensions/panel-mount.d.ts +26 -0
  34. package/dist/extensions/panel-mount.js +377 -0
  35. package/dist/extensions/taskbar.d.ts +29 -0
  36. package/dist/extensions/taskbar.js +64 -0
  37. package/dist/extensions/types.d.ts +182 -0
  38. package/dist/extensions/types.js +8 -0
  39. package/dist/info-panel.d.ts +2 -1
  40. package/dist/info-panel.js +78 -87
  41. package/dist/keybindings.d.ts +1 -1
  42. package/dist/keybindings.js +1 -0
  43. package/dist/layout-worker.d.ts +4 -1
  44. package/dist/layout-worker.js +51 -1
  45. package/dist/layout.d.ts +8 -0
  46. package/dist/layout.js +8 -1
  47. package/dist/main.js +216 -35
  48. package/dist/search.js +1 -1
  49. package/dist/server-api-routes.d.ts +56 -0
  50. package/dist/server-api-routes.js +442 -0
  51. package/dist/server-extensions.d.ts +126 -0
  52. package/dist/server-extensions.js +272 -0
  53. package/dist/server-viewer-state.d.ts +18 -0
  54. package/dist/server-viewer-state.js +33 -0
  55. package/dist/shortcuts.js +6 -2
  56. package/dist/sidebar.js +19 -7
  57. package/dist/style.css +356 -74
  58. package/dist/tools-pane.js +31 -14
  59. package/package.json +4 -3
  60. package/dist/app/assets/index-B3z5bBGl.css +0 -1
  61. package/dist/app/assets/index-CKYlU1zT.js +0 -35
  62. package/dist/app/assets/layout-worker-BZXiBoiC.js +0 -1
package/dist/style.css CHANGED
@@ -30,6 +30,21 @@
30
30
 
31
31
  --shadow: rgba(0, 0, 0, 0.6);
32
32
  --shadow-strong: rgba(0, 0, 0, 0.5);
33
+
34
+ /* Z-tier registry — named layers for everything that overlaps.
35
+ Six tiers, each spaced wide enough that click-to-front shuffling
36
+ within a tier (panels) stays inside its band. New code uses these
37
+ instead of magic numbers; existing rules will be migrated as
38
+ touched. */
39
+ --z-canvas-overlay: 20; /* path bar, focus indicator within canvas */
40
+ --z-panel: 30; /* info-panel + extension panels (click-to-front shuffles 30-39) */
41
+ --z-floating: 40; /* zoom, theme, copy-prompt, sidebar-expand, taskbar slots — hidden when a panel is fullscreen */
42
+ --z-panel-fullscreen: 45; /* a maximized panel covers the secondary floating tier */
43
+ --z-floating-primary: 50; /* search bar — always visible, stays above fullscreen panels so users can keep searching */
44
+ --z-popup: 55; /* context menu, search dropdown */
45
+ --z-modal: 60; /* dialog overlay */
46
+ --z-toast: 70; /* transient notifications */
47
+
33
48
  /* Canvas colors */
34
49
  --canvas-edge: rgba(255, 255, 255, 0.08);
35
50
  --canvas-edge-highlight: rgba(212, 162, 127, 0.5);
@@ -629,16 +644,33 @@ body {
629
644
  top: 16px;
630
645
  left: 16px;
631
646
  right: 16px;
632
- z-index: 30;
647
+ /* No z-index here on purpose — that would create a stacking context
648
+ and trap the children's z-indexes. Each child sets its own. */
633
649
  display: flex;
634
650
  justify-content: space-between;
635
651
  align-items: flex-start;
636
652
  pointer-events: none;
637
653
  }
638
654
 
655
+ /* Children get their own stacking contexts via position:relative + z.
656
+ Top-left and top-right hold "secondary" controls (zoom, theme,
657
+ copy-prompt, sidebar-expand, taskbar slots) and live at the
658
+ floating tier — these get hidden when a panel is fullscreen.
659
+ Top-center holds the search bar at floating-primary so it stays
660
+ visible even when a panel is fullscreen. */
639
661
  .canvas-top-left,
640
- .canvas-top-center,
641
662
  .canvas-top-right {
663
+ position: relative;
664
+ z-index: var(--z-floating);
665
+ pointer-events: auto;
666
+ display: flex;
667
+ align-items: center;
668
+ gap: 4px;
669
+ }
670
+
671
+ .canvas-top-center {
672
+ position: relative;
673
+ z-index: var(--z-floating-primary);
642
674
  pointer-events: auto;
643
675
  display: flex;
644
676
  align-items: center;
@@ -1020,30 +1052,20 @@ body {
1020
1052
  }
1021
1053
 
1022
1054
  /* --- Info Panel --- */
1055
+ /* The outer panel chrome (positioning, border, shadow, drag, fullscreen,
1056
+ close, header buttons) is owned by panel-mount via .extension-panel.
1057
+ Only the body-internal styles live here now. */
1023
1058
 
1024
- .info-panel {
1025
- position: absolute;
1026
- top: 56px;
1027
- right: 16px;
1028
- bottom: 16px;
1029
- width: 360px;
1030
- background: var(--bg-surface);
1031
- border: 1px solid var(--border);
1032
- border-radius: 10px;
1033
- overflow: hidden;
1059
+ .info-panel-content {
1060
+ flex: 1;
1034
1061
  display: flex;
1035
1062
  flex-direction: column;
1036
- padding: 0;
1037
- z-index: 10;
1038
- box-shadow: 0 8px 32px var(--shadow);
1039
- transition: top 0.25s ease, right 0.25s ease, bottom 0.25s ease,
1040
- left 0.25s ease, width 0.25s ease, max-height 0.25s ease,
1041
- border-radius 0.25s ease;
1063
+ min-height: 0;
1042
1064
  }
1043
1065
 
1044
1066
  .info-panel-header {
1045
1067
  flex-shrink: 0;
1046
- padding: 20px 20px 12px;
1068
+ padding: 16px 20px 12px;
1047
1069
  position: relative;
1048
1070
  }
1049
1071
 
@@ -1054,60 +1076,6 @@ body {
1054
1076
  padding: 0 20px 20px;
1055
1077
  }
1056
1078
 
1057
- .info-panel.hidden {
1058
- display: none;
1059
- }
1060
-
1061
- .info-panel.info-panel-maximized {
1062
- top: 0;
1063
- right: 0;
1064
- bottom: 0;
1065
- left: 0;
1066
- width: auto;
1067
- max-height: none;
1068
- border-radius: 0;
1069
- z-index: 40;
1070
- }
1071
-
1072
- /* --- Toolbar (back, forward, maximize, close) --- */
1073
-
1074
- .info-panel-toolbar {
1075
- position: absolute;
1076
- top: 12px;
1077
- right: 14px;
1078
- display: flex;
1079
- align-items: center;
1080
- gap: 2px;
1081
- z-index: 1;
1082
- }
1083
-
1084
- .info-toolbar-btn {
1085
- background: none;
1086
- border: none;
1087
- color: var(--text-muted);
1088
- font-size: 16px;
1089
- cursor: pointer;
1090
- padding: 4px 6px;
1091
- line-height: 1;
1092
- border-radius: 4px;
1093
- transition: color 0.15s, background 0.15s;
1094
- }
1095
-
1096
- .info-toolbar-btn:hover:not(:disabled) {
1097
- color: var(--text);
1098
- background: var(--bg-hover);
1099
- }
1100
-
1101
- .info-toolbar-btn:disabled {
1102
- color: var(--text-dim);
1103
- cursor: default;
1104
- opacity: 0.3;
1105
- }
1106
-
1107
- .info-close-btn {
1108
- font-size: 20px;
1109
- }
1110
-
1111
1079
  /* --- Clickable connections --- */
1112
1080
 
1113
1081
  .info-connection-link {
@@ -2338,7 +2306,7 @@ body {
2338
2306
  bottom: 16px;
2339
2307
  left: 50%;
2340
2308
  transform: translateX(-50%);
2341
- z-index: 25;
2309
+ z-index: var(--z-canvas-overlay);
2342
2310
  display: flex;
2343
2311
  align-items: center;
2344
2312
  gap: 4px;
@@ -2420,3 +2388,317 @@ body {
2420
2388
  0%, 100% { opacity: 0.6; border-color: rgba(212, 162, 127, 0.3); }
2421
2389
  50% { opacity: 1; border-color: rgba(212, 162, 127, 0.8); }
2422
2390
  }
2391
+
2392
+ /* --- Copy Prompt Button --- */
2393
+
2394
+ .copy-prompt-btn {
2395
+ background: var(--bg-surface);
2396
+ border: 1px solid var(--border);
2397
+ border-radius: 8px;
2398
+ color: var(--text-muted);
2399
+ font-size: 12px;
2400
+ cursor: pointer;
2401
+ padding: 6px 10px;
2402
+ line-height: 1;
2403
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
2404
+ box-shadow: 0 2px 8px var(--shadow);
2405
+ }
2406
+
2407
+ .copy-prompt-btn:hover {
2408
+ color: var(--text);
2409
+ border-color: var(--text-muted);
2410
+ background: var(--bg-hover);
2411
+ }
2412
+
2413
+ /* --- Extension taskbar slots --- */
2414
+
2415
+ /* Top slots flank the search bar inside .canvas-top-center. They live
2416
+ alongside the search overlay so extensions can flank it on either
2417
+ side without polluting the viewer's own top-left/top-right controls
2418
+ (zoom, theme, copy-prompt, tools toggle). Hidden when empty so they
2419
+ contribute no width and the search bar stays centered. */
2420
+ .ext-slot-top-left,
2421
+ .ext-slot-top-right {
2422
+ display: none;
2423
+ align-items: center;
2424
+ gap: 4px;
2425
+ }
2426
+
2427
+ .ext-slot-top-left.has-icons,
2428
+ .ext-slot-top-right.has-icons {
2429
+ display: inline-flex;
2430
+ }
2431
+
2432
+ /* Bottom slots float in the canvas corners. Hidden until populated so
2433
+ empty corners don't take any space. The bottom-center area is left
2434
+ clear for the path bar — see .path-bar above. */
2435
+ .ext-slot-bottom-left,
2436
+ .ext-slot-bottom-right {
2437
+ position: absolute;
2438
+ bottom: 16px;
2439
+ z-index: var(--z-floating);
2440
+ display: none;
2441
+ flex-direction: row;
2442
+ align-items: center;
2443
+ gap: 6px;
2444
+ padding: 6px;
2445
+ background: var(--bg-surface);
2446
+ border: 1px solid var(--border);
2447
+ border-radius: 12px;
2448
+ box-shadow: 0 4px 16px var(--shadow);
2449
+ pointer-events: auto;
2450
+ }
2451
+
2452
+ .ext-slot-bottom-left {
2453
+ left: 16px;
2454
+ }
2455
+
2456
+ .ext-slot-bottom-right {
2457
+ right: 16px;
2458
+ }
2459
+
2460
+ .ext-slot-bottom-left.has-icons,
2461
+ .ext-slot-bottom-right.has-icons {
2462
+ display: flex;
2463
+ }
2464
+
2465
+ /* Icon button — same look in every slot. In top slots it sits
2466
+ alongside the viewer's own buttons (zoom, theme, copy-prompt) and
2467
+ matches their visual weight. In bottom slots it lives inside the
2468
+ floating pill container styled above. */
2469
+ .extension-taskbar-icon {
2470
+ display: inline-flex;
2471
+ align-items: center;
2472
+ gap: 6px;
2473
+ background: var(--bg-surface);
2474
+ border: 1px solid var(--border);
2475
+ border-radius: 8px;
2476
+ color: var(--text-muted);
2477
+ font-size: 12px;
2478
+ font-weight: 500;
2479
+ font-family: inherit;
2480
+ cursor: pointer;
2481
+ padding: 6px 10px;
2482
+ line-height: 1;
2483
+ box-shadow: 0 2px 8px var(--shadow);
2484
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
2485
+ }
2486
+
2487
+ /* Inside the bottom floating pill, drop the per-icon shadow and
2488
+ border so the pill container's chrome wraps the group cleanly. */
2489
+ .ext-slot-bottom-left .extension-taskbar-icon,
2490
+ .ext-slot-bottom-right .extension-taskbar-icon {
2491
+ background: transparent;
2492
+ border-color: transparent;
2493
+ box-shadow: none;
2494
+ }
2495
+
2496
+ .extension-taskbar-icon:hover {
2497
+ color: var(--text);
2498
+ border-color: var(--text-muted);
2499
+ background: var(--bg-hover);
2500
+ }
2501
+
2502
+ .ext-slot-bottom-left .extension-taskbar-icon:hover,
2503
+ .ext-slot-bottom-right .extension-taskbar-icon:hover {
2504
+ border-color: var(--border);
2505
+ }
2506
+
2507
+ .extension-taskbar-icon-symbol {
2508
+ font-size: 11px;
2509
+ opacity: 0.7;
2510
+ }
2511
+
2512
+ .extension-taskbar-icon-label {
2513
+ font-size: 12px;
2514
+ }
2515
+
2516
+ /* --- Extension Panel Layer + Panels --- */
2517
+
2518
+ /* Extension panel layer is the parent container that holds all
2519
+ panels (info-panel + extension panels). It deliberately does NOT
2520
+ set a z-index — that would create a stacking context and trap
2521
+ the panels' inline z-indexes inside it, preventing fullscreen
2522
+ panels from rising above the top bar. Each panel sets its own
2523
+ z-index via JS (panel tier base + click-to-front offset, or
2524
+ --z-panel-fullscreen when maximized). The layer's only job is
2525
+ to be a positioned container with pointer-events:none so empty
2526
+ regions don't block the canvas. */
2527
+ .extension-panel-layer {
2528
+ position: absolute;
2529
+ inset: 0;
2530
+ pointer-events: none;
2531
+ }
2532
+
2533
+ /* Each panel is positioned via inline left/top from JS so it can be
2534
+ dragged. The class only handles the look, not the position — the
2535
+ default coordinates are computed at mount time. */
2536
+ .extension-panel {
2537
+ position: absolute;
2538
+ display: flex;
2539
+ flex-direction: column;
2540
+ background: var(--bg-surface);
2541
+ border: 1px solid var(--border);
2542
+ border-radius: 12px;
2543
+ box-shadow: 0 8px 32px var(--shadow);
2544
+ overflow: hidden;
2545
+ pointer-events: auto;
2546
+ width: 380px;
2547
+ max-height: calc(100vh - 120px);
2548
+ /* Panels start in the panel tier; click-to-front bumps individual
2549
+ panels by setting style.zIndex inline (always >= --z-panel). */
2550
+ }
2551
+
2552
+ /* Fullscreen takes over the canvas area entirely, suspending the
2553
+ inline left/top until exited. The inline z-index is bumped to
2554
+ --z-panel-fullscreen by JS so the panel covers the top bar. */
2555
+ .extension-panel.is-fullscreen {
2556
+ top: 0 !important;
2557
+ left: 0 !important;
2558
+ right: 0;
2559
+ bottom: 0;
2560
+ width: auto;
2561
+ max-height: none;
2562
+ border-radius: 0;
2563
+ }
2564
+
2565
+ /* setVisible(false) toggles this — display:none keeps the panel
2566
+ instance alive (for hideOnClose / persistent panels). */
2567
+ .extension-panel.is-hidden {
2568
+ display: none;
2569
+ }
2570
+
2571
+ /* When any panel is fullscreen, the panel-mount adds this class to
2572
+ #canvas-container. We hide secondary controls (zoom, theme,
2573
+ copy-prompt, sidebar-expand, taskbar slots) so they don't visually
2574
+ conflict with the panel's own chrome buttons. The search bar in
2575
+ .canvas-top-center is intentionally NOT hidden — users can keep
2576
+ searching and selecting nodes while a panel is fullscreen. */
2577
+ #canvas-container.has-fullscreen-panel .canvas-top-left,
2578
+ #canvas-container.has-fullscreen-panel .canvas-top-right,
2579
+ #canvas-container.has-fullscreen-panel .ext-slot-bottom-left,
2580
+ #canvas-container.has-fullscreen-panel .ext-slot-bottom-right,
2581
+ #canvas-container.has-fullscreen-panel .ext-slot-top-left,
2582
+ #canvas-container.has-fullscreen-panel .ext-slot-top-right {
2583
+ display: none;
2584
+ }
2585
+
2586
+ /* In fullscreen, the panel takes over the WHOLE canvas (top: 0 to
2587
+ bottom: 0). The search bar slides to the top of the canvas and
2588
+ visually meshes into the panel's surface — its parent containers
2589
+ (.canvas-top-bar / .canvas-top-center / .search-overlay) have no
2590
+ background, so the panel's --bg-surface extends up underneath the
2591
+ search input and reads as one continuous slab.
2592
+
2593
+ The panel's chrome header gets padding-top equal to the search bar
2594
+ height so the title + buttons appear below where the search bar
2595
+ sits. In normal (non-fullscreen) mode the top bar keeps its
2596
+ existing top: 16px so widgets align horizontally as before. */
2597
+ #canvas-container.has-fullscreen-panel .canvas-top-bar {
2598
+ top: 0;
2599
+ }
2600
+
2601
+ #canvas-container.has-fullscreen-panel .extension-panel.is-fullscreen .extension-panel-header {
2602
+ padding-top: 56px;
2603
+ }
2604
+
2605
+ /* Drop the panel's chrome (border, rounded corners, shadow) when
2606
+ fullscreen so it visually sits flush against the canvas edges
2607
+ instead of looking like a window with a frame. */
2608
+ .extension-panel.is-fullscreen {
2609
+ border: none;
2610
+ box-shadow: none;
2611
+ }
2612
+
2613
+ /* Header doubles as the drag handle. Cursor hint + grab-state, but
2614
+ buttons inside it stop propagation so clicking close/fullscreen
2615
+ doesn't start a drag. */
2616
+ .extension-panel-header {
2617
+ display: flex;
2618
+ align-items: center;
2619
+ gap: 6px;
2620
+ padding: 8px 10px;
2621
+ border-bottom: 1px solid var(--border);
2622
+ background: var(--bg-surface);
2623
+ flex-shrink: 0;
2624
+ cursor: grab;
2625
+ user-select: none;
2626
+ }
2627
+
2628
+ .extension-panel-header:active {
2629
+ cursor: grabbing;
2630
+ }
2631
+
2632
+ .extension-panel.is-fullscreen .extension-panel-header {
2633
+ cursor: default;
2634
+ }
2635
+
2636
+ .extension-panel-title {
2637
+ flex: 1;
2638
+ font-size: 13px;
2639
+ font-weight: 500;
2640
+ color: var(--text);
2641
+ white-space: nowrap;
2642
+ overflow: hidden;
2643
+ text-overflow: ellipsis;
2644
+ }
2645
+
2646
+ /* Custom-button container — small flex group for extension-supplied
2647
+ header buttons. Sits between the title and the built-in fullscreen +
2648
+ close controls. */
2649
+ .extension-panel-custom-btns {
2650
+ display: flex;
2651
+ gap: 4px;
2652
+ }
2653
+
2654
+ /* Header buttons (custom + fullscreen + close) all share one look so
2655
+ the chrome reads as a single row of controls. Matches the visual
2656
+ weight of info-panel's toolbar buttons. */
2657
+ .extension-panel-btn {
2658
+ display: inline-flex;
2659
+ align-items: center;
2660
+ justify-content: center;
2661
+ background: none;
2662
+ border: 1px solid var(--border);
2663
+ border-radius: 6px;
2664
+ color: var(--text-muted);
2665
+ font-size: 12px;
2666
+ font-family: inherit;
2667
+ cursor: pointer;
2668
+ padding: 3px 8px;
2669
+ line-height: 1;
2670
+ min-height: 22px;
2671
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
2672
+ }
2673
+
2674
+ .extension-panel-btn:hover:not(:disabled) {
2675
+ color: var(--text);
2676
+ background: var(--bg-hover);
2677
+ border-color: var(--text-muted);
2678
+ }
2679
+
2680
+ .extension-panel-btn:disabled {
2681
+ opacity: 0.4;
2682
+ cursor: not-allowed;
2683
+ }
2684
+
2685
+ .extension-panel-btn-fullscreen {
2686
+ padding: 2px 6px;
2687
+ }
2688
+
2689
+ .extension-panel-btn-fullscreen svg {
2690
+ display: block;
2691
+ }
2692
+
2693
+ .extension-panel-btn-close {
2694
+ font-size: 16px;
2695
+ padding: 0 7px;
2696
+ }
2697
+
2698
+ .extension-panel-body {
2699
+ flex: 1;
2700
+ overflow: hidden;
2701
+ display: flex;
2702
+ flex-direction: column;
2703
+ }
2704
+
@@ -1,5 +1,6 @@
1
1
  import { showPrompt, showConfirm } from "./dialog";
2
2
  import { getColor } from "./colors";
3
+ import { makeSvgIcon, snapshotChildren, restoreChildren } from "./dom-utils";
3
4
  export function initToolsPane(container, callbacks) {
4
5
  let data = null;
5
6
  let stats = null;
@@ -50,8 +51,11 @@ export function initToolsPane(container, callbacks) {
50
51
  const toggle = document.createElement("button");
51
52
  toggle.className = "tools-pane-toggle hidden";
52
53
  toggle.title = "Graph Inspector";
53
- toggle.innerHTML =
54
- '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h10"/></svg>';
54
+ toggle.appendChild(makeSvgIcon({ size: 16, strokeLinecap: "round", strokeLinejoin: "round" }, [
55
+ { tag: "path", attrs: { d: "M4 7h16" } },
56
+ { tag: "path", attrs: { d: "M4 12h16" } },
57
+ { tag: "path", attrs: { d: "M4 17h10" } },
58
+ ]));
55
59
  const content = document.createElement("div");
56
60
  content.className = "tools-pane-content hidden";
57
61
  container.appendChild(toggle);
@@ -65,16 +69,25 @@ export function initToolsPane(container, callbacks) {
65
69
  });
66
70
  // --- Render ---
67
71
  function render() {
68
- content.innerHTML = "";
72
+ content.replaceChildren();
69
73
  if (!stats)
70
74
  return;
71
75
  // Graph stats summary (always visible)
72
76
  const summary = document.createElement("div");
73
77
  summary.className = "tools-pane-summary";
74
- summary.innerHTML =
75
- `<span>${stats.nodeCount} nodes</span><span class="tools-pane-sep">&middot;</span>` +
76
- `<span>${stats.edgeCount} edges</span><span class="tools-pane-sep">&middot;</span>` +
77
- `<span>${stats.types.length} types</span>`;
78
+ const nodesEl = document.createElement("span");
79
+ nodesEl.textContent = `${stats.nodeCount} nodes`;
80
+ const sep1 = document.createElement("span");
81
+ sep1.className = "tools-pane-sep";
82
+ sep1.textContent = "\u00b7";
83
+ const edgesEl = document.createElement("span");
84
+ edgesEl.textContent = `${stats.edgeCount} edges`;
85
+ const sep2 = document.createElement("span");
86
+ sep2.className = "tools-pane-sep";
87
+ sep2.textContent = "\u00b7";
88
+ const typesEl = document.createElement("span");
89
+ typesEl.textContent = `${stats.types.length} types`;
90
+ summary.append(nodesEl, sep1, edgesEl, sep2, typesEl);
78
91
  content.appendChild(summary);
79
92
  // Token efficiency card
80
93
  if (data && stats.nodeCount > 0) {
@@ -162,7 +175,7 @@ export function initToolsPane(container, callbacks) {
162
175
  const tabContainer = content.querySelector(".tools-pane-tab-content");
163
176
  if (!tabContainer)
164
177
  return;
165
- tabContainer.innerHTML = "";
178
+ tabContainer.replaceChildren();
166
179
  if (activeTab === "types") {
167
180
  renderTypesTab(tabContainer);
168
181
  }
@@ -897,13 +910,17 @@ export function initToolsPane(container, callbacks) {
897
910
  input.className = "tools-pane-inline-input";
898
911
  input.value = currentValue;
899
912
  input.type = "text";
900
- // Replace row content with input
901
- const original = row.innerHTML;
902
- row.innerHTML = "";
913
+ // Snapshot the row's children so we can restore them on cancel
914
+ // without re-parsing HTML. Cloning is safe — the snapshotted markup
915
+ // is static (text + spans), no event handlers to lose.
916
+ const originalChildren = snapshotChildren(row);
917
+ row.replaceChildren(input);
903
918
  row.classList.add("tools-pane-editing");
904
- row.appendChild(input);
905
919
  input.focus();
906
920
  input.select();
921
+ function restore() {
922
+ restoreChildren(row, originalChildren);
923
+ }
907
924
  function commit() {
908
925
  const newValue = input.value.trim();
909
926
  row.classList.remove("tools-pane-editing");
@@ -911,7 +928,7 @@ export function initToolsPane(container, callbacks) {
911
928
  onCommit(newValue);
912
929
  }
913
930
  else {
914
- row.innerHTML = original;
931
+ restore();
915
932
  }
916
933
  }
917
934
  input.addEventListener("keydown", (e) => {
@@ -920,7 +937,7 @@ export function initToolsPane(container, callbacks) {
920
937
  commit();
921
938
  }
922
939
  if (e.key === "Escape") {
923
- row.innerHTML = original;
940
+ restore();
924
941
  row.classList.remove("tools-pane-editing");
925
942
  }
926
943
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backpack-viewer",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Web-based graph visualizer for backpack-ontology — Canvas 2D, force-directed layout, live reload",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Noah Irzinger",
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "dev": "vite",
17
- "build": "rm -rf dist && tsc && cp src/style.css dist/ && vite build",
17
+ "build": "rm -rf dist && tsc && tsc -p tsconfig.extensions.json && cp src/style.css dist/ && node scripts/build-extensions.js && vite build",
18
18
  "preview": "vite preview",
19
19
  "serve": "node bin/serve.js",
20
20
  "prepare": "npm run build",
@@ -23,7 +23,8 @@
23
23
  "release:major": "npm version major && git push && git push --tags"
24
24
  },
25
25
  "dependencies": {
26
- "backpack-ontology": "^0.5.0"
26
+ "age-encryption": "^0.3.0",
27
+ "backpack-ontology": "^0.7.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/node": "^25.5.0",
@@ -1 +0,0 @@
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);--canvas-walk-edge: #e8d5c4}[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);--canvas-walk-edge: #1a1a1a}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);border-left:3px solid var(--backpack-color, transparent);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-section-heading{font-size:10px;font-weight:600;color:var(--text-dim);letter-spacing:.08em;margin:16px 12px 6px}.remote-list{list-style:none;padding:0;margin:0}.ontology-item-remote .name{display:inline}.remote-name-row{display:flex;align-items:center;gap:8px}.remote-badge{font-size:9px;font-weight:600;color:var(--text-dim);background:var(--bg-hover);padding:1px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--border)}.remote-source{display:block;font-size:10px;color:var(--text-dim);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.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}.sidebar-stale-banner{background:#fff3cd;color:#5c3a00;border:1px solid #e6c263;border-radius:6px;padding:10px 12px;margin-bottom:10px;font-size:11px}.sidebar-stale-banner-title{font-weight:600;margin-bottom:2px}.sidebar-stale-banner-subtitle{opacity:.85;margin-bottom:6px}.sidebar-stale-banner-hint{background:#00000014;border-radius:4px;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;line-height:1.4;margin:0;white-space:pre-wrap;word-break:break-all}.backpack-picker-container{position:relative;margin-bottom:10px}.backpack-picker-pill{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:11px;font-family:inherit;cursor:pointer;text-align:left}.backpack-picker-pill:hover{border-color:var(--backpack-color, var(--accent))}.backpack-picker-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--backpack-color, var(--accent));flex-shrink:0}.backpack-picker-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}.backpack-picker-caret{opacity:.6;font-size:10px}.backpack-picker-dropdown{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px var(--shadow);z-index:50;max-height:300px;overflow-y:auto;padding:4px}.backpack-picker-item{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;background:transparent;border:none;border-radius:4px;color:var(--fg);font-family:inherit;font-size:11px;cursor:pointer;text-align:left}.backpack-picker-item:hover{background:var(--bg-hover)}.backpack-picker-item.active{background:var(--bg-hover);font-weight:600}.backpack-picker-item-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--backpack-color, var(--accent));flex-shrink:0}.backpack-picker-item-name{flex-shrink:0}.backpack-picker-item-path{flex:1;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;text-align:right}.backpack-picker-divider{height:1px;background:var(--border);margin:4px 0}.backpack-picker-add{opacity:.75;font-style:italic}.sidebar-lock-badge{font-size:10px;color:#c08c00;display:none;margin-top:2px}.sidebar-lock-badge.active{display:block}.sidebar-lock-badge.active:before{content:"● ";color:#c08c00}.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}.sidebar-snippets{margin-top:4px;padding-left:8px}.sidebar-snippet{display:flex;align-items:center;justify-content:space-between;padding:2px 4px;border-radius:4px;cursor:pointer}.sidebar-snippet:hover{background:var(--bg-hover)}.sidebar-snippet-label{font-size:10px;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-snippet-delete{background:none;border:none;color:var(--text-dim);font-size:12px;cursor:pointer;padding:0 2px;opacity:0}.sidebar-snippet:hover .sidebar-snippet-delete{opacity:1}.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)}.sidebar-version{font-size:9px;color:var(--text-dim);opacity:.5;margin-top:4px}.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)}.node-tooltip{position:absolute;pointer-events:none;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:12px;white-space:nowrap;z-index:20;box-shadow:0 2px 8px #00000026;opacity:.95}#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-badge-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.info-empty-message{font-size:12px;color:var(--text-dim)}.share-list-message{font-size:13px;color:var(--text-dim);text-align:center;padding:12px}.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-token-card{padding:8px 10px;margin-bottom:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-hover);font-size:11px}.token-card-label{font-weight:600;color:var(--text);margin-bottom:4px}.token-card-stat{color:var(--text-muted);margin-bottom:4px}.token-card-bar{height:4px;background:var(--border);border-radius:2px;margin:6px 0;overflow:hidden}.token-card-bar-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .3s ease}.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-actions{display:flex;gap:6px;padding-top:4px}.tools-pane-action-btn{background:none;border:1px solid var(--border);color:var(--text-muted);font-size:10px;padding:2px 8px;border-radius:3px;cursor:pointer;transition:color .1s,border-color .1s}.tools-pane-action-btn:hover{color:var(--accent);border-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;overflow:hidden}.empty-state.hidden{display:none}.empty-state-bg{position:absolute;top:0;right:0;bottom:0;left:0;overflow:hidden}.empty-state-circle{position:absolute;border-radius:50%;background:var(--accent);opacity:.07}.empty-state-circle.c1{width:80px;height:80px;left:20%;top:15%;animation:float-circle 8s ease-in-out infinite}.empty-state-circle.c2{width:50px;height:50px;right:25%;top:25%;animation:float-circle 6s ease-in-out 1s infinite}.empty-state-circle.c3{width:65px;height:65px;left:55%;bottom:20%;animation:float-circle 7s ease-in-out 2s infinite}.empty-state-circle.c4{width:40px;height:40px;left:15%;bottom:30%;animation:float-circle 9s ease-in-out .5s infinite}.empty-state-circle.c5{width:55px;height:55px;right:15%;bottom:35%;animation:float-circle 7.5s ease-in-out 1.5s infinite}.empty-state-lines{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;color:var(--text-dim);animation:float-circle 10s ease-in-out infinite}@keyframes float-circle{0%,to{transform:translateY(0)}50%{transform:translateY(-12px)}}.empty-state-content{text-align:center;max-width:420px;padding:40px 24px;position:relative;z-index:1}.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-label{display:block;font-size:11px;color:var(--text-muted);margin-bottom:4px;margin-top:8px;text-transform:uppercase;letter-spacing:.04em}.bp-dialog-path-row{display:flex;gap:8px;margin-bottom:4px}.bp-dialog-path-input{flex:1;margin-bottom:0}.bp-dialog-browse-btn{flex-shrink:0;padding:8px 14px;font-size:12px}.bp-dialog-browse-btn:disabled{opacity:.4;cursor:not-allowed}.bp-dialog-path-input.bp-dialog-drag-over{border-color:var(--accent);background:var(--bg-hover)}.bp-dialog-picker-hint{font-size:11px;color:var(--text-muted);min-height:1em;margin-bottom:8px}.bp-dialog-activate-row{display:flex;align-items:center;gap:8px;margin-top:8px;margin-bottom:12px;font-size:12px;color:var(--text-muted)}.bp-dialog-activate-row input[type=checkbox]{margin:0}.bp-dialog-activate-row label{cursor:pointer}.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)}.context-menu{position:absolute;z-index:100;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:4px;min-width:180px;box-shadow:0 8px 24px var(--shadow)}.context-menu-item{padding:6px 12px;font-size:12px;color:var(--text);border-radius:4px;cursor:pointer;white-space:nowrap}.context-menu-item:hover{background:var(--bg-hover)}.context-menu-separator{height:1px;background:var(--border);margin:4px 8px}.path-bar{position:absolute;bottom:16px;left:50%;transform:translate(-50%);z-index:25;display:flex;align-items:center;gap:4px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;padding:6px 12px;box-shadow:0 4px 16px var(--shadow);max-width:80%;overflow-x:auto}.path-bar.hidden{display:none}.path-bar-node{font-size:11px;color:var(--text);padding:2px 8px;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0}.path-bar-node:hover{background:var(--bg-hover)}.path-bar-edge{font-size:9px;color:var(--text-dim);white-space:nowrap;flex-shrink:0}.path-bar-close{background:none;border:none;color:var(--text-dim);font-size:14px;cursor:pointer;padding:0 4px;margin-left:8px;flex-shrink:0}.path-bar-close:hover{color:var(--text)}.walk-trail-edge{font-size:9px;color:var(--text-dim);padding:1px 0 1px 24px;opacity:.7}.walk-indicator{font-size:10px;color:var(--accent);padding:2px 8px;border:1px solid rgba(212,162,127,.4);border-radius:4px;cursor:pointer;opacity:.4}.walk-indicator.active{opacity:1;background:#d4a27f26;animation:walk-strobe 2s ease-in-out infinite}@keyframes walk-strobe{0%,to{opacity:.6;border-color:#d4a27f4d}50%{opacity:1;border-color:#d4a27fcc}}