@deltarobotics/uhd-viewer 0.2.0 → 0.2.1

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 (31) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/app/assets/index-CEJyT8hF.css +2 -0
  3. package/dist/app/assets/{index-CkpIRYgM.js → index-DWZ-48wc.js} +35 -35
  4. package/dist/app/index.html +2 -2
  5. package/dist/components/components.css +320 -56
  6. package/dist/components/index.js +1166 -605
  7. package/dist/components/index.js.map +1 -1
  8. package/dist/components/types/uhd-viewer/src/cad/ModelView.d.ts +3 -1
  9. package/dist/components/types/uhd-viewer/src/canvas/Canvas.d.ts +5 -1
  10. package/dist/components/types/uhd-viewer/src/canvas/interaction.d.ts +2 -2
  11. package/dist/components/types/uhd-viewer/src/canvas/reveal.d.ts +18 -0
  12. package/dist/components/types/uhd-viewer/src/chrome/Chrome.d.ts +12 -2
  13. package/dist/components/types/uhd-viewer/src/chrome/InterfaceCards.d.ts +11 -1
  14. package/dist/components/types/uhd-viewer/src/chrome/Sections.d.ts +22 -1
  15. package/dist/components/types/uhd-viewer/src/components.d.ts +4 -4
  16. package/dist/components/types/uhd-viewer/src/host/targets.d.ts +16 -0
  17. package/dist/components/types/uhd-viewer/src/model/classification.d.ts +3 -0
  18. package/dist/components/types/uhd-viewer/src/model/from-uhd.d.ts +3 -2
  19. package/dist/components/types/uhd-viewer/src/model/hierarchy.d.ts +10 -6
  20. package/dist/components/types/uhd-viewer/src/model/level.d.ts +7 -0
  21. package/dist/components/types/uhd-viewer/src/model/slots.d.ts +2 -0
  22. package/dist/components/types/uhd-viewer/src/ui/Legend.d.ts +23 -4
  23. package/dist/components/types/uhd-viewer/src/ui/legendFit.d.ts +26 -13
  24. package/dist/components/types/uhd-viewer/src/viewer/controller.d.ts +1 -0
  25. package/dist/components/types/uhd-viewer/src/viewer/hierarchy.d.ts +34 -12
  26. package/dist/components/types/uhd-viewer/src/viewer/views.d.ts +49 -18
  27. package/dist/headless/index.js.map +1 -1
  28. package/dist/lib/uhd-viewer.css +1 -1
  29. package/dist/lib/uhd-viewer.js +11291 -10973
  30. package/package.json +1 -1
  31. package/dist/app/assets/index-Boi2erhd.css +0 -2
@@ -1,6 +1,6 @@
1
- import { Component, StrictMode, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
1
+ import { Component, Fragment, StrictMode, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { Background, BackgroundVariant, BaseEdge, ConnectionMode, EdgeLabelRenderer, Handle, Position, ReactFlow, ReactFlowProvider, useNodesInitialized, useReactFlow, useStore } from "@xyflow/react";
5
5
  import * as Radix from "@radix-ui/react-scroll-area";
6
6
  import { icons } from "lucide-react";
@@ -7417,6 +7417,16 @@ function systemDiagnostics(root, lookup) {
7417
7417
  return out;
7418
7418
  }
7419
7419
  //#endregion
7420
+ //#region src/model/level.ts
7421
+ /**
7422
+ * The modules a level draws: its nodes less what is not a module of its own
7423
+ * (the drilled-into module's boundary ports, a board's net junctions and
7424
+ * their taps). A level with none is empty, whatever ports it shows.
7425
+ */
7426
+ function levelModules(nodes) {
7427
+ return nodes.filter((n) => !n.sheetPort && !n.net && !n.tapOf);
7428
+ }
7429
+ //#endregion
7420
7430
  //#region src/model/classification.ts
7421
7431
  var CLASSIFICATIONS = [
7422
7432
  {
@@ -7487,64 +7497,76 @@ function classKey(item, mode) {
7487
7497
  var DRC_LEGEND = [
7488
7498
  {
7489
7499
  key: "configured",
7490
- label: "CONFIGURED",
7500
+ label: "Configured",
7501
+ meaning: "No findings",
7491
7502
  color: DRC_COLORS.configured
7492
7503
  },
7493
7504
  {
7494
7505
  key: "partial",
7495
- label: "PARTIAL",
7506
+ label: "Partial",
7507
+ meaning: "Warnings",
7496
7508
  color: DRC_COLORS.partial
7497
7509
  },
7498
7510
  {
7499
7511
  key: "incompatible",
7500
- label: "INCOMPATIBLE",
7512
+ label: "Incompatible",
7513
+ meaning: "Errors",
7501
7514
  color: DRC_COLORS.incompatible
7502
7515
  },
7503
7516
  {
7504
7517
  key: "unconfigured",
7505
- label: "NOT CONFIGURED",
7518
+ label: "Not configured",
7519
+ meaning: "Nothing linked",
7506
7520
  color: DRC_COLORS.unconfigured
7507
7521
  }
7508
7522
  ];
7509
7523
  var SIGNAL_LEGEND = [
7510
7524
  {
7511
7525
  key: "power",
7512
- label: "POWER",
7526
+ label: "Power",
7527
+ meaning: "Supply rails",
7513
7528
  color: SIGNAL_COLORS.power
7514
7529
  },
7515
7530
  {
7516
7531
  key: "ground",
7517
- label: "GROUND",
7532
+ label: "Ground",
7533
+ meaning: "Return paths",
7518
7534
  color: SIGNAL_COLORS.ground
7519
7535
  },
7520
7536
  {
7521
7537
  key: "phase",
7522
- label: "MOTOR PHASE",
7538
+ label: "Motor phase",
7539
+ meaning: "ESC to motor windings",
7523
7540
  color: SIGNAL_COLORS.bldc_3phase
7524
7541
  },
7525
7542
  {
7526
7543
  key: "pwm",
7527
- label: "DSHOT / ESC",
7544
+ label: "DShot / ESC",
7545
+ meaning: "Motor control",
7528
7546
  color: SIGNAL_COLORS.dshot
7529
7547
  },
7530
7548
  {
7531
7549
  key: "uart",
7532
7550
  label: "UART / CRSF",
7551
+ meaning: "Serial links",
7533
7552
  color: SIGNAL_COLORS.uart
7534
7553
  },
7535
7554
  {
7536
7555
  key: "i2c",
7537
7556
  label: "I2C",
7557
+ meaning: "Sensor bus",
7538
7558
  color: SIGNAL_COLORS.i2c
7539
7559
  },
7540
7560
  {
7541
7561
  key: "rf",
7542
7562
  label: "RF",
7563
+ meaning: "Antenna feeds",
7543
7564
  color: SIGNAL_COLORS.rf
7544
7565
  },
7545
7566
  {
7546
7567
  key: "other-domains",
7547
- label: "OTHER DOMAINS",
7568
+ label: "Other domains",
7569
+ meaning: "Not electrical",
7548
7570
  color: "var(--uhd-domain-other)"
7549
7571
  }
7550
7572
  ];
@@ -7553,68 +7575,81 @@ var SIGNAL_EXTRA = [
7553
7575
  {
7554
7576
  key: "spi",
7555
7577
  label: "SPI",
7578
+ meaning: "Peripheral bus",
7556
7579
  color: SIGNAL_COLORS.spi
7557
7580
  },
7558
7581
  {
7559
7582
  key: "analog",
7560
- label: "ANALOG",
7583
+ label: "Analog",
7584
+ meaning: "Analog signals",
7561
7585
  color: SIGNAL_COLORS.analog
7562
7586
  },
7563
7587
  {
7564
7588
  key: "digital",
7565
- label: "DIGITAL",
7589
+ label: "Digital",
7590
+ meaning: "Logic I/O",
7566
7591
  color: SIGNAL_COLORS.digital
7567
7592
  },
7568
7593
  {
7569
7594
  key: "can",
7570
7595
  label: "CAN",
7596
+ meaning: "Field bus",
7571
7597
  color: SIGNAL_COLORS.can
7572
7598
  },
7573
7599
  {
7574
7600
  key: "other",
7575
- label: "OTHER SIGNALS",
7601
+ label: "Other signals",
7602
+ meaning: "Unclassified protocols",
7576
7603
  color: "var(--uhd-signal-other)"
7577
7604
  }
7578
7605
  ];
7579
7606
  var DOMAIN_LEGEND = [
7580
7607
  {
7581
7608
  key: "electrical",
7582
- label: "ELECTRICAL",
7609
+ label: "Electrical",
7610
+ meaning: "Power and signals",
7583
7611
  color: DOMAIN_COLORS.electrical
7584
7612
  },
7585
7613
  {
7586
7614
  key: "mechanical",
7587
- label: "MECHANICAL",
7615
+ label: "Mechanical",
7616
+ meaning: "Mounts and fasteners",
7588
7617
  color: DOMAIN_COLORS.mechanical
7589
7618
  },
7590
7619
  {
7591
7620
  key: "pneumatic",
7592
- label: "PNEUMATIC",
7621
+ label: "Pneumatic",
7622
+ meaning: "Air lines",
7593
7623
  color: DOMAIN_COLORS.pneumatic
7594
7624
  },
7595
7625
  {
7596
7626
  key: "hydraulic",
7597
- label: "HYDRAULIC",
7627
+ label: "Hydraulic",
7628
+ meaning: "Fluid lines",
7598
7629
  color: DOMAIN_COLORS.hydraulic
7599
7630
  },
7600
7631
  {
7601
7632
  key: "thermal",
7602
- label: "THERMAL",
7633
+ label: "Thermal",
7634
+ meaning: "Heat paths",
7603
7635
  color: DOMAIN_COLORS.thermal
7604
7636
  },
7605
7637
  {
7606
7638
  key: "network",
7607
- label: "NETWORK",
7639
+ label: "Network",
7640
+ meaning: "Data links",
7608
7641
  color: DOMAIN_COLORS.network
7609
7642
  }
7610
7643
  ];
7611
7644
  var DOMAIN_EXTRA = [{
7612
7645
  key: "software",
7613
- label: "SOFTWARE",
7646
+ label: "Software",
7647
+ meaning: "Software interfaces",
7614
7648
  color: DOMAIN_COLORS.software
7615
7649
  }, {
7616
7650
  key: "other",
7617
- label: "OTHER",
7651
+ label: "Other",
7652
+ meaning: "Other domains",
7618
7653
  color: "var(--uhd-signal-other)"
7619
7654
  }];
7620
7655
  /**
@@ -10619,12 +10654,12 @@ var rightRelease = (press) => press.panning ? "pan" : "menu";
10619
10654
  var TOOL_OPTIONS = [{
10620
10655
  id: "select",
10621
10656
  icon: "mouse-pointer-2",
10622
- label: "Select: drag on the canvas to select several",
10657
+ label: "Select",
10623
10658
  shortcut: "V"
10624
10659
  }, {
10625
10660
  id: "pan",
10626
10661
  icon: "hand",
10627
- label: "Pan: drag to move the view",
10662
+ label: "Pan",
10628
10663
  shortcut: "H"
10629
10664
  }];
10630
10665
  var ARROWS = {
@@ -12454,7 +12489,7 @@ function PairPreview({ vm, link, mode, onOpen }) {
12454
12489
  }), /* @__PURE__ */ jsx(IconButton, {
12455
12490
  icon: "git-compare-arrows",
12456
12491
  size: "sm",
12457
- label: "Open the pair view: every link between these two modules",
12492
+ label: "Pair view",
12458
12493
  onClick: onOpen
12459
12494
  })]
12460
12495
  }),
@@ -12663,6 +12698,36 @@ function selectableLink(vm, id) {
12663
12698
  id: line.linkId
12664
12699
  } : null;
12665
12700
  }
12701
+ /**
12702
+ * The lines a harness is drawn along on this level, when it has no card of
12703
+ * its own: a cable between two parts is one line with the harness as a region
12704
+ * along it, and the links that name a harness carry its region too. Empty for
12705
+ * a harness drawn as a card (and for anything else).
12706
+ */
12707
+ function harnessLines(vm, id) {
12708
+ if (vm.nodes.some((n) => n.id === id)) return [];
12709
+ return vm.links.filter((l) => l.run?.id === id);
12710
+ }
12711
+ /**
12712
+ * A module selection as the diagram can show it: a harness drawn along its
12713
+ * lines (no card of its own) is selected as the line that carries its name
12714
+ * tab, so a row of the hierarchy or a host's focus on it selects and frames
12715
+ * something drawn. Anything else as it is.
12716
+ */
12717
+ function drawnSelection(vm, selection) {
12718
+ if (selection?.kind !== "node") return selection;
12719
+ const lines = harnessLines(vm, selection.id);
12720
+ const line = lines.find((l) => l.run?.tab) ?? lines[0];
12721
+ return line ? {
12722
+ kind: "link",
12723
+ id: line.linkId
12724
+ } : selection;
12725
+ }
12726
+ /** The harness a selected line runs through (its path on the level), if any: its row in the hierarchy. */
12727
+ function selectedHarness(vm, selection) {
12728
+ if (selection?.kind !== "link") return void 0;
12729
+ return vm.links.find((l) => (l.linkId === selection.id || l.id === selection.id) && l.run)?.run?.id;
12730
+ }
12666
12731
  /** `id` relative to the level at `drill`: itself, and without the level's path when it starts with it. */
12667
12732
  function relative(id, drill) {
12668
12733
  const at = drill.join("/");
@@ -12687,6 +12752,10 @@ function selectTarget(vm, drill, ids) {
12687
12752
  kind: "node",
12688
12753
  id: c
12689
12754
  };
12755
+ if (kind !== "link" && harnessLines(vm, c).length) return drawnSelection(vm, {
12756
+ kind: "node",
12757
+ id: c
12758
+ });
12690
12759
  if (kind !== "node") {
12691
12760
  const link = selectableLink(vm, c);
12692
12761
  if (link) return link;
@@ -13142,13 +13211,20 @@ function useViewerController(source, options, host) {
13142
13211
  menu: Boolean(menu)
13143
13212
  };
13144
13213
  const seq = useRef(0);
13214
+ const viewAt = (at) => at.join("/") === drill.join("/") ? vm : buildView(system, scenario.lookup, at, {
13215
+ expanded,
13216
+ overrides,
13217
+ diagnostics,
13218
+ grounds: showGrounds
13219
+ });
13145
13220
  const latest = useRef({
13146
13221
  system,
13147
13222
  lookup: scenario.lookup,
13148
13223
  drill,
13149
13224
  expanded,
13150
13225
  vm,
13151
- view
13226
+ view,
13227
+ viewAt
13152
13228
  });
13153
13229
  latest.current = {
13154
13230
  system,
@@ -13156,7 +13232,28 @@ function useViewerController(source, options, host) {
13156
13232
  drill,
13157
13233
  expanded,
13158
13234
  vm,
13159
- view
13235
+ view,
13236
+ viewAt
13237
+ };
13238
+ /**
13239
+ * A focus on a module (a row of the hierarchy, a host's focus, a finding's Locate), as the diagram shows it at
13240
+ * `at`: the module selected and framed; a harness drawn along its lines (no card of its own) selected as the
13241
+ * line with its name tab, and its lines framed.
13242
+ */
13243
+ const focusOn = (at, target, n) => {
13244
+ if (target?.kind !== "node") return target;
13245
+ const levelVm = latest.current.viewAt(at);
13246
+ const shown = drawnSelection(levelVm, target);
13247
+ const lines = shown?.kind === "link" ? harnessLines(levelVm, target.id).map((l) => l.id) : void 0;
13248
+ setCentre(lines?.length ? {
13249
+ id: target.id,
13250
+ seq: n,
13251
+ lines
13252
+ } : {
13253
+ id: target.id,
13254
+ seq: n
13255
+ });
13256
+ return shown;
13160
13257
  };
13161
13258
  useEffect(() => {
13162
13259
  if (!host) return;
@@ -13175,32 +13272,36 @@ function useViewerController(source, options, host) {
13175
13272
  drill: d,
13176
13273
  expanded: ex
13177
13274
  });
13275
+ const n = ++seq.current;
13178
13276
  setDrill(sameDrill(t.drill));
13179
- setSelection(t.selection);
13277
+ setSelection(focusOn(t.drill, t.selection, n));
13180
13278
  setPairLink(null);
13181
13279
  setPanel((p) => p ?? "hierarchy");
13182
- if (t.selection) {
13183
- const n = ++seq.current;
13184
- setCentre({
13185
- id: t.selection.id,
13186
- seq: n
13187
- });
13188
- setCadFocus({
13189
- path: fullId(t.drill, t.selection.id),
13190
- seq: n
13191
- });
13192
- }
13280
+ if (t.selection) setCadFocus({
13281
+ path: fullId(t.drill, t.selection.id),
13282
+ seq: n
13283
+ });
13193
13284
  return;
13194
13285
  }
13195
13286
  case "uhd-viewer:select": {
13196
13287
  const sel = selectTarget(v, d, m.ids);
13197
13288
  if (sel !== void 0) setSelection(sel);
13198
- if (sel && m.reveal !== false && shownView !== "3d") setReveal({
13289
+ const run = selectedHarness(v, sel ?? null);
13290
+ const named = run !== void 0 && m.ids.some((id) => [run, fullId(d, run)].includes(id.replace(/^node:/, "")));
13291
+ if (sel && named && m.reveal !== false && shownView !== "3d") setCentre({
13292
+ id: run,
13293
+ seq: ++seq.current,
13294
+ lines: harnessLines(v, run).map((l) => l.id)
13295
+ });
13296
+ else if (sel && m.reveal !== false && shownView !== "3d") setReveal({
13199
13297
  selection: sel,
13200
13298
  seq: ++seq.current
13201
13299
  });
13202
13300
  if (shownView === "3d") {
13203
- const t = cadSelectTarget(sys, lookup, d, sel, m.ids);
13301
+ const t = named ? {
13302
+ kind: "node",
13303
+ path: fullId(d, run)
13304
+ } : cadSelectTarget(sys, lookup, d, sel, m.ids);
13204
13305
  if (t) setCadFocus({
13205
13306
  ...t,
13206
13307
  seq: ++seq.current,
@@ -13289,11 +13390,7 @@ function useViewerController(source, options, host) {
13289
13390
  expanded: ex
13290
13391
  });
13291
13392
  setDrill((cur) => cur.join("/") === t.drill.join("/") ? cur : t.drill);
13292
- setSelection(t.selection);
13293
- if (t.selection) setCentre({
13294
- id: t.selection.id,
13295
- seq: n
13296
- });
13393
+ setSelection(focusOn(t.drill, t.selection, n));
13297
13394
  }, [setSelection]);
13298
13395
  const focusRef = useRef(focusPath);
13299
13396
  focusRef.current = focusPath;
@@ -13350,57 +13447,85 @@ function useViewerController(source, options, host) {
13350
13447
  y
13351
13448
  }), []);
13352
13449
  const menuNode = menu?.id ? vm.nodes.find((n) => n.id === menu.id) : void 0;
13450
+ /** What a module's menu offers about it: inspect, open, its source, copy its path. */
13451
+ const nodeItems = (node) => {
13452
+ const items = [];
13453
+ if (node.sheetPort) return items;
13454
+ const path = fullId(drill, node.tapOf ?? node.id);
13455
+ const { defId } = node;
13456
+ items.push({
13457
+ label: "Inspect",
13458
+ icon: "scan-search",
13459
+ run: () => hostActions.inspect ? hostActions.inspect(path, defId) : setPanel("hierarchy")
13460
+ });
13461
+ if (openAction(node, Boolean(hostActions.openModule)) === "open") {
13462
+ const at = fullId(drill, node.id);
13463
+ items.push({
13464
+ label: "Open in new tab",
13465
+ icon: "square-arrow-out-up-right",
13466
+ run: () => hostActions.openModule(at)
13467
+ });
13468
+ }
13469
+ if (defId && hostActions.openSource) items.push({
13470
+ label: "Open source",
13471
+ icon: "file-code",
13472
+ run: () => hostActions.openSource(defId, fullId(drill, node.id))
13473
+ });
13474
+ if (typeof navigator !== "undefined" && navigator.clipboard) items.push({
13475
+ label: "Copy path",
13476
+ icon: "copy",
13477
+ run: () => void navigator.clipboard.writeText(path).catch(() => void 0)
13478
+ });
13479
+ return items;
13480
+ };
13353
13481
  const menuItems = [];
13354
13482
  if (menu && !menu.id) {
13355
13483
  menuItems.push({
13356
- label: "FIT VIEW",
13484
+ label: "Fit view",
13485
+ icon: "maximize",
13486
+ shortcut: "F",
13357
13487
  run: () => viewDo("fit")
13358
13488
  });
13359
13489
  if (drawn.nodes.length > 1) menuItems.push({
13360
- label: "SELECT ALL",
13490
+ label: "Select all",
13491
+ icon: "square-dashed",
13492
+ shortcut: "Ctrl+A",
13361
13493
  run: onSelectAll
13362
13494
  });
13363
13495
  menuItems.push(interaction.tool === "pan" ? {
13364
- label: "SELECT TOOL",
13496
+ label: "Select tool",
13497
+ icon: "mouse-pointer-2",
13498
+ shortcut: "V",
13365
13499
  run: () => setTool("select")
13366
13500
  } : {
13367
- label: "PAN TOOL",
13501
+ label: "Pan tool",
13502
+ icon: "hand",
13503
+ shortcut: "H",
13368
13504
  run: () => setTool("pan")
13369
13505
  });
13370
13506
  if (isolated.size) menuItems.push({
13371
- label: "SHOW EVERYTHING",
13507
+ label: "Show all",
13508
+ icon: "eye",
13509
+ shortcut: "Esc",
13372
13510
  run: clearIsolated
13373
13511
  });
13512
+ const chosen = selection?.kind === "node" && group.length <= 1 ? levelModules(vm.nodes).find((n) => n.id === selection.id) : void 0;
13513
+ (chosen ? nodeItems(chosen) : []).forEach((it, i) => menuItems.push(i === 0 ? {
13514
+ ...it,
13515
+ separator: true
13516
+ } : it));
13374
13517
  }
13375
- if (menuNode && !menuNode.sheetPort) {
13376
- const path = fullId(drill, menuNode.tapOf ?? menuNode.id);
13377
- const { defId } = menuNode;
13378
- menuItems.push({
13379
- label: "INSPECT",
13380
- run: () => hostActions.inspect ? hostActions.inspect(path, defId) : setPanel("hierarchy")
13381
- });
13382
- }
13383
- if (menuNode && openAction(menuNode, Boolean(hostActions.openModule)) === "open") {
13384
- const path = fullId(drill, menuNode.id);
13385
- menuItems.push({
13386
- label: "OPEN IN NEW TAB",
13387
- run: () => hostActions.openModule(path)
13388
- });
13389
- }
13390
- if (menuNode?.defId && hostActions.openSource) {
13391
- const { defId } = menuNode;
13392
- menuItems.push({
13393
- label: "OPEN SOURCE",
13394
- run: () => hostActions.openSource(defId, fullId(drill, menuNode.id))
13395
- });
13396
- }
13518
+ if (menuNode) menuItems.push(...nodeItems(menuNode));
13397
13519
  if (menuNode?.childCount) {
13398
13520
  menuItems.push({
13399
- label: menuNode.expanded ? "COLLAPSE" : "EXPAND IN PLACE",
13521
+ label: menuNode.expanded ? "Collapse" : "Expand in place",
13522
+ icon: menuNode.expanded ? "chevrons-down-up" : "chevrons-up-down",
13523
+ separator: true,
13400
13524
  run: () => onToggleExpand(menuNode.id)
13401
13525
  });
13402
13526
  menuItems.push({
13403
- label: "OPEN AS CANVAS",
13527
+ label: "Open as canvas",
13528
+ icon: "layers",
13404
13529
  run: () => onDrill(menuNode.id)
13405
13530
  });
13406
13531
  }
@@ -13675,7 +13800,7 @@ function Port({ placed, x, y, color, faded, exported, label, ground, selected, d
13675
13800
  const mechanical = placed.port.domain === "mechanical";
13676
13801
  const size = exported ? 11 : 9;
13677
13802
  const id = `p:${placed.port.id}`;
13678
- return /* @__PURE__ */ jsxs(Fragment, { children: [
13803
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
13679
13804
  /* @__PURE__ */ jsx(Handle, {
13680
13805
  type: "source",
13681
13806
  id,
@@ -13767,7 +13892,7 @@ function Ports({ data, labels, border = 2 }) {
13767
13892
  const { box, ports, mode, showGrounds, selectedPorts, node } = data;
13768
13893
  const perSide = (side) => ports.filter((q) => q.side === side).length;
13769
13894
  const widthFor = (p) => p.side === "left" || p.side === "right" ? sideLabelWidth(box.w) : Math.max(30, Math.min(160, (box.w - 32) / (perSide(p.side) + 1) - 4));
13770
- return /* @__PURE__ */ jsx(Fragment, { children: ports.map((p) => {
13895
+ return /* @__PURE__ */ jsx(Fragment$1, { children: ports.map((p) => {
13771
13896
  const onlyGround = p.links.every((l) => l.ground);
13772
13897
  return /* @__PURE__ */ jsx(Port, {
13773
13898
  placed: p,
@@ -14751,7 +14876,7 @@ function LinkEdge({ id, data, selected }) {
14751
14876
  ]
14752
14877
  }, `${kind}${index}`);
14753
14878
  const anchor = geo.anchor;
14754
- return /* @__PURE__ */ jsxs(Fragment, { children: [
14879
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
14755
14880
  geo.region && /* @__PURE__ */ jsx("path", {
14756
14881
  d: geo.region,
14757
14882
  fill: "color-mix(in oklab, var(--uhd-text) 5%, transparent)",
@@ -14908,22 +15033,57 @@ function selectionBounds(vm, layout, selection) {
14908
15033
  y1: Math.max(...ys)
14909
15034
  };
14910
15035
  }
14911
- /**
14912
- * The viewport that shows `rect` clear of the floating chrome (the inspector
14913
- * too, when open), or undefined when it is in view already. It keeps the zoom
14914
- * and pans, zooming out only as far as the selection needs; never in.
14915
- */
14916
- function revealViewport(rect, viewport, pane, inspector, minZoom = .2) {
15036
+ /** Where several lines are drawn together (a harness along the lines that carry it), in flow coordinates. */
15037
+ function linesBounds(layout, lineIds) {
15038
+ const pts = lineIds.flatMap((id) => layout.routes[id] ?? []);
15039
+ if (!pts.length) return void 0;
15040
+ const xs = pts.map((p) => p[0]);
15041
+ const ys = pts.map((p) => p[1]);
15042
+ return {
15043
+ x0: Math.min(...xs),
15044
+ y0: Math.min(...ys),
15045
+ x1: Math.max(...xs),
15046
+ y1: Math.max(...ys)
15047
+ };
15048
+ }
15049
+ /** The part of the pane clear of the floating chrome (the inspector too, when open). */
15050
+ function clearView(pane, inspector) {
14917
15051
  const left = 64;
14918
15052
  const top = 56;
14919
15053
  const bottom = 72;
14920
15054
  const right = inspector ? Math.max(64, Math.min(376, pane.width - left - 160)) : 64;
14921
- const view = {
15055
+ return {
14922
15056
  x0: left,
14923
15057
  y0: top,
14924
15058
  x1: pane.width - right,
14925
15059
  y1: pane.height - bottom
14926
15060
  };
15061
+ }
15062
+ /**
15063
+ * The viewport that frames `rect`, as a module is framed when it is focused:
15064
+ * centred in the view clear of the floating chrome, zoomed to fit it, never
15065
+ * past `maxZoom`.
15066
+ */
15067
+ function frameViewport(rect, pane, inspector, { maxZoom = 1, minZoom = .2 } = {}) {
15068
+ const view = clearView(pane, inspector);
15069
+ const w = Math.max(1, view.x1 - view.x0 - 48);
15070
+ const h = Math.max(1, view.y1 - view.y0 - 48);
15071
+ const zoom = Math.max(minZoom, Math.min(maxZoom, w / Math.max(1, rect.x1 - rect.x0), h / Math.max(1, rect.y1 - rect.y0)));
15072
+ const cx = (rect.x0 + rect.x1) / 2;
15073
+ const cy = (rect.y0 + rect.y1) / 2;
15074
+ return {
15075
+ x: (view.x0 + view.x1) / 2 - cx * zoom,
15076
+ y: (view.y0 + view.y1) / 2 - cy * zoom,
15077
+ zoom
15078
+ };
15079
+ }
15080
+ /**
15081
+ * The viewport that shows `rect` clear of the floating chrome (the inspector
15082
+ * too, when open), or undefined when it is in view already. It keeps the zoom
15083
+ * and pans, zooming out only as far as the selection needs; never in.
15084
+ */
15085
+ function revealViewport(rect, viewport, pane, inspector, minZoom = .2) {
15086
+ const view = clearView(pane, inspector);
14927
15087
  const z = viewport.zoom;
14928
15088
  const on = {
14929
15089
  x0: rect.x0 * z + viewport.x,
@@ -15303,8 +15463,24 @@ function Canvas(props) {
15303
15463
  const moved = useRef(false);
15304
15464
  const centred = useRef(0);
15305
15465
  const { centre, inspector } = props;
15466
+ const paneWidth = useStore((s) => s.width);
15467
+ const paneHeight = useStore((s) => s.height);
15306
15468
  useEffect(() => {
15307
- if (!centre || !drawn || centred.current === centre.seq || !nodes.some((n) => n.id === centre.id)) return;
15469
+ if (!centre || !drawn || centred.current === centre.seq) return;
15470
+ if (centre.lines) {
15471
+ const rect = linesBounds(layout, centre.lines);
15472
+ if (!rect || !paneWidth || !paneHeight) return;
15473
+ const t = setTimeout(() => {
15474
+ centred.current = centre.seq;
15475
+ moved.current = true;
15476
+ flow.setViewport(frameViewport(rect, {
15477
+ width: paneWidth,
15478
+ height: paneHeight
15479
+ }, inspector ?? false), { duration: 300 });
15480
+ }, 60);
15481
+ return () => clearTimeout(t);
15482
+ }
15483
+ if (!nodes.some((n) => n.id === centre.id)) return;
15308
15484
  const t = setTimeout(() => {
15309
15485
  centred.current = centre.seq;
15310
15486
  moved.current = true;
@@ -15320,13 +15496,14 @@ function Canvas(props) {
15320
15496
  centre,
15321
15497
  drawn,
15322
15498
  nodes,
15499
+ layout,
15323
15500
  flow,
15324
- inspector
15501
+ inspector,
15502
+ paneWidth,
15503
+ paneHeight
15325
15504
  ]);
15326
15505
  const revealed = useRef(0);
15327
15506
  const { reveal } = props;
15328
- const paneWidth = useStore((s) => s.width);
15329
- const paneHeight = useStore((s) => s.height);
15330
15507
  useEffect(() => {
15331
15508
  if (!reveal || !drawn || revealed.current === reveal.seq || !paneWidth || !paneHeight) return;
15332
15509
  const rect = selectionBounds(vm, layout, reveal.selection);
@@ -16242,7 +16419,7 @@ function PackageSection({ def, titled = true }) {
16242
16419
  const pkg = def ? partPackage(def) : void 0;
16243
16420
  if (!pkg) return null;
16244
16421
  const s = pkg.size_mm;
16245
- return /* @__PURE__ */ jsxs(Fragment, { children: [titled && /* @__PURE__ */ jsx("div", {
16422
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [titled && /* @__PURE__ */ jsx("div", {
16246
16423
  className: "section-title",
16247
16424
  children: "PACKAGE"
16248
16425
  }), /* @__PURE__ */ jsxs("div", {
@@ -16322,7 +16499,7 @@ function PinTable({ def, node, vm, titled = true }) {
16322
16499
  return m;
16323
16500
  }, [vm, node]);
16324
16501
  if (!pins.length) return null;
16325
- return /* @__PURE__ */ jsxs(Fragment, { children: [titled && /* @__PURE__ */ jsxs("div", {
16502
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [titled && /* @__PURE__ */ jsxs("div", {
16326
16503
  className: "section-title",
16327
16504
  children: [
16328
16505
  "PINS · ",
@@ -16364,7 +16541,7 @@ function PinTable({ def, node, vm, titled = true }) {
16364
16541
  /** Functional links derived over nets, listed by name. */
16365
16542
  function DerivedList({ items, title, onSelect, titled = true }) {
16366
16543
  if (!items.length) return null;
16367
- return /* @__PURE__ */ jsxs(Fragment, { children: [titled && /* @__PURE__ */ jsxs("div", {
16544
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [titled && /* @__PURE__ */ jsxs("div", {
16368
16545
  className: "section-title",
16369
16546
  children: [
16370
16547
  title,
@@ -16518,7 +16695,7 @@ function DerivedDetails({ d, vm, onSelect, bare }) {
16518
16695
  children: label(n)
16519
16696
  }, n))
16520
16697
  }),
16521
- d.children.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
16698
+ d.children.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
16522
16699
  className: "section-title",
16523
16700
  children: ["CONDUCTORS · ", d.children.length]
16524
16701
  }), /* @__PURE__ */ jsx("table", {
@@ -16537,7 +16714,7 @@ function DerivedDetails({ d, vm, onSelect, bare }) {
16537
16714
  })
16538
16715
  ] }, i)) })
16539
16716
  })] }),
16540
- d.diagnostics.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
16717
+ d.diagnostics.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
16541
16718
  className: "section-title",
16542
16719
  children: "DIAGNOSTICS"
16543
16720
  }), d.diagnostics.map((m) => /* @__PURE__ */ jsx("p", {
@@ -16610,7 +16787,7 @@ function BoardViews({ def, lookup, resolve, highlight, onPick, drawings = true }
16610
16787
  ].filter((a) => Boolean(a));
16611
16788
  const shown = new Set(files);
16612
16789
  const sample = files.some((a) => a?.tags?.includes("sample"));
16613
- return /* @__PURE__ */ jsxs(Fragment, { children: [
16790
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
16614
16791
  /* @__PURE__ */ jsx("div", {
16615
16792
  className: "section-title",
16616
16793
  children: /* @__PURE__ */ jsxs("span", { children: ["BOARD OUTPUTS", sample && /* @__PURE__ */ jsx("span", {
@@ -16618,7 +16795,7 @@ function BoardViews({ def, lookup, resolve, highlight, onPick, drawings = true }
16618
16795
  children: "SAMPLE"
16619
16796
  })] })
16620
16797
  }),
16621
- drawings && current && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
16798
+ drawings && current && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
16622
16799
  className: "artifact-parts board-tabs",
16623
16800
  children: tabs.map((t) => /* @__PURE__ */ jsx("button", {
16624
16801
  className: `mini${t.id === current ? " on" : ""}`,
@@ -17245,62 +17422,6 @@ function ArtifactLink({ defId, artifactId, href, title, children }) {
17245
17422
  });
17246
17423
  }
17247
17424
  var urlOf = (def, a, resolve) => a.url ?? (a.filePath ? resolve(def.id, a.filePath) : void 0);
17248
- var hasArtifacts = (def) => Boolean(thumbnailOf(def) || glbFor(def));
17249
- /**
17250
- * An assembly (FC + ESC stack, an arm) usually has no CAD of its own: show its
17251
- * parts' artifacts instead, one part at a time, picked from a row of names.
17252
- */
17253
- function AssemblyArtifacts({ def, lookup, resolve, appearance, titled = true }) {
17254
- const [pick, setPick] = useState(0);
17255
- if (!def) return null;
17256
- if (hasArtifacts(def)) return /* @__PURE__ */ jsx(ArtifactsSection, {
17257
- def,
17258
- resolve,
17259
- appearance: appearance?.[def.id],
17260
- titled
17261
- });
17262
- const parts = [];
17263
- const seen = /* @__PURE__ */ new Set();
17264
- const walk = (d, depth) => {
17265
- for (const ref of d.children ?? []) {
17266
- const c = lookup(ref.moduleDefId);
17267
- if (!c || seen.has(c.id)) continue;
17268
- seen.add(c.id);
17269
- if (hasArtifacts(c)) parts.push({
17270
- name: ref.name ?? c.name,
17271
- def: c
17272
- });
17273
- else if (depth < 3) walk(c, depth + 1);
17274
- }
17275
- };
17276
- walk(def, 0);
17277
- const cur = parts[Math.min(pick, parts.length - 1)];
17278
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ArtifactsSection, {
17279
- def,
17280
- resolve,
17281
- appearance: appearance?.[def.id],
17282
- titled
17283
- }), cur && /* @__PURE__ */ jsxs(Fragment, { children: [
17284
- /* @__PURE__ */ jsx("div", {
17285
- className: "section-title",
17286
- children: "ARTIFACTS · PARTS"
17287
- }),
17288
- /* @__PURE__ */ jsx("div", {
17289
- className: "artifact-parts",
17290
- children: parts.map((p, i) => /* @__PURE__ */ jsx("button", {
17291
- className: `mini${p === cur ? " on" : ""}`,
17292
- onClick: () => setPick(i),
17293
- children: p.name.toUpperCase()
17294
- }, p.def.id))
17295
- }),
17296
- /* @__PURE__ */ jsx(ArtifactsSection, {
17297
- def: cur.def,
17298
- resolve,
17299
- appearance: appearance?.[cur.def.id],
17300
- titled: false
17301
- })
17302
- ] })] });
17303
- }
17304
17425
  /**
17305
17426
  * A definition's artifacts: its picture, its body model (turning in a small
17306
17427
  * 3D preview, unless the host shows models as cards), and every artifact as
@@ -17324,7 +17445,7 @@ function ArtifactsSection({ def, resolve, appearance, titled = true }) {
17324
17445
  sources: files?.sources
17325
17446
  });
17326
17447
  if (!thumb && !glb && !cards.length) return null;
17327
- return /* @__PURE__ */ jsxs(Fragment, { children: [
17448
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
17328
17449
  titled && /* @__PURE__ */ jsxs("div", {
17329
17450
  className: "section-title",
17330
17451
  children: ["ARTIFACTS · ", (def.artifacts ?? []).filter((a) => (a.filePath || a.url) && !board.has(a) && !isInternalArtifact(a)).length]
@@ -17491,7 +17612,7 @@ function EvidenceSection({ def, titled = true, performance = true }) {
17491
17612
  if (!def || !ev || !hasEvidence(ev, performance)) return null;
17492
17613
  const v = ev.verification;
17493
17614
  const named = (v?.assumptions ?? []).map(text).filter((a) => !ev.assumptions.some((t) => t.field === a));
17494
- return /* @__PURE__ */ jsxs(Fragment, { children: [titled && /* @__PURE__ */ jsx("div", {
17615
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [titled && /* @__PURE__ */ jsx("div", {
17495
17616
  className: "section-title",
17496
17617
  children: "EVIDENCE"
17497
17618
  }), /* @__PURE__ */ jsxs("div", {
@@ -17813,19 +17934,23 @@ function interfaceSlots(node, vm, opts) {
17813
17934
  const mine = lines.filter((l) => l.source.node === node.id && l.source.port === port || l.target.node === node.id && l.target.port === port);
17814
17935
  return mine.filter((l, i) => mine.findIndex((m) => m.linkId === l.linkId) === i);
17815
17936
  };
17816
- const linkedIds = new Set(node.ports.filter((p) => linksOf(p.id).length).map((p) => p.id));
17937
+ const drawn = new Set(node.ports.map((p) => p.id));
17938
+ const ports = [...node.ports, ...def && node.kind !== "harness" ? def.interfaces.filter((i) => !drawn.has(i.id)).map((i) => toPort(i, def)) : []];
17939
+ const boundInto = /* @__PURE__ */ new Map();
17940
+ for (const i of def?.interfaces ?? []) for (const profile of i.profiles ?? []) for (const v of Object.values(profile.bindings)) for (const id of Array.isArray(v) ? v : [v]) if (id !== i.id && !boundInto.has(id)) boundInto.set(id, i.name && i.name !== i.id ? i.name : humanize(i.id));
17941
+ const linkedIds = new Set(ports.filter((p) => linksOf(p.id).length).map((p) => p.id));
17817
17942
  const exportedAs = /* @__PURE__ */ new Map();
17818
17943
  for (const l of vm.links) if (l.passThrough && l.target.node === node.id && !exportedAs.has(l.target.port)) exportedAs.set(l.target.port, l.name);
17819
17944
  const usedBy = /* @__PURE__ */ new Map();
17820
- for (const p of node.ports) if (linkedIds.has(p.id)) {
17945
+ for (const p of ports) if (linkedIds.has(p.id)) {
17821
17946
  for (const c of p.children) if (c.leafId && !usedBy.has(c.leafId)) usedBy.set(c.leafId, p.name === p.id ? humanize(p.id) : p.name);
17822
17947
  }
17823
- for (const p of node.ports) {
17948
+ for (const p of ports) {
17824
17949
  if (linkedIds.has(p.id) || usedBy.has(p.id)) continue;
17825
17950
  const via = p.children.map((c) => c.leafId && usedBy.get(c.leafId)).find(Boolean);
17826
17951
  if (via) usedBy.set(p.id, via);
17827
17952
  }
17828
- const slots = node.ports.map((port) => {
17953
+ const slots = ports.map((port) => {
17829
17954
  const iface = findInterface(def, port.id, lookup);
17830
17955
  const leaves = port.children.map((c) => c.leafId ? findInterface(def, c.leafId, lookup) : void 0);
17831
17956
  const links = linksOf(port.id);
@@ -17963,9 +18088,10 @@ function interfaceSlots(node, vm, opts) {
17963
18088
  ...role ? { role } : {},
17964
18089
  pins,
17965
18090
  linked: linked.filter((x, i) => linked.findIndex((y) => y.module === x.module && y.via === x.via) === i),
17966
- open: !links.length && !usedBy.has(port.id) && !exportedAs.has(port.id) && !vm.derived.some((d) => d.a.node === node.id && d.a.iface === port.id || d.b.node === node.id && d.b.iface === port.id),
18091
+ open: !links.length && !usedBy.has(port.id) && !exportedAs.has(port.id) && !(!drawn.has(port.id) && boundInto.has(port.id)) && !vm.derived.some((d) => d.a.node === node.id && d.a.iface === port.id || d.b.node === node.id && d.b.iface === port.id),
17967
18092
  ...!links.length && usedBy.has(port.id) ? { usedBy: usedBy.get(port.id) } : {},
17968
18093
  ...!links.length && !usedBy.has(port.id) && exportedAs.has(port.id) ? { exportedAs: exportedAs.get(port.id) } : {},
18094
+ ...!links.length && !usedBy.has(port.id) && !exportedAs.has(port.id) && !drawn.has(port.id) && boundInto.has(port.id) ? { partOf: boundInto.get(port.id) } : {},
17969
18095
  ...state ? { state } : {},
17970
18096
  pinMap,
17971
18097
  params,
@@ -17976,7 +18102,7 @@ function interfaceSlots(node, vm, opts) {
17976
18102
  refs: [...refs]
17977
18103
  };
17978
18104
  });
17979
- const order = (s) => s.linked.length || s.links.length ? s.kind === "mechanical" ? 3 : s.kind === "power" ? 0 : 1 : s.open ? 2 : 4;
18105
+ const order = (s) => s.linked.length || s.links.length ? s.kind === "mechanical" ? 3 : s.kind === "power" ? 0 : 1 : s.open ? 2 : s.partOf ? 5 : 4;
17980
18106
  return groupSlots(slots.map((s, i) => ({
17981
18107
  s,
17982
18108
  i
@@ -18005,7 +18131,7 @@ var shape = (name) => name.replace(/\d+/g, "#");
18005
18131
  */
18006
18132
  function groupSlots(slots) {
18007
18133
  const out = [];
18008
- const key = (s) => `${shape(s.name)}|${s.kind}|${s.protocol}|${s.linked.length ? "linked" : s.open ? "open" : s.exportedAs ? "exported" : "used"}`;
18134
+ const key = (s) => `${shape(s.name)}|${s.kind}|${s.protocol}|${s.linked.length ? "linked" : s.open ? "open" : s.exportedAs ? "exported" : s.partOf ? `part of ${s.partOf}` : "used"}`;
18009
18135
  for (const s of slots) {
18010
18136
  if (!/\d/.test(s.name)) {
18011
18137
  out.push(s);
@@ -18316,7 +18442,122 @@ function KeyValues({ rows, limit }) {
18316
18442
  })]
18317
18443
  }, `${r.key}-${i}`)) });
18318
18444
  }
18319
- /** A section's rows shown in part, and the header action that shows them all. */
18445
+ /** How many rows of a long list are shown at first, and how many more each "Show more" adds (PB-1088). */
18446
+ var LIST_STEP = 10;
18447
+ var MORE_LOOK = {
18448
+ row: "flex min-h-6 items-center gap-3 pt-1",
18449
+ button: "inline-flex h-6 cursor-pointer items-center gap-1 rounded-[3px] px-1 -mx-1 text-[11px] leading-none text-fg-muted hover:text-fg focus-visible:outline focus-visible:outline-1 focus-visible:outline-focus"
18450
+ };
18451
+ /**
18452
+ * A long list shown `step` rows at a time (PB-1088): "Show 10 more" under the
18453
+ * rows adds the next ones, "Show fewer" goes back to the first `step`.
18454
+ * `keep`: rows that must stay shown (an expanded card), counted from the top.
18455
+ */
18456
+ function useLoadMore(total, step = 10, keep = 0) {
18457
+ const [shown, setShown] = useState(step);
18458
+ const limit = Math.min(total, Math.max(shown, keep));
18459
+ if (total <= step) return {
18460
+ limit: total,
18461
+ footer: null
18462
+ };
18463
+ const next = Math.min(step, total - limit);
18464
+ return {
18465
+ limit,
18466
+ footer: /* @__PURE__ */ jsxs("div", {
18467
+ className: MORE_LOOK.row,
18468
+ "data-load-more": "",
18469
+ children: [next > 0 && /* @__PURE__ */ jsxs("button", {
18470
+ type: "button",
18471
+ "data-nav": "",
18472
+ onKeyDown: onNavKey,
18473
+ className: MORE_LOOK.button,
18474
+ onClick: () => setShown(limit + next),
18475
+ children: [
18476
+ /* @__PURE__ */ jsx(Icon, {
18477
+ name: "chevron-down",
18478
+ size: 12,
18479
+ strokeWidth: 1.75,
18480
+ "aria-hidden": true,
18481
+ className: "shrink-0"
18482
+ }),
18483
+ "Show ",
18484
+ next,
18485
+ " more"
18486
+ ]
18487
+ }), limit > step && /* @__PURE__ */ jsxs("button", {
18488
+ type: "button",
18489
+ "data-nav": "",
18490
+ onKeyDown: onNavKey,
18491
+ className: cx(MORE_LOOK.button, next > 0 && "ml-auto"),
18492
+ onClick: () => setShown(step),
18493
+ children: [/* @__PURE__ */ jsx(Icon, {
18494
+ name: "chevron-up",
18495
+ size: 12,
18496
+ strokeWidth: 1.75,
18497
+ "aria-hidden": true,
18498
+ className: "shrink-0"
18499
+ }), "Show fewer"]
18500
+ })]
18501
+ })
18502
+ };
18503
+ }
18504
+ /**
18505
+ * A long free text (a module's description) cut to `lines` lines, with "More"
18506
+ * to read it all and "Less" to cut it again (PB-1088). The toggle shows only
18507
+ * when the text is cut.
18508
+ */
18509
+ function ClampedText({ text, lines = 5, className }) {
18510
+ const [open, setOpen] = useState(false);
18511
+ const [cut, setCut] = useState(() => text.length > lines * 60);
18512
+ const ref = useRef(null);
18513
+ useLayoutEffect(() => {
18514
+ const el = ref.current;
18515
+ if (!el || open) return;
18516
+ const measure = () => {
18517
+ setCut(el.clientHeight > 0 ? el.scrollHeight > el.clientHeight + 1 : text.length > lines * 60);
18518
+ };
18519
+ measure();
18520
+ if (typeof ResizeObserver === "undefined") return;
18521
+ const ro = new ResizeObserver(measure);
18522
+ ro.observe(el);
18523
+ return () => ro.disconnect();
18524
+ }, [
18525
+ text,
18526
+ lines,
18527
+ open
18528
+ ]);
18529
+ const id = useId();
18530
+ return /* @__PURE__ */ jsxs("div", {
18531
+ className: "pb-1.5",
18532
+ children: [/* @__PURE__ */ jsx("p", {
18533
+ ref,
18534
+ id,
18535
+ "data-clamped": !open && cut ? "" : void 0,
18536
+ className: cx("m-0 [overflow-wrap:anywhere] whitespace-pre-line", className),
18537
+ style: open ? void 0 : {
18538
+ display: "-webkit-box",
18539
+ WebkitBoxOrient: "vertical",
18540
+ WebkitLineClamp: lines,
18541
+ overflow: "hidden"
18542
+ },
18543
+ children: text
18544
+ }), (cut || open) && /* @__PURE__ */ jsxs("button", {
18545
+ type: "button",
18546
+ "aria-expanded": open,
18547
+ "aria-controls": id,
18548
+ className: cx(MORE_LOOK.button, "mt-0.5"),
18549
+ onClick: () => setOpen(!open),
18550
+ children: [/* @__PURE__ */ jsx(Icon, {
18551
+ name: open ? "chevron-up" : "chevron-down",
18552
+ size: 12,
18553
+ strokeWidth: 1.75,
18554
+ "aria-hidden": true,
18555
+ className: "shrink-0"
18556
+ }), open ? "Less" : "More"]
18557
+ })]
18558
+ });
18559
+ }
18560
+ /** A section's rows shown in part, and the header action that shows them all. Lists use `useLoadMore` (PB-1088). */
18320
18561
  function useShowAll(total, limit) {
18321
18562
  const [all, setAll] = useState(false);
18322
18563
  if (total <= limit) return {
@@ -18360,6 +18601,16 @@ var CARD_LOOK = {
18360
18601
  toggle: "min-w-0 truncate text-left text-[12px] leading-[18px] text-fg outline-none after:absolute after:inset-0 after:content-[''] cursor-pointer",
18361
18602
  chip: "inline-flex h-[18px] shrink-0 items-center rounded-[4px] bg-[color-mix(in_oklab,var(--uhd-text)_8%,transparent)] px-1.5 text-[10px] leading-none text-fg-muted",
18362
18603
  pin: "inline-flex h-4 shrink-0 items-center rounded-[4px] border border-line px-1 text-[10px] leading-none text-fg-muted",
18604
+ /**
18605
+ * The pin mapping's columns, shared by its rows: the pin as wide as the widest (at least 44 px, its cell's
18606
+ * minimum; at most 40 % of the card), the leaf (when any pin binds one) at most 30 %, and what it meets in the rest.
18607
+ */
18608
+ pinMap: "grid gap-x-2 gap-y-[3px] text-[11px] leading-none",
18609
+ pinMapPins: "grid-cols-[fit-content(40%)_minmax(0,1fr)]",
18610
+ pinMapLeaves: "grid-cols-[fit-content(40%)_fit-content(30%)_minmax(0,1fr)]",
18611
+ pinRow: "col-span-full grid min-h-5 grid-cols-subgrid items-center",
18612
+ /** A pin in the mapping: a chip that ends in an ellipsis when the column is narrower than its name. */
18613
+ pinName: "inline-block h-4 max-w-full truncate rounded-[4px] border border-line px-1 align-middle text-[10px] leading-[14px] text-fg-muted",
18363
18614
  meta: "mt-[5px] flex min-w-0 items-center gap-1",
18364
18615
  role: "ml-auto shrink-0 text-[10px] leading-none text-fg-faint",
18365
18616
  linked: "mt-[5px] flex min-w-0 items-center gap-1.5 text-[11px] leading-[16px]",
@@ -18434,6 +18685,61 @@ function Group({ title, children }) {
18434
18685
  }), children]
18435
18686
  });
18436
18687
  }
18688
+ /**
18689
+ * The pin mapping: one row per pin, in columns shared by every row (the pin,
18690
+ * the leaf it binds when it is not the pin itself, and what it meets across
18691
+ * the link). Every column fits the card: a pin or name too long for it ends
18692
+ * in an ellipsis and says itself in full in its tooltip, so nothing runs over
18693
+ * the next column or the next line in a narrow Inspector.
18694
+ */
18695
+ function PinMap({ pins }) {
18696
+ const leaves = pins.some((p) => p.leaf);
18697
+ return /* @__PURE__ */ jsx("div", {
18698
+ className: cx(CARD_LOOK.pinMap, leaves ? CARD_LOOK.pinMapLeaves : CARD_LOOK.pinMapPins),
18699
+ children: pins.map((p, i) => /* @__PURE__ */ jsxs("div", {
18700
+ className: CARD_LOOK.pinRow,
18701
+ "data-pin": p.pin,
18702
+ children: [
18703
+ /* @__PURE__ */ jsx("span", {
18704
+ className: "min-w-11",
18705
+ children: /* @__PURE__ */ jsx("span", {
18706
+ className: CARD_LOOK.pinName,
18707
+ "data-tooltip": p.pin.length > 6 ? p.pin : void 0,
18708
+ children: p.pin
18709
+ })
18710
+ }),
18711
+ leaves && /* @__PURE__ */ jsx("span", {
18712
+ className: "min-w-0 truncate text-fg-muted",
18713
+ "data-tooltip": p.leaf,
18714
+ children: p.leaf ?? ""
18715
+ }),
18716
+ /* @__PURE__ */ jsxs("span", {
18717
+ className: "flex min-w-0 items-center gap-1.5",
18718
+ children: [
18719
+ /* @__PURE__ */ jsx("span", {
18720
+ className: "shrink-0 text-fg",
18721
+ "aria-label": p.dir === "←" ? "from" : p.dir === "→" ? "to" : p.dir === "—" ? "shared with" : "with",
18722
+ children: p.dir
18723
+ }),
18724
+ p.other ? /* @__PURE__ */ jsx("span", {
18725
+ className: "max-w-[60%] min-w-0 shrink-0 truncate text-fg",
18726
+ "data-tooltip": p.other.length > 12 ? p.other : void 0,
18727
+ children: p.other
18728
+ }) : /* @__PURE__ */ jsx("span", {
18729
+ className: "shrink-0 text-fg-faint",
18730
+ children: p.otherModule ? "" : "not linked"
18731
+ }),
18732
+ p.otherModule && /* @__PURE__ */ jsx("span", {
18733
+ className: "min-w-0 truncate text-fg-faint",
18734
+ "data-tooltip": p.otherModule.length > 12 ? p.otherModule : void 0,
18735
+ children: p.otherModule
18736
+ })
18737
+ ]
18738
+ })
18739
+ ]
18740
+ }, `${p.pin}-${i}`))
18741
+ });
18742
+ }
18437
18743
  /** What an expanded card shows below its summary. */
18438
18744
  function CardDetail({ slot, actions }) {
18439
18745
  return /* @__PURE__ */ jsxs("div", {
@@ -18442,46 +18748,7 @@ function CardDetail({ slot, actions }) {
18442
18748
  children: [
18443
18749
  slot.pinMap.length > 0 && /* @__PURE__ */ jsx(Group, {
18444
18750
  title: "Pin mapping",
18445
- children: slot.pinMap.map((p, i) => /* @__PURE__ */ jsxs("div", {
18446
- className: "flex min-h-5 min-w-0 items-center gap-2 text-[11px] leading-none",
18447
- "data-pin": p.pin,
18448
- children: [
18449
- /* @__PURE__ */ jsx("span", {
18450
- className: "w-11 shrink-0",
18451
- children: /* @__PURE__ */ jsx("span", {
18452
- className: CARD_LOOK.pin,
18453
- children: p.pin
18454
- })
18455
- }),
18456
- /* @__PURE__ */ jsx("span", {
18457
- className: "w-[88px] shrink-0 truncate text-fg-muted",
18458
- "data-tooltip": p.leaf,
18459
- children: p.leaf ?? ""
18460
- }),
18461
- /* @__PURE__ */ jsxs("span", {
18462
- className: "flex min-w-0 flex-1 items-center gap-1.5",
18463
- children: [
18464
- /* @__PURE__ */ jsx("span", {
18465
- className: "shrink-0 text-fg",
18466
- "aria-label": p.dir === "←" ? "from" : p.dir === "→" ? "to" : p.dir === "—" ? "shared with" : "with",
18467
- children: p.dir
18468
- }),
18469
- p.other ? /* @__PURE__ */ jsx("span", {
18470
- className: "max-w-[60%] min-w-0 shrink-0 truncate text-fg",
18471
- "data-tooltip": p.other.length > 12 ? p.other : void 0,
18472
- children: p.other
18473
- }) : /* @__PURE__ */ jsx("span", {
18474
- className: "text-fg-faint",
18475
- children: p.otherModule ? "" : "not linked"
18476
- }),
18477
- p.otherModule && /* @__PURE__ */ jsx("span", {
18478
- className: "min-w-0 truncate text-fg-faint",
18479
- children: p.otherModule
18480
- })
18481
- ]
18482
- })
18483
- ]
18484
- }, `${p.pin}-${i}`))
18751
+ children: /* @__PURE__ */ jsx(PinMap, { pins: slot.pinMap })
18485
18752
  }),
18486
18753
  (slot.protocol || slot.params.length > 0) && /* @__PURE__ */ jsxs(Group, {
18487
18754
  title: "Protocol and parameters",
@@ -18799,6 +19066,18 @@ function InterfaceCard({ slot, open, onToggle, actions }) {
18799
19066
  className: "min-w-0 truncate text-fg-faint",
18800
19067
  children: ["in use through ", slot.usedBy]
18801
19068
  })]
19069
+ }) : slot.partOf ? /* @__PURE__ */ jsxs("div", {
19070
+ className: CARD_LOOK.linked,
19071
+ children: [/* @__PURE__ */ jsx(Icon, {
19072
+ name: "corner-down-right",
19073
+ size: 12,
19074
+ strokeWidth: 1.75,
19075
+ "aria-hidden": true,
19076
+ className: "shrink-0 text-fg-faint"
19077
+ }), /* @__PURE__ */ jsxs("span", {
19078
+ className: "min-w-0 truncate text-fg-faint",
19079
+ children: ["part of ", slot.partOf]
19080
+ })]
18802
19081
  }) : null
18803
19082
  ]
18804
19083
  }), /* @__PURE__ */ jsx("div", {
@@ -18811,46 +19090,20 @@ function InterfaceCard({ slot, open, onToggle, actions }) {
18811
19090
  })]
18812
19091
  });
18813
19092
  }
18814
- /** The cards, the first few shown; "N more · names" opens the rest. */
18815
- function InterfaceCards({ slots, actions, first = 4 }) {
19093
+ /** The cards, ten at a time (PB-1088): "Show 10 more" adds the next ten, "Show fewer" goes back to the first ten. */
19094
+ function InterfaceCards({ slots, actions, first = 10 }) {
18816
19095
  const [expanded, toggle] = useExpandedCards();
18817
- const [all, setAll] = useState(false);
18818
19096
  const lastOpen = slots.reduce((m, s, i) => expanded.has(s.id) ? i : m, -1);
18819
- const cut = all ? slots.length : Math.max(first, lastOpen + 1);
18820
- const shown = slots.slice(0, cut);
18821
- const rest = slots.slice(cut);
19097
+ const more = useLoadMore(slots.length, first, lastOpen + 1);
18822
19098
  return /* @__PURE__ */ jsxs("div", {
18823
19099
  className: CARD_LOOK.list,
18824
19100
  "data-interface-cards": "",
18825
- children: [shown.map((s) => /* @__PURE__ */ jsx(InterfaceCard, {
19101
+ children: [slots.slice(0, more.limit).map((s) => /* @__PURE__ */ jsx(InterfaceCard, {
18826
19102
  slot: s,
18827
19103
  open: expanded.has(s.id),
18828
19104
  onToggle: () => toggle(s.id),
18829
19105
  actions
18830
- }, s.id)), rest.length > 0 && /* @__PURE__ */ jsxs("button", {
18831
- type: "button",
18832
- "data-nav": "",
18833
- onKeyDown: onNavKey,
18834
- className: CARD_LOOK.more,
18835
- "aria-expanded": false,
18836
- onClick: () => setAll(true),
18837
- "data-tooltip": rest.map((s) => s.name).join(", "),
18838
- "data-tooltip-clamp": "4",
18839
- children: [/* @__PURE__ */ jsx(Icon, {
18840
- name: "chevron-right",
18841
- size: 12,
18842
- strokeWidth: 1.75,
18843
- "aria-hidden": true,
18844
- className: "shrink-0"
18845
- }), /* @__PURE__ */ jsxs("span", {
18846
- className: "min-w-0 truncate",
18847
- children: [
18848
- rest.length,
18849
- " more · ",
18850
- rest.map((s) => s.name).join(", ")
18851
- ]
18852
- })]
18853
- })]
19106
+ }, s.id)), more.footer]
18854
19107
  });
18855
19108
  }
18856
19109
  //#endregion
@@ -18862,25 +19115,18 @@ function factRows(rows) {
18862
19115
  value
18863
19116
  }));
18864
19117
  }
18865
- /** "24 models · 2 docs · 1 evidence": what a definition's artifacts, and its parts', are, by group. */
18866
- function artifactSummary(def, lookup) {
19118
+ /**
19119
+ * "4 models · 2 docs · 1 evidence": what a definition's own artifacts are, by group. A parent's are its own only
19120
+ * (PB-1088): its children's are theirs, a selection away.
19121
+ */
19122
+ function artifactSummary(def, _lookup) {
18867
19123
  if (!def) return {
18868
19124
  count: 0,
18869
19125
  text: ""
18870
19126
  };
18871
- const defs = [def];
18872
- const seen = /* @__PURE__ */ new Set([def.id]);
18873
- const walk = (d, depth) => {
18874
- for (const ref of d.children ?? []) {
18875
- const c = lookup(ref.moduleDefId);
18876
- if (!c || seen.has(c.id)) continue;
18877
- seen.add(c.id);
18878
- defs.push(c);
18879
- if (depth < 3) walk(c, depth + 1);
18880
- }
18881
- };
18882
- walk(def, 0);
18883
- const cards = defs.flatMap((d) => artifactCards(d, () => void 0));
19127
+ const outs = circuitOutputs(def);
19128
+ const board = new Set(outs ? Object.values(outs).filter(Boolean) : []);
19129
+ const cards = artifactCards(def, () => void 0, { skip: (a) => board.has(a) });
18884
19130
  const WORD = {
18885
19131
  images: ["image", "images"],
18886
19132
  models: ["model", "models"],
@@ -18953,7 +19199,7 @@ function ModuleDetails({ node, vm, def, onDrill, onToggleExpand, onSelect, artif
18953
19199
  ["Part number", node.partNumber],
18954
19200
  ["Path", node.id || void 0]
18955
19201
  ]), ...performance];
18956
- const showAll = useShowAll(params.length, 5);
19202
+ const more = useLoadMore(params.length);
18957
19203
  const slots = useMemo(() => node.kind === "harness" ? [] : interfaceSlots(node, vm, {
18958
19204
  lookup: (id) => find(id),
18959
19205
  drill,
@@ -18970,7 +19216,6 @@ function ModuleDetails({ node, vm, def, onDrill, onToggleExpand, onSelect, artif
18970
19216
  findings
18971
19217
  ]);
18972
19218
  const summary = slotSummary(slots);
18973
- const firstCards = Math.min(12, Math.max(4, slots.filter((s) => s.links.length && s.kind !== "mechanical" || s.open).length));
18974
19219
  const arts = artifactSummary(def, find);
18975
19220
  const derived = vm.derived.filter((d) => d.a.node === node.id || d.b.node === node.id);
18976
19221
  const pins = (def?.interfaces ?? []).filter((i) => i.pin !== void 0).length;
@@ -19009,14 +19254,18 @@ function ModuleDetails({ node, vm, def, onDrill, onToggleExpand, onSelect, artif
19009
19254
  /* @__PURE__ */ jsxs(DetailsSection, {
19010
19255
  id: "parameters",
19011
19256
  title: "Parameters",
19012
- aside: showAll.aside,
19013
- children: [node.description && /* @__PURE__ */ jsx("p", {
19014
- className: "m-0 pb-1.5 text-[11px] leading-[1.5] text-fg-muted",
19015
- children: node.description
19016
- }), /* @__PURE__ */ jsx(KeyValues, {
19017
- rows: params,
19018
- limit: showAll.limit
19019
- })]
19257
+ children: [
19258
+ node.description && /* @__PURE__ */ jsx(ClampedText, {
19259
+ text: node.description,
19260
+ lines: 5,
19261
+ className: "text-[11px] leading-[1.5] text-fg-muted"
19262
+ }, node.description),
19263
+ /* @__PURE__ */ jsx(KeyValues, {
19264
+ rows: params,
19265
+ limit: more.limit
19266
+ }),
19267
+ more.footer
19268
+ ]
19020
19269
  }),
19021
19270
  board && /* @__PURE__ */ jsx(DetailsSection, {
19022
19271
  id: "board",
@@ -19030,12 +19279,12 @@ function ModuleDetails({ node, vm, def, onDrill, onToggleExpand, onSelect, artif
19030
19279
  summary: summary.text,
19031
19280
  children: /* @__PURE__ */ jsx(InterfaceCards, {
19032
19281
  slots,
19033
- first: firstCards,
19282
+ first: 10,
19034
19283
  actions: cardActions ?? { selectLink: (id) => onSelect({
19035
19284
  kind: "link",
19036
19285
  id
19037
19286
  }) }
19038
- })
19287
+ }, node.id)
19039
19288
  }),
19040
19289
  artifacts && arts.count > 0 && /* @__PURE__ */ jsx(DetailsSection, {
19041
19290
  id: "artifacts",
@@ -19297,7 +19546,7 @@ function Hierarchy({ vm, selection, onSelect, board, onContext }) {
19297
19546
  children: ["MODULES · ", vm.nodes.filter((n) => !n.sheetPort && n.kind !== "net").length]
19298
19547
  }),
19299
19548
  roots.map((n) => item(n, 0)),
19300
- nets.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
19549
+ nets.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
19301
19550
  className: "section-title",
19302
19551
  children: ["NETS · ", nets.length]
19303
19552
  }), nets.map((n) => /* @__PURE__ */ jsxs("button", {
@@ -19601,11 +19850,10 @@ function InspectorView(props) {
19601
19850
  onSelect: props.onSelect,
19602
19851
  board: boardFor(node),
19603
19852
  onOpenSource: host.openSource && node.defId ? () => host.openSource(node.defId, fullId(props.drill, node.id)) : void 0,
19604
- artifacts: /* @__PURE__ */ jsx(AssemblyArtifacts, {
19853
+ artifacts: /* @__PURE__ */ jsx(ArtifactsSection, {
19605
19854
  def: props.defFor(node.defId),
19606
- lookup: props.defFor,
19607
19855
  resolve: props.resolveArtifact,
19608
- appearance: props.appearance,
19856
+ appearance: node.defId ? props.appearance?.[node.defId] : void 0,
19609
19857
  titled: false
19610
19858
  }, node.id),
19611
19859
  lookup: props.defFor,
@@ -19709,8 +19957,9 @@ function NodeMenu({ x, y, items, onClose }) {
19709
19957
  };
19710
19958
  }, [onClose]);
19711
19959
  if (!items.length) return null;
19712
- const left = Math.min(x, window.innerWidth - 180);
19713
- const top = Math.min(y, window.innerHeight - 12 - items.length * 30);
19960
+ const rules = items.filter((it, i) => it.separator && i > 0).length;
19961
+ const left = Math.min(x, window.innerWidth - 228);
19962
+ const top = Math.max(4, Math.min(y, window.innerHeight - 12 - items.length * 28 - rules * 9));
19714
19963
  return /* @__PURE__ */ jsx("div", {
19715
19964
  ref,
19716
19965
  className: "context-menu",
@@ -19719,14 +19968,36 @@ function NodeMenu({ x, y, items, onClose }) {
19719
19968
  top
19720
19969
  },
19721
19970
  role: "menu",
19722
- children: items.map((it) => /* @__PURE__ */ jsx("button", {
19971
+ children: items.map((it, i) => /* @__PURE__ */ jsxs(Fragment, { children: [it.separator && i > 0 && /* @__PURE__ */ jsx("div", {
19972
+ role: "separator",
19973
+ className: "context-menu-rule"
19974
+ }), /* @__PURE__ */ jsxs("button", {
19975
+ type: "button",
19723
19976
  role: "menuitem",
19724
19977
  onClick: () => {
19725
19978
  onClose();
19726
19979
  it.run();
19727
19980
  },
19728
- children: it.label
19729
- }, it.label))
19981
+ children: [
19982
+ /* @__PURE__ */ jsx("span", {
19983
+ className: "context-menu-icon",
19984
+ "aria-hidden": true,
19985
+ children: it.icon && /* @__PURE__ */ jsx(Icon, {
19986
+ name: it.icon,
19987
+ size: 14,
19988
+ strokeWidth: 1.75
19989
+ })
19990
+ }),
19991
+ /* @__PURE__ */ jsx("span", {
19992
+ className: "context-menu-label",
19993
+ children: it.label
19994
+ }),
19995
+ it.shortcut && /* @__PURE__ */ jsx("kbd", {
19996
+ className: "context-menu-key",
19997
+ children: it.shortcut
19998
+ })
19999
+ ]
20000
+ })] }, it.label))
19730
20001
  });
19731
20002
  }
19732
20003
  /** Memoised: re-renders only when its props change, not on every frame of a canvas drag. */
@@ -20296,7 +20567,7 @@ function vscodeThemeAdapter(doc = typeof document === "undefined" ? void 0 : doc
20296
20567
  * component, and the tooltip lives inside the themed root, so it keeps the
20297
20568
  * host's tokens.
20298
20569
  */
20299
- var OPEN_DELAY = 350;
20570
+ var OPEN_DELAY = 250;
20300
20571
  /** Moving from one control to the next within this time shows the next tip at once. */
20301
20572
  var SKIP_DELAY = 300;
20302
20573
  /** Where to put the tip for a target: below it unless it says otherwise, kept inside the root. */
@@ -20390,7 +20661,7 @@ function Tooltips({ root }) {
20390
20661
  return /* @__PURE__ */ jsxs("div", {
20391
20662
  ref,
20392
20663
  role: "tooltip",
20393
- className: `uhd-tooltip pointer-events-none absolute z-[1000] flex items-center gap-2 rounded-md border border-line bg-raised px-2 py-1 font-ui text-[11px] leading-4 text-fg shadow-float outline outline-1 -outline-offset-1 outline-contrast ${tip.clamp ? "w-max max-w-[min(300px,calc(100%-12px))] whitespace-normal" : "whitespace-nowrap"}`,
20664
+ className: `uhd-tooltip pointer-events-none absolute select-none z-[1000] flex items-center gap-2 rounded-md border border-line bg-raised px-2 py-1 font-ui text-[11px] leading-4 text-fg shadow-float outline outline-1 -outline-offset-1 outline-contrast ${tip.clamp ? "w-max max-w-[min(300px,calc(100%-12px))] whitespace-normal" : "whitespace-nowrap"}`,
20394
20665
  style: {
20395
20666
  left: tip.x + nudge,
20396
20667
  top: tip.y,
@@ -22433,10 +22704,12 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22433
22704
  const h = handle.current;
22434
22705
  const recs = selectionRecs(sel.body);
22435
22706
  const rec = recs.length === 1 && sel.body !== void 0 ? bodies.current.get(sel.body) : void 0;
22436
- if (!h || !recs.length) return;
22437
- if (!sel.iface || !rec) {
22707
+ const wire = sel.body === void 0 && sel.harness !== void 0 ? harnessObjs.current.get(sel.harness) : void 0;
22708
+ const objs = wire ? [wire] : recs.map((r) => r.obj);
22709
+ if (!h || !objs.length) return;
22710
+ if (wire || !sel.iface || !rec) {
22438
22711
  const box = new THREE.Box3();
22439
- for (const r of recs) box.expandByObject(r.obj);
22712
+ for (const o of objs) box.expandByObject(o);
22440
22713
  if (box.isEmpty()) return;
22441
22714
  const size = box.getSize(new THREE.Vector3());
22442
22715
  const grow = Math.max(size.length() * .5, 40);
@@ -22518,6 +22791,11 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22518
22791
  if (model.assembly.mates.some((m) => m.linkId === key)) setSel({ mate: key });
22519
22792
  return;
22520
22793
  }
22794
+ if (model.assembly.harnesses.some((hp) => hp.key === key)) {
22795
+ setSel({ harness: key });
22796
+ look.current = focusOn.look !== false && harnessObjs.current.has(key);
22797
+ return;
22798
+ }
22521
22799
  const has = (k) => model.bodies.some((b) => b.key === k);
22522
22800
  const holder = model.within.get(key);
22523
22801
  const parts = key.split("/");
@@ -22532,7 +22810,8 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22532
22810
  look.current = focusOn.look !== false;
22533
22811
  }, [focusOn, model]);
22534
22812
  useEffect(() => {
22535
- if (!look.current || sel.body === void 0 || !selectionRecs(sel.body).length) return;
22813
+ if (!look.current) return;
22814
+ if (!(sel.body === void 0 && sel.harness !== void 0 && harnessObjs.current.has(sel.harness)) && (sel.body === void 0 || !selectionRecs(sel.body).length)) return;
22536
22815
  look.current = false;
22537
22816
  focus();
22538
22817
  handle.current?.renderOnce();
@@ -22571,11 +22850,11 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22571
22850
  options: [{
22572
22851
  id: "assembled",
22573
22852
  icon: "box",
22574
- label: "Assembled: every module where its links put it"
22853
+ label: "Assembled"
22575
22854
  }, {
22576
22855
  id: "exploded",
22577
22856
  icon: "layers",
22578
- label: "Exploded: apart in assembly order, bottom to top"
22857
+ label: "Exploded"
22579
22858
  }],
22580
22859
  value: exploded ? "exploded" : "assembled",
22581
22860
  onChange: (v) => toggleExploded(v === "exploded")
@@ -22629,7 +22908,7 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22629
22908
  }),
22630
22909
  (model?.assembly.hardware.length ?? 0) > 0 && /* @__PURE__ */ jsx(IconButton, {
22631
22910
  icon: "wrench",
22632
- label: "Mounting hardware from the harnesses' fastener stacks",
22911
+ label: "Mounting hardware",
22633
22912
  tooltipSide: "top",
22634
22913
  pressed: showHardware,
22635
22914
  onClick: () => setShowHardware(!showHardware)
@@ -22654,7 +22933,7 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22654
22933
  children: [
22655
22934
  /* @__PURE__ */ jsx(IconButton, {
22656
22935
  icon: "scissors",
22657
- label: "Section: cut the model with a plane",
22936
+ label: "Section",
22658
22937
  tooltipSide: "top",
22659
22938
  pressed: Boolean(section),
22660
22939
  onClick: () => setSection(section ? null : {
@@ -22728,7 +23007,7 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22728
23007
  }),
22729
23008
  /* @__PURE__ */ jsx(IconButton, {
22730
23009
  icon: "scan-eye",
22731
- label: shells ? "X-ray: see through bodies that hold others" : "X-ray: nothing to see through here (no body holds another)",
23010
+ label: shells ? "X-ray" : "X-ray: nothing to see through",
22732
23011
  tooltipSide: "top",
22733
23012
  pressed: xray && shells > 0,
22734
23013
  "aria-disabled": !shells || void 0,
@@ -22764,7 +23043,7 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22764
23043
  }),
22765
23044
  /* @__PURE__ */ jsx(IconButton, {
22766
23045
  icon: "crosshair",
22767
- label: "Look along the selected interface's normal",
23046
+ label: "Look along normal",
22768
23047
  tooltipSide: "top",
22769
23048
  disabled: !sel.body,
22770
23049
  onClick: focus
@@ -22814,7 +23093,7 @@ function CadView({ system, lookup, source, diagnostics = [], hostDiagnostics = [
22814
23093
  children: [/* @__PURE__ */ jsxs("div", {
22815
23094
  className: "cad-caption-brand",
22816
23095
  children: [/* @__PURE__ */ jsx("span", { children: "UHD" }), " VIEWER · 5-INCH 6S QUADCOPTER"]
22817
- }), caption && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
23096
+ }), caption && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
22818
23097
  className: "cad-caption-title",
22819
23098
  children: caption[0]
22820
23099
  }), caption[1] && /* @__PURE__ */ jsx("div", {
@@ -22879,7 +23158,7 @@ function FrameTable({ frame }) {
22879
23158
  return /* @__PURE__ */ jsxs("div", {
22880
23159
  className: "kv-grid",
22881
23160
  children: [
22882
- frame.artifact && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
23161
+ frame.artifact && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
22883
23162
  className: "k",
22884
23163
  children: "body"
22885
23164
  }), /* @__PURE__ */ jsx("span", {
@@ -22949,7 +23228,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
22949
23228
  }), /* @__PURE__ */ jsx(ScrollArea, {
22950
23229
  className: "inspector-scroll",
22951
23230
  contentClassName: "inspector-body",
22952
- children: body ? /* @__PURE__ */ jsxs(Fragment, { children: [
23231
+ children: body ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
22953
23232
  /* @__PURE__ */ jsx("h3", {
22954
23233
  className: "cad-title",
22955
23234
  children: body.def.name
@@ -22960,7 +23239,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
22960
23239
  }),
22961
23240
  /* @__PURE__ */ jsx("div", {
22962
23241
  className: "cad-sub",
22963
- children: body.via ? /* @__PURE__ */ jsxs(Fragment, { children: ["placed by link ", /* @__PURE__ */ jsx("button", {
23242
+ children: body.via ? /* @__PURE__ */ jsxs(Fragment$1, { children: ["placed by link ", /* @__PURE__ */ jsx("button", {
22964
23243
  className: "linklike",
22965
23244
  onClick: () => onSel({ mate: body.via }),
22966
23245
  children: body.via
@@ -23024,7 +23303,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23024
23303
  ]
23025
23304
  }, a.id))
23026
23305
  })
23027
- ] }) : harness ? /* @__PURE__ */ jsxs(Fragment, { children: [
23306
+ ] }) : harness ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
23028
23307
  /* @__PURE__ */ jsx("h3", {
23029
23308
  className: "cad-title",
23030
23309
  children: harness.def.name
@@ -23048,7 +23327,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23048
23327
  className: "v",
23049
23328
  children: cableSummary(harness, hm)
23050
23329
  }),
23051
- hm && /* @__PURE__ */ jsxs(Fragment, { children: [
23330
+ hm && /* @__PURE__ */ jsxs(Fragment$1, { children: [
23052
23331
  /* @__PURE__ */ jsx("span", {
23053
23332
  className: "k",
23054
23333
  children: "routed"
@@ -23134,7 +23413,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23134
23413
  ]
23135
23414
  }, a.id))
23136
23415
  })
23137
- ] }) : mate ? /* @__PURE__ */ jsxs(Fragment, { children: [
23416
+ ] }) : mate ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
23138
23417
  /* @__PURE__ */ jsx("h3", {
23139
23418
  className: "cad-title",
23140
23419
  children: mate.linkId
@@ -23180,7 +23459,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23180
23459
  })
23181
23460
  ]
23182
23461
  })
23183
- ] }) : route ? /* @__PURE__ */ jsxs(Fragment, { children: [
23462
+ ] }) : route ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
23184
23463
  /* @__PURE__ */ jsx("h3", {
23185
23464
  className: "cad-title",
23186
23465
  children: route.linkId
@@ -23212,7 +23491,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23212
23491
  ")"
23213
23492
  ]
23214
23493
  })
23215
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
23494
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
23216
23495
  /* @__PURE__ */ jsxs("div", {
23217
23496
  className: "section-title",
23218
23497
  children: ["PLACED FROM LINKS · ", model.assembly.placements.length]
@@ -23231,7 +23510,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23231
23510
  })]
23232
23511
  }, b.key))
23233
23512
  }),
23234
- findings.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
23513
+ findings.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
23235
23514
  className: "section-title",
23236
23515
  children: ["FINDINGS ON THE MODEL · ", findings.length]
23237
23516
  }), findings.map((f) => /* @__PURE__ */ jsxs("button", {
@@ -23309,7 +23588,7 @@ function CadInspector({ model, sel, onSel, direction, findings, resolve }) {
23309
23588
  ]
23310
23589
  })]
23311
23590
  }, r.linkId)),
23312
- unrouted.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
23591
+ unrouted.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
23313
23592
  className: "section-title",
23314
23593
  children: ["UNROUTED · ", unrouted.length]
23315
23594
  }), /* @__PURE__ */ jsxs("div", {
@@ -23336,8 +23615,8 @@ function BodyContents({ model, body, sel, onSel, resolve }) {
23336
23615
  const named = Object.entries(manifest?.bodies ?? {});
23337
23616
  const cad = cadOutputs(body.def);
23338
23617
  const circuit = circuitOutputs(body.def);
23339
- return /* @__PURE__ */ jsxs(Fragment, { children: [
23340
- components.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
23618
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
23619
+ components.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
23341
23620
  className: "section-title",
23342
23621
  children: ["COMPONENTS · ", components.length]
23343
23622
  }), /* @__PURE__ */ jsx("div", {
@@ -23357,7 +23636,7 @@ function BodyContents({ model, body, sel, onSel, resolve }) {
23357
23636
  })]
23358
23637
  }, c.id))
23359
23638
  })] }),
23360
- named.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
23639
+ named.length > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
23361
23640
  /* @__PURE__ */ jsxs("div", {
23362
23641
  className: "section-title",
23363
23642
  children: ["BODIES · ", named.length]
@@ -23380,7 +23659,7 @@ function BodyContents({ model, body, sel, onSel, resolve }) {
23380
23659
  children: "Exploding separates them along the line between their centres."
23381
23660
  })
23382
23661
  ] }),
23383
- cad && cad.exports.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
23662
+ cad && cad.exports.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
23384
23663
  className: "section-title",
23385
23664
  children: ["MANUFACTURING EXPORTS · ", cad.exports.length]
23386
23665
  }), /* @__PURE__ */ jsx("div", {
@@ -23477,13 +23756,27 @@ function Legend({ title, children, className, compact, ...rest }) {
23477
23756
  }), children]
23478
23757
  });
23479
23758
  }
23480
- function LegendItem({ color, label, shape = "line", onSelect, pressed, dimmed, absent, hint, hintShortcut, compact }) {
23759
+ function LegendItem({ color, label, shape = "line", onSelect, pressed, dimmed, absent, hint, hintShortcut, compact, meaning }) {
23760
+ const two = !compact && !!meaning;
23481
23761
  const swatch = /* @__PURE__ */ jsx("span", {
23482
23762
  "aria-hidden": true,
23483
- className: cx("shrink-0", shape === "dot" ? "size-2 rounded-full" : "h-[3px] w-4 rounded-sm"),
23763
+ className: cx("shrink-0", shape === "dot" ? "size-2 rounded-full" : two ? "h-1 w-[18px] rounded-sm" : "h-[3px] w-4 rounded-sm"),
23484
23764
  style: shape === "dash" ? { background: `repeating-linear-gradient(90deg, ${color} 0 4px, transparent 4px 7px)` } : { background: color }
23485
23765
  });
23486
23766
  const name = typeof label === "string" ? label : void 0;
23767
+ const text = two ? /* @__PURE__ */ jsxs("span", {
23768
+ className: "flex min-w-0 flex-1 flex-col",
23769
+ children: [/* @__PURE__ */ jsx("span", {
23770
+ className: "truncate text-[11.5px] leading-[15px] text-fg",
23771
+ children: label
23772
+ }), /* @__PURE__ */ jsx("span", {
23773
+ className: "truncate text-[10px] leading-[13px] text-fg-muted",
23774
+ children: meaning
23775
+ })]
23776
+ }) : /* @__PURE__ */ jsx("span", {
23777
+ className: "min-w-0 flex-1 truncate",
23778
+ children: label
23779
+ });
23487
23780
  if (!onSelect) {
23488
23781
  if (compact) return /* @__PURE__ */ jsx("span", {
23489
23782
  role: "img",
@@ -23493,8 +23786,8 @@ function LegendItem({ color, label, shape = "line", onSelect, pressed, dimmed, a
23493
23786
  children: swatch
23494
23787
  });
23495
23788
  return /* @__PURE__ */ jsxs("div", {
23496
- className: "flex items-center gap-2 text-[9.5px] text-fg-muted",
23497
- children: [swatch, /* @__PURE__ */ jsx("span", { children: label })]
23789
+ className: cx("flex items-center text-[9.5px] text-fg-muted", two ? "gap-2.5" : "gap-2"),
23790
+ children: [swatch, text]
23498
23791
  });
23499
23792
  }
23500
23793
  return /* @__PURE__ */ jsxs("button", {
@@ -23504,13 +23797,10 @@ function LegendItem({ color, label, shape = "line", onSelect, pressed, dimmed, a
23504
23797
  "aria-label": compact ? name : void 0,
23505
23798
  "data-tooltip": compact ? name : hint,
23506
23799
  "data-tooltip-shortcut": hintShortcut,
23507
- "data-tooltip-side": compact ? "top" : "right",
23800
+ "data-tooltip-side": compact ? "top" : "left",
23508
23801
  onClick: (e) => !absent && onSelect(e.shiftKey || e.ctrlKey || e.metaKey),
23509
- className: cx("uhd-legend-item flex min-h-[18px] items-center rounded-sm border-0 bg-transparent font-ui text-[9.5px] text-fg-muted transition-[opacity,background-color]", compact ? "h-[18px] w-6 justify-center p-0" : "-mx-1.5 -my-0.5 gap-2 px-1.5 py-0.5 text-left", "focus-visible:outline focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-focus", absent ? "cursor-default opacity-40" : "cursor-pointer hover:bg-hover hover:text-fg", pressed && "bg-accent-tint text-fg outline outline-1 -outline-offset-1 outline-contrast-active", dimmed && !absent && "opacity-55"),
23510
- children: [swatch, !compact && /* @__PURE__ */ jsx("span", {
23511
- className: "min-w-0 flex-1 truncate",
23512
- children: label
23513
- })]
23802
+ className: cx("uhd-legend-item flex min-h-[18px] items-center rounded-sm border-0 bg-transparent font-ui text-[9.5px] text-fg-muted transition-[opacity,background-color]", compact ? "h-[18px] w-6 justify-center p-0" : two ? "-mx-1.5 gap-2.5 px-1.5 py-1 text-left" : "-mx-1.5 -my-0.5 gap-2 px-1.5 py-0.5 text-left", "focus-visible:outline focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-focus", absent ? "cursor-default opacity-40" : "cursor-pointer hover:bg-hover hover:text-fg", pressed && "bg-accent-tint text-fg outline outline-1 -outline-offset-1 outline-contrast-active", dimmed && !absent && "opacity-55"),
23803
+ children: [swatch, !compact && text]
23514
23804
  });
23515
23805
  }
23516
23806
  /** Compact items side by side, `columns` to a row: a legend as narrow as its swatches. */
@@ -23524,22 +23814,49 @@ function LegendSwatches({ columns = 3, children, label }) {
23524
23814
  });
23525
23815
  }
23526
23816
  /**
23527
- * The room the legend has before the toolbar (px): from its left edge to
23528
- * the toolbar's, less the gap. The toolbar is centred, so its button for the
23529
- * legend moves its left edge by half the button: that half is given back,
23530
- * and the room is the same whether or not the button is in it.
23817
+ * A folded legend: a narrow tab at the right edge, a chevron and the
23818
+ * colours it explains as dots. A click opens the legend; its label names it.
23819
+ */
23820
+ function LegendTab({ dots, className, ...rest }) {
23821
+ return /* @__PURE__ */ jsxs("button", {
23822
+ type: "button",
23823
+ ...rest,
23824
+ className: cx("uhd-legend-tab group flex w-9 cursor-pointer flex-col items-center gap-3 rounded-l-panel border border-r-0 border-line bg-surface px-0 py-3 text-fg-muted shadow-panel outline outline-1 -outline-offset-1 outline-contrast transition-colors", "hover:text-fg hover:outline-contrast-active focus-visible:outline-focus", className),
23825
+ children: [/* @__PURE__ */ jsx(Icon, {
23826
+ name: "chevron-left",
23827
+ size: 16,
23828
+ strokeWidth: 1.75,
23829
+ "aria-hidden": true
23830
+ }), /* @__PURE__ */ jsx("span", {
23831
+ "aria-hidden": true,
23832
+ className: "flex flex-col items-center gap-1.5",
23833
+ children: dots.map((d) => /* @__PURE__ */ jsx("span", {
23834
+ className: cx("size-1.5 rounded-full transition-opacity", d.dimmed && "opacity-35"),
23835
+ style: { background: d.color }
23836
+ }, d.key))
23837
+ })]
23838
+ });
23839
+ }
23840
+ /** Below this canvas height (px) the legend folds by itself: open, it would cover much of the drawing. */
23841
+ var LEGEND_SHORT = 500;
23842
+ /**
23843
+ * The room the legend has before the toolbar (px): from the toolbar's right
23844
+ * edge to the legend's, less the gap. The toolbar is centred, so its button
23845
+ * for the legend moves its right edge by half the button: that half is taken
23846
+ * back, and the room is the same whether or not the button is in it.
23531
23847
  */
23532
23848
  function legendRoom(m) {
23533
23849
  if (!m.toolbar) return Infinity;
23534
- return m.toolbar.left + (m.slot ?? 0) / 2 - m.left - 8;
23850
+ return m.right - (m.toolbar.right - (m.slot ?? 0) / 2) - 8;
23535
23851
  }
23536
23852
  var overlapsY = (a, b) => a.top < b.bottom && b.top < a.bottom;
23537
23853
  /**
23538
23854
  * The largest form that does not run into the toolbar. A form fits when it is
23539
23855
  * clear of the toolbar's height, or narrower than the room. Moving up to a
23540
- * larger form than `current` takes `LEGEND_HYSTERESIS` more.
23856
+ * larger form than `current` takes `LEGEND_HYSTERESIS` more. Folded
23857
+ * (`folded`), the tab is the only form: "full" while it fits, else "button".
23541
23858
  */
23542
- function legendStage(m, current = "full") {
23859
+ function legendStage(m, current = "full", folded = false) {
23543
23860
  const room = legendRoom(m);
23544
23861
  const order = [
23545
23862
  "full",
@@ -23547,16 +23864,25 @@ function legendStage(m, current = "full") {
23547
23864
  "button"
23548
23865
  ];
23549
23866
  const at = order.indexOf(current);
23550
- const fits = (stage) => {
23551
- const box = m[stage];
23867
+ const fits = (box, stage) => {
23552
23868
  if (!m.toolbar || !overlapsY(box, m.toolbar)) return true;
23553
23869
  const spare = order.indexOf(stage) < at ? 6 : 0;
23554
23870
  return box.right - box.left + spare <= room;
23555
23871
  };
23556
- if (fits("full")) return "full";
23557
- if (fits("compact")) return "compact";
23872
+ if (folded) return !m.tab || fits(m.tab, "full") ? "full" : "button";
23873
+ if (fits(m.full, "full")) return "full";
23874
+ if (fits(m.compact, "compact")) return "compact";
23558
23875
  return "button";
23559
23876
  }
23877
+ /**
23878
+ * Whether the legend shows folded: the user folded it, or the canvas is
23879
+ * shorter than `LEGEND_SHORT` and the user has not opened it since it became
23880
+ * so (`peek`).
23881
+ */
23882
+ function legendFolded(open, canvasHeight, peek) {
23883
+ if (!open) return true;
23884
+ return canvasHeight > 0 && canvasHeight < 500 && !peek;
23885
+ }
23560
23886
  //#endregion
23561
23887
  //#region src/viewer/views.tsx
23562
23888
  /**
@@ -23566,7 +23892,7 @@ function legendStage(m, current = "full") {
23566
23892
  * none of them is app chrome.
23567
23893
  */
23568
23894
  /** The diagram of the current level: modules, interfaces, links and harnesses. */
23569
- function DiagramCanvas({ toolbar = true, legend = true, coveredRight, missingHint, className }) {
23895
+ function DiagramCanvas({ toolbar = true, legend = true, coveredRight, missingHint, emptyAction, className }) {
23570
23896
  const c = useViewer();
23571
23897
  const covered = coveredRight ?? Boolean(c.panel);
23572
23898
  return /* @__PURE__ */ jsxs("main", {
@@ -23608,16 +23934,38 @@ function DiagramCanvas({ toolbar = true, legend = true, coveredRight, missingHin
23608
23934
  viewport: c.viewport,
23609
23935
  onViewport: c.onViewport
23610
23936
  }, `${c.scenario.id}/${c.levelKey}/${c.viewportsSeq}`),
23611
- c.drawn.nodes.length === 0 && /* @__PURE__ */ jsxs("div", {
23937
+ levelModules(c.vm.nodes).length === 0 && (emptyAction ? /* @__PURE__ */ jsx("div", {
23938
+ className: cx("canvas-empty", covered && "covered-right"),
23939
+ role: "status",
23940
+ "aria-label": `${c.levelDef.name}: no modules`,
23941
+ children: /* @__PURE__ */ jsxs("button", {
23942
+ type: "button",
23943
+ className: "canvas-empty-add",
23944
+ "data-add-part": "",
23945
+ onClick: emptyAction.onClick,
23946
+ children: [/* @__PURE__ */ jsx("span", {
23947
+ className: "canvas-empty-box",
23948
+ "aria-hidden": true,
23949
+ children: /* @__PURE__ */ jsx(Icon, {
23950
+ name: "plus",
23951
+ size: 48,
23952
+ strokeWidth: 1.5
23953
+ })
23954
+ }), /* @__PURE__ */ jsx("span", {
23955
+ className: "canvas-empty-label",
23956
+ children: emptyAction.label
23957
+ })]
23958
+ })
23959
+ }) : /* @__PURE__ */ jsxs("div", {
23612
23960
  className: cx("canvas-empty", covered && "covered-right"),
23613
23961
  role: "status",
23614
23962
  children: [/* @__PURE__ */ jsxs("div", {
23615
23963
  className: "canvas-empty-title",
23616
23964
  children: [c.levelDef.name, " has no modules yet"]
23617
23965
  }), /* @__PURE__ */ jsx("p", { children: "Add child modules to its definition to see them here." })]
23618
- }),
23966
+ })),
23619
23967
  toolbar && /* @__PURE__ */ jsx(DiagramToolbar, { coveredRight: covered }),
23620
- legend && /* @__PURE__ */ jsx(DiagramLegend, {}),
23968
+ legend && /* @__PURE__ */ jsx(DiagramLegend, { coveredRight: covered }),
23621
23969
  c.pair && /* @__PURE__ */ jsx(PairView, {
23622
23970
  vm: c.vm,
23623
23971
  link: c.pair,
@@ -23708,7 +24056,7 @@ function MissingNotice({ hint, className }) {
23708
24056
  icon: "x",
23709
24057
  size: "sm",
23710
24058
  tooltipSide: "bottom",
23711
- label: "Hide this notice",
24059
+ label: "Hide",
23712
24060
  onClick: () => setDismissed(key),
23713
24061
  className: "size-6 shrink-0"
23714
24062
  })
@@ -23736,11 +24084,11 @@ var CLASSIFICATION_OPTIONS = [
23736
24084
  var DETAIL_OPTIONS = [{
23737
24085
  id: "bundled",
23738
24086
  icon: "minus",
23739
- label: "Basic: one line per link"
24087
+ label: "One line per link"
23740
24088
  }, {
23741
24089
  id: "conductors",
23742
24090
  icon: "rows-3",
23743
- label: "Ribbon: every conductor"
24091
+ label: "Every conductor"
23744
24092
  }];
23745
24093
  /**
23746
24094
  * The pointer's tool: select (a drag on the empty canvas draws a selection
@@ -23782,7 +24130,7 @@ function ViewButtons({ tooltipSide = "top" }) {
23782
24130
  });
23783
24131
  }
23784
24132
  function ViewButtonsView({ onFit, onZoomIn, onZoomOut, tooltipSide }) {
23785
- return /* @__PURE__ */ jsxs(Fragment, { children: [
24133
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
23786
24134
  /* @__PURE__ */ jsx(IconButton, {
23787
24135
  icon: "maximize",
23788
24136
  label: "Fit view",
@@ -23818,7 +24166,7 @@ var DiagramToolbarView = memo(function DiagramToolbarView(p) {
23818
24166
  tooltipSide: "top"
23819
24167
  }),
23820
24168
  /* @__PURE__ */ jsx(ToolbarSeparator, {}),
23821
- p.controls && /* @__PURE__ */ jsxs(Fragment, { children: [
24169
+ p.controls && /* @__PURE__ */ jsxs(Fragment$1, { children: [
23822
24170
  /* @__PURE__ */ jsx(ToggleGroup, {
23823
24171
  label: "Classification",
23824
24172
  tooltipSide: "top",
@@ -23837,7 +24185,7 @@ var DiagramToolbarView = memo(function DiagramToolbarView(p) {
23837
24185
  /* @__PURE__ */ jsx(IconButton, {
23838
24186
  icon: "arrow-down-to-line",
23839
24187
  tooltipSide: "top",
23840
- label: p.showGrounds ? "Draw grounds as symbols" : "Draw grounds as wires",
24188
+ label: p.showGrounds ? "Grounds as symbols" : "Grounds as wires",
23841
24189
  pressed: p.showGrounds,
23842
24190
  onClick: () => p.onGrounds(!p.showGrounds)
23843
24191
  }),
@@ -23852,7 +24200,7 @@ var DiagramToolbarView = memo(function DiagramToolbarView(p) {
23852
24200
  /* @__PURE__ */ jsx(ToolbarSeparator, {}),
23853
24201
  /* @__PURE__ */ jsx(IconButton, {
23854
24202
  icon: "rotate-ccw",
23855
- label: "Reset layout of this level",
24203
+ label: "Reset layout",
23856
24204
  tooltipSide: "top",
23857
24205
  onClick: p.onReset
23858
24206
  })
@@ -23887,19 +24235,19 @@ function DiagramToolbar({ coveredRight, className, controls = true, legend }) {
23887
24235
  });
23888
24236
  }
23889
24237
  var LegendItemsView = memo(function LegendItemsView({ mode, present, isolated, onIsolate, compact }) {
23890
- const what = mode === "signal" ? "signal type" : mode === "domain" ? "domain" : "state";
23891
- return /* @__PURE__ */ jsx(Fragment, { children: legendFor(mode, present).map((item) => {
24238
+ return /* @__PURE__ */ jsx(Fragment$1, { children: legendFor(mode, present).map((item) => {
23892
24239
  const pressed = isolated.has(item.key);
23893
24240
  const absent = !present.has(item.key) && !pressed;
23894
24241
  return /* @__PURE__ */ jsx(LegendItem, {
23895
24242
  color: item.color,
23896
24243
  label: item.label,
24244
+ meaning: item.meaning,
23897
24245
  onSelect: (add) => onIsolate(item.key, add),
23898
24246
  pressed,
23899
24247
  dimmed: isolated.size > 0 && !pressed,
23900
24248
  absent,
23901
- hint: absent ? `Nothing on this level is of this ${what}` : pressed ? isolated.size > 1 ? "Stop isolating this one" : "Show everything again" : `Isolate: fade everything that is not of this ${what}`,
23902
- hintShortcut: absent ? void 0 : pressed && isolated.size === 1 ? "Esc" : "Shift+click: hold several",
24249
+ hint: absent ? "Not on this level" : pressed ? isolated.size > 1 ? "Release" : "Show all" : "Isolate",
24250
+ hintShortcut: absent ? void 0 : pressed && isolated.size === 1 ? "Esc" : "Shift+click: several",
23903
24251
  compact
23904
24252
  }, item.key);
23905
24253
  }) });
@@ -23908,8 +24256,9 @@ var LegendItemsView = memo(function LegendItemsView({ mode, present, isolated, o
23908
24256
  * The entries of the current classification's legend, as buttons: a click
23909
24257
  * isolates one (its links stay, with the interfaces and modules they touch;
23910
24258
  * the rest of the level fades), a click on it again shows everything, a
23911
- * click on another switches, and Shift, Ctrl or Cmd holds several. For a
23912
- * shell that draws its own legend card.
24259
+ * click on another switches, and Shift, Ctrl or Cmd holds several. Each
24260
+ * reads as its name and, under it, what it stands for. For a shell that
24261
+ * draws its own legend card.
23913
24262
  */
23914
24263
  function DiagramLegendItems({ compact, columns = 3 } = {}) {
23915
24264
  const c = useViewer();
@@ -23920,7 +24269,10 @@ function DiagramLegendItems({ compact, columns = 3 } = {}) {
23920
24269
  onIsolate: c.onIsolate,
23921
24270
  compact
23922
24271
  });
23923
- if (!compact) return items;
24272
+ if (!compact) return /* @__PURE__ */ jsx("div", {
24273
+ className: "flex flex-col gap-1",
24274
+ children: items
24275
+ });
23924
24276
  return /* @__PURE__ */ jsx(LegendSwatches, {
23925
24277
  columns,
23926
24278
  label: CLASSIFICATIONS.find((x) => x.id === c.mode)?.label,
@@ -23935,66 +24287,127 @@ function IsolationClear({ className }) {
23935
24287
  icon: "x",
23936
24288
  size: "sm",
23937
24289
  tooltipSide: "top",
23938
- label: "Show everything again",
24290
+ label: "Show all",
23939
24291
  shortcut: "Esc",
23940
24292
  onClick: c.clearIsolated,
23941
24293
  className: cx("size-6", className)
23942
24294
  });
23943
24295
  }
24296
+ /** What a legend that folds gives the content it draws: the way to fold it (`FittedLegend`). */
24297
+ var LegendFoldContext = createContext(null);
24298
+ /**
24299
+ * The legend's fold button, at the right of its header: the legend becomes a
24300
+ * tab at the right edge. Nothing where the legend does not fold (the
24301
+ * toolbar's popover).
24302
+ */
24303
+ function LegendFold({ className }) {
24304
+ const fold = useContext(LegendFoldContext);
24305
+ if (!fold) return null;
24306
+ return /* @__PURE__ */ jsx(IconButton, {
24307
+ icon: "chevron-right",
24308
+ size: "sm",
24309
+ tooltipSide: "top",
24310
+ label: "Hide legend",
24311
+ onClick: fold,
24312
+ className: cx("size-6", className)
24313
+ });
24314
+ }
23944
24315
  /**
23945
24316
  * What the legend shows in a form: `full` the title and the entries with their
23946
- * labels; `compact` the classification's icon and the swatches alone (each
23947
- * named by its label).
24317
+ * names and meanings; `compact` the classification's icon and the swatches
24318
+ * alone (each named by its label).
23948
24319
  */
23949
24320
  function DiagramLegendBody({ form }) {
23950
24321
  const c = useViewer();
23951
24322
  const title = CLASSIFICATIONS.find((x) => x.id === c.mode)?.label;
23952
24323
  if (form === "compact") {
23953
24324
  const icon = CLASSIFICATION_OPTIONS.find((o) => o.id === c.mode)?.icon ?? "layers";
23954
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
24325
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
23955
24326
  className: "flex min-h-6 items-center justify-between gap-1",
23956
- children: [/* @__PURE__ */ jsx(Icon, {
23957
- name: icon,
23958
- size: 13,
23959
- strokeWidth: 1.75,
23960
- className: "text-fg-muted",
23961
- "aria-label": title,
23962
- role: "img"
23963
- }), /* @__PURE__ */ jsx(IsolationClear, { className: "-my-1 -mr-1.5" })]
24327
+ children: [
24328
+ /* @__PURE__ */ jsx(Icon, {
24329
+ name: icon,
24330
+ size: 13,
24331
+ strokeWidth: 1.75,
24332
+ className: "text-fg-muted",
24333
+ "aria-label": title,
24334
+ role: "img"
24335
+ }),
24336
+ /* @__PURE__ */ jsx(IsolationClear, { className: "-my-1" }),
24337
+ /* @__PURE__ */ jsx(LegendFold, { className: "-my-1 -mr-1.5" })
24338
+ ]
23964
24339
  }), /* @__PURE__ */ jsx(DiagramLegendItems, {
23965
24340
  compact: true,
23966
24341
  columns: 3
23967
24342
  })] });
23968
24343
  }
23969
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
23970
- className: "mb-0.5 flex min-h-6 items-center justify-between gap-2",
24344
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
24345
+ className: "legend-head -mx-3 -mt-1 mb-1 flex min-h-7 items-center justify-between gap-2 border-b border-line-subtle px-3 pb-1",
23971
24346
  children: [/* @__PURE__ */ jsx("span", {
23972
24347
  className: "font-label text-[10px] tracking-[0.8px] text-fg-muted uppercase",
23973
24348
  children: title
23974
- }), /* @__PURE__ */ jsx(IsolationClear, { className: "-my-1 -mr-1.5" })]
24349
+ }), /* @__PURE__ */ jsxs("span", {
24350
+ className: "-mr-1.5 flex items-center",
24351
+ children: [/* @__PURE__ */ jsx(IsolationClear, {}), /* @__PURE__ */ jsx(LegendFold, {})]
24352
+ })]
23975
24353
  }), /* @__PURE__ */ jsx(DiagramLegendItems, {})] });
23976
24354
  }
23977
24355
  /** What the link colours mean in the current classification; a click on an entry isolates it. It gives way to the toolbar as the canvas narrows (`FittedLegend`). */
23978
- function DiagramLegend({ className }) {
24356
+ function DiagramLegend({ className, coveredRight }) {
23979
24357
  return /* @__PURE__ */ jsx(FittedLegend, {
23980
24358
  label: "Legend",
23981
24359
  className,
24360
+ coveredRight,
23982
24361
  render: (form) => /* @__PURE__ */ jsx(DiagramLegendBody, { form })
23983
24362
  });
23984
24363
  }
23985
24364
  /**
23986
- * A legend card at the canvas's bottom left that stays there and gives way
24365
+ * A legend card at the canvas's bottom right that stays there and gives way
23987
24366
  * to the bottom toolbar (ui/legendFit.ts): the full form while it fits beside
23988
24367
  * the toolbar, then the compact one, and at its smallest none, the toolbar
23989
- * then offering it behind a button (`LegendToolbarButton`). It measures the
23990
- * room it has (a `ResizeObserver` on the canvas, the toolbar and both forms)
23991
- * from hidden copies of the two forms, so it never overlaps the toolbar.
23992
- * `render` draws a form; the card is a sibling of the toolbar (`.toolbar`).
24368
+ * then offering it behind a button (`LegendToolbarButton`). Folded, it is a
24369
+ * tab at the right edge with a dot per colour, which opens it; a canvas
24370
+ * shorter than `LEGEND_SHORT` folds it until the user opens it there. It
24371
+ * measures the room it has (a `ResizeObserver` on the canvas, the toolbar and
24372
+ * every form) from hidden copies of the forms, so it never overlaps the
24373
+ * toolbar. `render` draws a form; the card is a sibling of the toolbar
24374
+ * (`.toolbar`).
23993
24375
  */
23994
- function FittedLegend({ label, render, className }) {
23995
- const { legendStage: stage, setLegendStage, panel } = useViewer();
24376
+ function FittedLegend({ label, render, className, open: given, onOpenChange, defaultOpen = true, coveredRight = false }) {
24377
+ const { legendStage: stage, setLegendStage, panel, mode, present, isolated } = useViewer();
24378
+ const [own, setOwn] = useState(defaultOpen);
24379
+ const open = given ?? own;
24380
+ const setOpen = (next) => {
24381
+ if (given === void 0) setOwn(next);
24382
+ onOpenChange?.(next);
24383
+ };
24384
+ const [height, setHeight] = useState(0);
24385
+ const [peek, setPeek] = useState(false);
24386
+ const short = height > 0 && height < 500;
24387
+ useEffect(() => {
24388
+ if (!short) setPeek(false);
24389
+ }, [short]);
24390
+ const folded = legendFolded(open, height, peek);
24391
+ const unfold = () => {
24392
+ if (!open) setOpen(true);
24393
+ if (short) setPeek(true);
24394
+ };
24395
+ const fold = useCallback(() => {
24396
+ if (open && short && peek) setPeek(false);
24397
+ else {
24398
+ if (given === void 0) setOwn(false);
24399
+ onOpenChange?.(false);
24400
+ }
24401
+ }, [
24402
+ open,
24403
+ short,
24404
+ peek,
24405
+ given,
24406
+ onOpenChange
24407
+ ]);
23996
24408
  const full = useRef(null);
23997
24409
  const compact = useRef(null);
24410
+ const tab = useRef(null);
23998
24411
  const current = useRef(stage);
23999
24412
  current.current = stage;
24000
24413
  const measure = useCallback(() => {
@@ -24002,27 +24415,31 @@ function FittedLegend({ label, render, className }) {
24002
24415
  const k = compact.current;
24003
24416
  const host = f?.parentElement;
24004
24417
  if (!f || !k || !host) return;
24418
+ setHeight(host.clientHeight);
24005
24419
  const toolbar = host.querySelector(":scope > .toolbar");
24006
24420
  const box = toolbar && toolbar.getClientRects().length ? toolbar.getBoundingClientRect() : null;
24007
24421
  const slotEl = toolbar?.querySelector("[data-legend-slot]");
24008
24422
  const slot = slotEl ? slotEl.getBoundingClientRect().width + (parseFloat(getComputedStyle(toolbar).columnGap) || 0) : 0;
24009
24423
  const fr = f.getBoundingClientRect();
24424
+ const tr = tab.current?.getBoundingClientRect();
24010
24425
  const next = legendStage({
24011
- left: fr.left,
24426
+ right: fr.right,
24012
24427
  full: fr,
24013
24428
  compact: k.getBoundingClientRect(),
24429
+ ...tr ? { tab: tr } : {},
24014
24430
  toolbar: box,
24015
24431
  slot
24016
- }, current.current);
24432
+ }, current.current, folded);
24017
24433
  if (next !== current.current) {
24018
24434
  current.current = next;
24019
24435
  setLegendStage(next);
24020
24436
  }
24021
- }, [setLegendStage]);
24437
+ }, [setLegendStage, folded]);
24022
24438
  useLayoutEffect(measure, [
24023
24439
  measure,
24024
24440
  panel,
24025
- stage
24441
+ stage,
24442
+ coveredRight
24026
24443
  ]);
24027
24444
  useEffect(() => {
24028
24445
  const host = full.current?.parentElement;
@@ -24031,36 +24448,66 @@ function FittedLegend({ label, render, className }) {
24031
24448
  ro.observe(host);
24032
24449
  if (full.current) ro.observe(full.current);
24033
24450
  if (compact.current) ro.observe(compact.current);
24451
+ if (tab.current) ro.observe(tab.current);
24034
24452
  const toolbar = host.querySelector(":scope > .toolbar");
24035
24453
  if (toolbar) ro.observe(toolbar);
24036
24454
  return () => ro.disconnect();
24037
24455
  }, [measure]);
24038
24456
  useEffect(() => () => setLegendStage("full"), [setLegendStage]);
24039
- return /* @__PURE__ */ jsxs(Fragment, { children: [
24040
- stage !== "button" && /* @__PURE__ */ jsx(Legend, {
24041
- role: "group",
24042
- "aria-label": label,
24043
- compact: stage === "compact",
24044
- className: cx("legend", className),
24045
- "data-legend-stage": stage,
24046
- children: render(stage)
24047
- }),
24048
- /* @__PURE__ */ jsx(Legend, {
24049
- ref: full,
24050
- "aria-hidden": true,
24051
- inert: true,
24052
- className: cx("legend-measure", className),
24053
- children: render("full")
24054
- }),
24055
- /* @__PURE__ */ jsx(Legend, {
24056
- ref: compact,
24057
- "aria-hidden": true,
24058
- inert: true,
24059
- compact: true,
24060
- className: cx("legend-measure", className),
24061
- children: render("compact")
24062
- })
24063
- ] });
24457
+ const entries = legendFor(mode, present);
24458
+ const shown = entries.filter((e) => present.has(e.key) || isolated.has(e.key));
24459
+ const dots = (shown.length ? shown : entries).slice(0, 8).map((e) => ({
24460
+ key: e.key,
24461
+ color: e.color,
24462
+ dimmed: isolated.size > 0 && !isolated.has(e.key)
24463
+ }));
24464
+ const side = cx(coveredRight && "covered-right", className);
24465
+ return /* @__PURE__ */ jsxs(LegendFoldContext.Provider, {
24466
+ value: fold,
24467
+ children: [
24468
+ stage !== "button" && (folded ? /* @__PURE__ */ jsx(LegendTab, {
24469
+ ref: tab,
24470
+ dots,
24471
+ "aria-label": label,
24472
+ "aria-expanded": false,
24473
+ "data-tooltip": label,
24474
+ "data-tooltip-side": "left",
24475
+ "data-legend-tab": "",
24476
+ onClick: unfold,
24477
+ className: cx("legend-tab", side)
24478
+ }) : /* @__PURE__ */ jsx(Legend, {
24479
+ role: "group",
24480
+ "aria-label": label,
24481
+ compact: stage === "compact",
24482
+ className: cx("legend", side),
24483
+ "data-legend-stage": stage,
24484
+ children: render(stage)
24485
+ })),
24486
+ /* @__PURE__ */ jsx(Legend, {
24487
+ ref: full,
24488
+ "aria-hidden": true,
24489
+ inert: true,
24490
+ className: cx("legend-measure", side),
24491
+ children: render("full")
24492
+ }),
24493
+ /* @__PURE__ */ jsx(Legend, {
24494
+ ref: compact,
24495
+ "aria-hidden": true,
24496
+ inert: true,
24497
+ compact: true,
24498
+ className: cx("legend-measure", side),
24499
+ children: render("compact")
24500
+ }),
24501
+ folded && stage === "button" && /* @__PURE__ */ jsx(LegendTab, {
24502
+ ref: tab,
24503
+ dots,
24504
+ "aria-hidden": true,
24505
+ inert: true,
24506
+ tabIndex: -1,
24507
+ className: cx("legend-tab legend-measure", side)
24508
+ })
24509
+ ]
24510
+ });
24064
24511
  }
24065
24512
  /**
24066
24513
  * The legend's button in the toolbar, shown only when the legend has no room
@@ -24225,7 +24672,9 @@ function ViewerNodeMenu() {
24225
24672
  * assembly): a GLB by URL, or a scene a host built (a STEP it converted), in
24226
24673
  * the 3D view's own scene, lights, materials and theme colours, with a
24227
24674
  * floating icon toolbar (fit, standard views, wireframe, x-ray, grid, turn)
24228
- * and a card with its size. Nothing here is specific to a host.
24675
+ * and, on request from the toolbar, a card with its name and size (PB-1074:
24676
+ * shown all the time it covered the model, and a Select All lit it up). Nothing
24677
+ * here is specific to a host.
24229
24678
  */
24230
24679
  var VIEWS = [
24231
24680
  {
@@ -24291,7 +24740,7 @@ function stats(root) {
24291
24740
  };
24292
24741
  }
24293
24742
  var mm = (n) => n >= 100 ? n.toFixed(0) : n >= 10 ? n.toFixed(1) : n.toFixed(2);
24294
- function ModelView({ url, object, name, detail, error, onReady, actions, className }) {
24743
+ function ModelView({ url, object, name, detail, error, onReady, actions, infoOpen = false, className }) {
24295
24744
  const host = useRef(null);
24296
24745
  const handle = useRef(null);
24297
24746
  const model = useRef(null);
@@ -24301,6 +24750,7 @@ function ModelView({ url, object, name, detail, error, onReady, actions, classNa
24301
24750
  const [grid, setGrid] = useState(true);
24302
24751
  const [spin, setSpin] = useState(false);
24303
24752
  const [view, setView] = useState("iso");
24753
+ const [showInfo, setShowInfo] = useState(infoOpen);
24304
24754
  useEffect(() => {
24305
24755
  if (!host.current) return;
24306
24756
  let h;
@@ -24458,14 +24908,16 @@ function ModelView({ url, object, name, detail, error, onReady, actions, classNa
24458
24908
  }, [spin]);
24459
24909
  const info = state.info;
24460
24910
  return /* @__PURE__ */ jsxs("div", {
24461
- className: cx("uhd-model-view relative size-full min-h-[240px] min-w-[280px] overflow-hidden bg-canvas", className),
24911
+ className: cx("uhd-model-view relative size-full min-h-[240px] min-w-[280px] overflow-hidden bg-canvas select-none", className),
24462
24912
  children: [
24463
24913
  /* @__PURE__ */ jsx("div", {
24464
24914
  ref: host,
24465
24915
  className: "absolute inset-0 [&>canvas]:block [&>canvas]:size-full"
24466
24916
  }),
24467
- (name || info) && /* @__PURE__ */ jsxs("div", {
24468
- className: "absolute top-[calc(var(--uhd-inset-top,0px)+12px)] left-3 max-w-[min(360px,calc(100%-24px))] rounded-panel border border-line bg-surface px-3 py-2 shadow-panel outline outline-1 -outline-offset-1 outline-contrast",
24917
+ showInfo && (name || info) && /* @__PURE__ */ jsxs("div", {
24918
+ role: "note",
24919
+ "aria-label": "Model info",
24920
+ className: "pointer-events-none absolute top-[calc(var(--uhd-inset-top,0px)+12px)] left-3 max-w-[min(360px,calc(100%-24px))] rounded-panel border border-line bg-surface px-3 py-2 shadow-panel outline outline-1 -outline-offset-1 outline-contrast",
24469
24921
  children: [
24470
24922
  name && /* @__PURE__ */ jsx("div", {
24471
24923
  className: "truncate font-label text-[12px] tracking-[0.06em] text-fg uppercase",
@@ -24554,7 +25006,7 @@ function ModelView({ url, object, name, detail, error, onReady, actions, classNa
24554
25006
  }),
24555
25007
  /* @__PURE__ */ jsx(IconButton, {
24556
25008
  icon: "scan-eye",
24557
- label: "X-ray: see through the model",
25009
+ label: "X-ray",
24558
25010
  tooltipSide: "top",
24559
25011
  pressed: xray,
24560
25012
  onClick: () => setXray(!xray)
@@ -24573,6 +25025,13 @@ function ModelView({ url, object, name, detail, error, onReady, actions, classNa
24573
25025
  pressed: spin,
24574
25026
  onClick: () => setSpin(!spin)
24575
25027
  }),
25028
+ (name || info) && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ToolbarSeparator, {}), /* @__PURE__ */ jsx(IconButton, {
25029
+ icon: "info",
25030
+ label: showInfo ? "Hide the model info" : "Model info: name and size",
25031
+ tooltipSide: "top",
25032
+ pressed: showInfo,
25033
+ onClick: () => setShowInfo(!showInfo)
25034
+ })] }),
24576
25035
  actions && /* @__PURE__ */ jsx(ToolbarSeparator, {}),
24577
25036
  actions
24578
25037
  ]
@@ -24902,25 +25361,30 @@ function panelState(v, fallbackOpen = true) {
24902
25361
  };
24903
25362
  }
24904
25363
  var clampWidth = (w, room = Infinity) => Math.round(Math.max(HIERARCHY_WIDTH.min, Math.min(HIERARCHY_WIDTH.max, room, w)));
24905
- /** The row that holds the wiring and hardware (harnesses, cables, fastener stacks): not a module of the system. */
25364
+ /** The row "Harnesses & links" that holds the harnesses, cables and fastener stacks: not a module of the system. */
24906
25365
  var WIRING_PATH = "#wiring";
24907
25366
  var WIRING_KINDS = /* @__PURE__ */ new Set(["harness", "fasteners"]);
24908
25367
  /**
24909
- * The tree as the panel shows it: the system's structure first, and its
24910
- * wiring and hardware (every harness and fastener stack, wherever it sits)
24911
- * folded into one group row at the bottom, `WIRING_PATH`. Without any, the
24912
- * tree as it is.
25368
+ * The tree as the panel shows it: the system's structure first, and the
25369
+ * harnesses and links (harnesses, cables and fastener stacks) folded into one
25370
+ * group row at the bottom, `WIRING_PATH`. With `level` (the instance path of
25371
+ * the level on screen, "" for the root), the group holds only that level's own
25372
+ * harnesses, not those of the modules inside it: they show once that module is
25373
+ * the level. Without `level`, every harness, wherever it sits, each with the
25374
+ * module it belongs to. Harnesses never show in the structure. Without any for
25375
+ * the group, the structure alone.
24913
25376
  */
24914
- function groupWiring(nodes) {
25377
+ function groupWiring(nodes, level) {
24915
25378
  const wiring = [];
24916
25379
  const strip = (n) => {
24917
25380
  if (!n.children.some((c) => WIRING_KINDS.has(c.kind) || c.children.length)) return n;
24918
25381
  const kept = [];
24919
- for (const c of n.children) if (WIRING_KINDS.has(c.kind)) wiring.push({
25382
+ for (const c of n.children) if (!WIRING_KINDS.has(c.kind)) kept.push(strip(c));
25383
+ else if (level === void 0) wiring.push({
24920
25384
  ...c,
24921
25385
  where: n.label
24922
25386
  });
24923
- else kept.push(strip(c));
25387
+ else if (n.path === level) wiring.push(c);
24924
25388
  return {
24925
25389
  ...n,
24926
25390
  children: kept,
@@ -24933,7 +25397,7 @@ function groupWiring(nodes) {
24933
25397
  for (const w of wiring) worst = worse(worst, w.worst);
24934
25398
  const group = {
24935
25399
  path: WIRING_PATH,
24936
- label: "Wiring & hardware",
25400
+ label: "Harnesses & links",
24937
25401
  defId: "",
24938
25402
  kind: "group",
24939
25403
  icon: "cable",
@@ -24974,21 +25438,28 @@ function markLegend(nodes) {
24974
25438
  //#endregion
24975
25439
  //#region src/viewer/hierarchy.tsx
24976
25440
  /**
24977
- * The hierarchy panel (PB-1042): the module tree of the viewer's system,
24978
- * docked at the left of the view, beside the canvas. It belongs to one viewer
24979
- * (one tab) and follows it: a click on a row selects the module on the level
24980
- * that holds it, a double-click (or Enter on the selected row) drills into it,
24981
- * a selection on the canvas opens and highlights its row, and the level the
24982
- * diagram shows is marked. It folds to a thin rail, is resized by its edge,
24983
- * filters as you type, and is a tree to the keyboard and to screen readers.
25441
+ * The hierarchy panel (PB-1042): the module tree of the viewer's system, a
25442
+ * floating card at the left of the view, beside the canvas. It belongs to one
25443
+ * viewer (one tab) and follows it: a click on a row selects the module on the
25444
+ * level that holds it, a double-click (or Enter on the selected row) drills
25445
+ * into it, a selection on the canvas opens and highlights its row, and the
25446
+ * level the diagram shows is marked. Its own button folds it away, to a
25447
+ * small icon-only button floating at the top left of the view that opens it
25448
+ * again (PB-1070); it is resized by its edge, filters as you type, and is a
25449
+ * tree to the keyboard and to screen readers.
24984
25450
  *
24985
25451
  * Its look is the viewer's tokens only (a floating card: radius, border,
24986
25452
  * shadow, faces of the theme), so a host's theme decides it. What a host keeps
24987
25453
  * per tab (open, width, open rows) comes in as `state` and goes out through
24988
25454
  * `onStateChange`.
24989
25455
  */
24990
- /** The folded panel's width. */
24991
- var RAIL_WIDTH = 36;
25456
+ /** The row of the selection on the diagram: the module selected, or the harness a selected line runs through. */
25457
+ function selectedRow(drill, vm, selection) {
25458
+ const harness = selectedHarness(vm, selection);
25459
+ return harness !== void 0 ? fullId(drill, harness) : selectedPath(drill, selection);
25460
+ }
25461
+ /** What the folded panel keeps of the view's width: nothing (its button to open it again floats over the view). */
25462
+ var RAIL_WIDTH = 0;
24992
25463
  var ROW_INDENT = 12;
24993
25464
  var SEVERITY_TEXT = {
24994
25465
  error: "text-error",
@@ -24996,16 +25467,28 @@ var SEVERITY_TEXT = {
24996
25467
  info: "text-info"
24997
25468
  };
24998
25469
  /**
24999
- * The panel's look, in one place (PB-1042; the Pencil iterations, 2026-10-21):
25000
- * docked flush at the left of the canvas, below the floating header, with a
25001
- * border at its edge; rows highlighted in the theme's selection colour. Only
25002
- * the viewer's tokens, so a host's theme decides the colours (orange only
25003
- * under the Protoboard theme). Swap these to restyle it.
25470
+ * The panel's look, in one place (PB-1042, PB-1070): a floating card like the
25471
+ * host's header, 4 px from the view's edges and below the header, with the
25472
+ * panel radius, a border all round and the panel shadow; rows highlighted in
25473
+ * the theme's selection colour. Only the viewer's tokens, so a host's theme
25474
+ * decides the colours (orange only under the Protoboard theme). Swap these to
25475
+ * restyle it.
25004
25476
  */
25005
25477
  var HIERARCHY_LOOK = {
25006
- dock: "uhd-hierarchy-dock absolute inset-y-0 left-0 z-30 flex flex-col pt-[var(--uhd-inset-top,0px)]",
25007
- panel: "relative flex min-h-0 flex-1 flex-col overflow-hidden border-t border-r border-line bg-surface font-ui text-fg",
25008
- rail: "flex min-h-0 flex-1 flex-col items-center gap-1 border-t border-r border-line bg-surface py-1",
25478
+ dock: "uhd-hierarchy-dock absolute bottom-1 left-1 z-30 flex flex-col top-[calc(var(--uhd-inset-top,0px)+4px)] transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none",
25479
+ /** The dock while it opens or folds (and while folded): slid out to the left, faded. */
25480
+ dockAway: "pointer-events-none -translate-x-[calc(100%+8px)] opacity-0",
25481
+ panel: "relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-panel border border-line bg-surface font-ui text-fg shadow-panel outline outline-1 -outline-offset-1 outline-contrast",
25482
+ /**
25483
+ * The folded panel: the icon alone, floating over the canvas at the card's
25484
+ * corner (no border, face or shadow of its own), that opens it again. On
25485
+ * hover it takes a soft rounded fill with a faint glow, in the theme's
25486
+ * toolbar hover colour (high contrast: a dashed outline instead). It fades
25487
+ * in as the card leaves and out as it comes.
25488
+ */
25489
+ folded: "uhd-hierarchy-reopen absolute left-1 z-30 top-[calc(var(--uhd-inset-top,0px)+4px)] grid size-8 cursor-pointer place-items-center rounded-md border-0 bg-transparent p-0 text-fg-muted transition-[opacity,background-color,color,box-shadow] duration-200 ease-out motion-reduce:transition-none hover:bg-hover hover:text-fg hover:shadow-[0_0_10px_1px_var(--uhd-hover)] hover:outline hover:outline-1 hover:outline-dashed hover:-outline-offset-1 hover:outline-contrast-active focus-visible:outline focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-focus",
25490
+ /** The button while it fades in or out (and while the panel is open). */
25491
+ foldedAway: "pointer-events-none opacity-0",
25009
25492
  header: "flex h-8 shrink-0 items-center gap-0.5 pr-1 pl-3",
25010
25493
  title: "min-w-0 flex-1 truncate font-label text-[11px] tracking-[0.08em] text-fg-muted uppercase",
25011
25494
  filterBox: "mx-2 mb-1.5 flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-line bg-sunken px-2 focus-within:outline focus-within:outline-1 focus-within:-outline-offset-1 focus-within:outline-focus",
@@ -25070,7 +25553,8 @@ function ViewerHierarchy({ state: given, onStateChange, onOpenChange, command, r
25070
25553
  c.diagnostics,
25071
25554
  c.hostDiagnostics
25072
25555
  ]);
25073
- const shown = useMemo(() => groupWiring(tree), [tree]);
25556
+ const level = c.drill.join("/");
25557
+ const shown = useMemo(() => groupWiring(tree, level), [tree, level]);
25074
25558
  const index = useMemo(() => hierarchyIndex(shown), [shown]);
25075
25559
  const wiring = useMemo(() => new Set(index.get("#wiring")?.children.map((n) => n.path) ?? []), [index]);
25076
25560
  const legend = useMemo(() => markLegend(tree), [tree]);
@@ -25085,8 +25569,7 @@ function ViewerHierarchy({ state: given, onStateChange, onOpenChange, command, r
25085
25569
  shut
25086
25570
  ]);
25087
25571
  const rows = useMemo(() => visibleRows(filtered.nodes, open), [filtered, open]);
25088
- const level = c.drill.join("/");
25089
- const selected = c.view === "3d" ? c.cadSelection?.kind === "node" && !c.cadSelection.id.includes("#") ? c.cadSelection.id : void 0 : selectedPath(c.drill, c.selection);
25572
+ const selected = c.view === "3d" ? c.cadSelection?.kind === "node" && !c.cadSelection.id.includes("#") ? c.cadSelection.id : void 0 : selectedRow(c.drill, c.vm, c.selection);
25090
25573
  const together = useMemo(() => new Set(c.view === "3d" ? [] : c.group.map((g) => fullId(c.drill, g))), [
25091
25574
  c.view,
25092
25575
  c.group,
@@ -25226,38 +25709,48 @@ function ViewerHierarchy({ state: given, onStateChange, onOpenChange, command, r
25226
25709
  if (menuNode && !menuNode.group) {
25227
25710
  const node = menuNode;
25228
25711
  menuItems.push({
25229
- label: "SELECT",
25712
+ label: "Select",
25713
+ icon: "mouse-pointer-2",
25230
25714
  run: () => select(node.path)
25231
25715
  });
25232
25716
  if (node.drillable) menuItems.push({
25233
- label: "OPEN AS CANVAS",
25717
+ label: "Open as canvas",
25718
+ icon: "layers",
25234
25719
  run: () => drill(node)
25235
25720
  });
25236
25721
  if (node.drillable && node.path && c.hostActions.openModule) menuItems.push({
25237
- label: "OPEN IN NEW TAB",
25722
+ label: "Open in new tab",
25723
+ icon: "square-arrow-out-up-right",
25238
25724
  run: () => c.hostActions.openModule(node.path)
25239
25725
  });
25240
25726
  if (c.hostActions.openSource) menuItems.push({
25241
- label: "OPEN SOURCE",
25727
+ label: "Open source",
25728
+ icon: "file-code",
25242
25729
  run: () => c.hostActions.openSource(node.defId, node.path || void 0)
25243
25730
  });
25244
25731
  if (node.path && c.hostActions.inspect) menuItems.push({
25245
- label: "INSPECT",
25732
+ label: "Inspect",
25733
+ icon: "scan-search",
25246
25734
  run: () => c.hostActions.inspect(node.path, node.defId)
25247
25735
  });
25248
25736
  if (node.children.length) {
25249
25737
  const below = [...hierarchyIndex([node]).values()].filter((n) => n.children.length).map((n) => n.path);
25250
25738
  const allOpen = below.every((p) => expanded.has(p));
25251
25739
  menuItems.push(allOpen ? {
25252
- label: "COLLAPSE ALL BELOW",
25740
+ label: "Collapse all below",
25741
+ icon: "chevrons-down-up",
25742
+ separator: true,
25253
25743
  run: () => setExpanded(new Set([...expanded].filter((p) => !below.includes(p))))
25254
25744
  } : {
25255
- label: "EXPAND ALL BELOW",
25745
+ label: "Expand all below",
25746
+ icon: "chevrons-up-down",
25747
+ separator: true,
25256
25748
  run: () => setExpanded(/* @__PURE__ */ new Set([...expanded, ...below]))
25257
25749
  });
25258
25750
  }
25259
25751
  if (typeof navigator !== "undefined" && navigator.clipboard) menuItems.push({
25260
- label: "COPY PATH",
25752
+ label: "Copy path",
25753
+ icon: "copy",
25261
25754
  run: () => void navigator.clipboard.writeText(node.path || node.defId).catch(() => void 0)
25262
25755
  });
25263
25756
  }
@@ -25299,13 +25792,45 @@ function ViewerHierarchy({ state: given, onStateChange, onOpenChange, command, r
25299
25792
  if (wasWide.current || kept.open) autoFolded.current = autoFolded.current && !kept.open;
25300
25793
  }, [kept.open]);
25301
25794
  const shownWidth = clampWidth(kept.width, Math.max(HIERARCHY_WIDTH.min, viewWidth * .6));
25302
- const docked = kept.open ? shownWidth : 36;
25795
+ const motion = useFold(kept.open);
25796
+ const reopen = useFold(!kept.open);
25797
+ const foldRef = useRef(null);
25798
+ const reopenRef = useRef(null);
25799
+ const cardRef = useRef(null);
25800
+ const wasOpenForFocus = useRef(kept.open);
25801
+ const focusFrom = useRef(null);
25802
+ if (wasOpenForFocus.current !== kept.open && typeof document !== "undefined") {
25803
+ const active = document.activeElement;
25804
+ focusFrom.current = !active ? null : cardRef.current?.contains(active) ? "card" : active === reopenRef.current ? "reopen" : null;
25805
+ }
25806
+ useLayoutEffect(() => {
25807
+ if (wasOpenForFocus.current === kept.open) return;
25808
+ wasOpenForFocus.current = kept.open;
25809
+ const from = focusFrom.current;
25810
+ focusFrom.current = null;
25811
+ if (!kept.open && from === "card") reopenRef.current?.focus();
25812
+ else if (kept.open && from === "reopen") foldRef.current?.focus();
25813
+ }, [kept.open]);
25814
+ const docked = kept.open ? 4 + shownWidth + 4 : 0;
25303
25815
  useLayoutEffect(() => {
25304
25816
  const parent = dock.current?.parentElement;
25305
25817
  if (!parent) return;
25306
25818
  parent.style.setProperty("--uhd-dock-left", `${docked}px`);
25307
25819
  return () => void parent.style.removeProperty("--uhd-dock-left");
25308
25820
  }, [docked]);
25821
+ const wasOpen = useRef(kept.open);
25822
+ useLayoutEffect(() => {
25823
+ if (wasOpen.current === kept.open) return;
25824
+ wasOpen.current = kept.open;
25825
+ const parent = dock.current?.parentElement;
25826
+ if (!parent) return;
25827
+ parent.classList.add("uhd-dock-moving");
25828
+ const t = setTimeout(() => parent.classList.remove("uhd-dock-moving"), 240);
25829
+ return () => {
25830
+ clearTimeout(t);
25831
+ parent.classList.remove("uhd-dock-moving");
25832
+ };
25833
+ }, [kept.open]);
25309
25834
  const room = () => Math.max(HIERARCHY_WIDTH.min, (dock.current?.parentElement?.clientWidth ?? window.innerWidth) * .6);
25310
25835
  const onResizeStart = (e) => {
25311
25836
  if (e.button !== 0) return;
@@ -25337,185 +25862,216 @@ function ViewerHierarchy({ state: given, onStateChange, onOpenChange, command, r
25337
25862
  const count = hierarchyIndex(tree).size;
25338
25863
  const rootLabel = tree[0]?.label ?? "System";
25339
25864
  const L = HIERARCHY_LOOK;
25340
- if (!kept.open) return /* @__PURE__ */ jsx("div", {
25341
- ref: dock,
25342
- className: cx(L.dock, className),
25343
- style: { width: 36 },
25344
- "data-hierarchy": "folded",
25345
- children: /* @__PURE__ */ jsxs("div", {
25346
- className: L.rail,
25347
- children: [/* @__PURE__ */ jsx(IconButton, {
25348
- icon: "panel-left-open",
25349
- size: "sm",
25350
- label: "Show the hierarchy",
25351
- tooltipSide: "right",
25352
- "aria-expanded": false,
25353
- "aria-controls": "uhd-hierarchy",
25354
- onClick: () => setKept({ open: true })
25355
- }), /* @__PURE__ */ jsx("button", {
25356
- type: "button",
25357
- onClick: () => setKept({ open: true }),
25358
- className: "mt-1 cursor-pointer border-0 bg-transparent p-0 font-label text-[10px] tracking-[0.12em] text-fg-muted uppercase [writing-mode:vertical-rl] hover:text-fg",
25359
- tabIndex: -1,
25360
- "aria-hidden": true,
25361
- children: "Hierarchy"
25362
- })]
25363
- })
25364
- });
25365
25865
  return /* @__PURE__ */ jsxs("div", {
25366
25866
  ref: dock,
25367
- className: cx(L.dock, className),
25368
- style: { width: shownWidth },
25369
- "data-hierarchy": "open",
25370
- children: [
25371
- /* @__PURE__ */ jsxs("section", {
25372
- id: "uhd-hierarchy",
25373
- "aria-label": "Hierarchy",
25374
- className: L.panel,
25375
- children: [
25376
- /* @__PURE__ */ jsxs("header", {
25377
- className: L.header,
25378
- children: [
25379
- /* @__PURE__ */ jsxs("span", {
25380
- className: L.title,
25381
- children: ["Hierarchy", /* @__PURE__ */ jsxs("span", {
25382
- className: "text-fg-faint",
25383
- children: [" · ", count]
25384
- })]
25385
- }),
25386
- /* @__PURE__ */ jsx(IconButton, {
25387
- icon: "list-filter",
25388
- size: "sm",
25389
- label: "Filter modules",
25390
- pressed: Boolean(filter),
25391
- onClick: () => filterRef.current?.focus()
25392
- }),
25393
- /* @__PURE__ */ jsx(IconButton, {
25394
- icon: "chevrons-down-up",
25395
- size: "sm",
25396
- label: "Collapse all",
25397
- onClick: () => setExpanded(/* @__PURE__ */ new Set([""]))
25398
- }),
25399
- /* @__PURE__ */ jsx(IconButton, {
25400
- icon: "panel-left-close",
25401
- size: "sm",
25402
- label: "Fold the hierarchy",
25403
- "aria-expanded": true,
25404
- "aria-controls": "uhd-hierarchy",
25405
- onClick: () => setKept({ open: false })
25406
- })
25407
- ]
25408
- }),
25409
- /* @__PURE__ */ jsxs("label", {
25410
- className: L.filterBox,
25411
- children: [
25412
- /* @__PURE__ */ jsx(Icon, {
25413
- name: "search",
25414
- size: 12,
25415
- strokeWidth: 1.75,
25416
- className: "shrink-0 text-fg-faint",
25417
- "aria-hidden": true
25418
- }),
25419
- /* @__PURE__ */ jsx("input", {
25420
- ref: filterRef,
25421
- type: "text",
25422
- value: filter,
25423
- onChange: (e) => setFilter(e.target.value),
25424
- onKeyDown: onFilterKey,
25425
- placeholder: "Filter modules",
25426
- "aria-label": "Filter the hierarchy",
25427
- "aria-controls": "uhd-hierarchy-tree",
25428
- spellCheck: false,
25429
- className: "min-w-0 flex-1 border-0 bg-transparent p-0 font-ui text-[12px] text-fg outline-none placeholder:text-fg-faint"
25430
- }),
25431
- filter && /* @__PURE__ */ jsx("button", {
25432
- type: "button",
25433
- "aria-label": "Clear the filter",
25434
- "data-tooltip": "Clear the filter",
25435
- onClick: () => (setFilter(""), filterRef.current?.focus()),
25436
- className: "grid size-5 cursor-pointer place-items-center rounded border-0 bg-transparent p-0 text-fg-muted hover:bg-hover hover:text-fg",
25437
- children: /* @__PURE__ */ jsx(Icon, {
25438
- name: "x",
25867
+ className: "contents",
25868
+ children: [reopen.mounted && /* @__PURE__ */ jsx("button", {
25869
+ ref: reopenRef,
25870
+ type: "button",
25871
+ "aria-label": "Show hierarchy",
25872
+ "aria-expanded": false,
25873
+ "aria-controls": "uhd-hierarchy",
25874
+ "data-tooltip": "Show hierarchy",
25875
+ "data-tooltip-side": "right",
25876
+ "data-hierarchy-reopen": "",
25877
+ inert: kept.open || void 0,
25878
+ onClick: () => setKept({ open: true }),
25879
+ className: cx(L.folded, !reopen.shown && L.foldedAway, className),
25880
+ children: /* @__PURE__ */ jsx(Icon, {
25881
+ name: "panel-left-open",
25882
+ size: 16,
25883
+ strokeWidth: 1.75,
25884
+ "aria-hidden": true
25885
+ })
25886
+ }), motion.mounted && /* @__PURE__ */ jsxs("div", {
25887
+ ref: cardRef,
25888
+ className: cx(L.dock, !motion.shown && L.dockAway, className),
25889
+ style: { width: shownWidth },
25890
+ "data-hierarchy": kept.open ? "open" : "folded",
25891
+ inert: !kept.open || void 0,
25892
+ "aria-hidden": !kept.open || void 0,
25893
+ children: [
25894
+ /* @__PURE__ */ jsxs("section", {
25895
+ id: "uhd-hierarchy",
25896
+ "aria-label": "Hierarchy",
25897
+ className: L.panel,
25898
+ children: [
25899
+ /* @__PURE__ */ jsxs("header", {
25900
+ className: L.header,
25901
+ children: [
25902
+ /* @__PURE__ */ jsxs("span", {
25903
+ className: L.title,
25904
+ children: ["Hierarchy", /* @__PURE__ */ jsxs("span", {
25905
+ className: "text-fg-faint",
25906
+ children: [" · ", count]
25907
+ })]
25908
+ }),
25909
+ /* @__PURE__ */ jsx(IconButton, {
25910
+ icon: "list-filter",
25911
+ size: "sm",
25912
+ label: "Filter modules",
25913
+ pressed: Boolean(filter),
25914
+ onClick: () => filterRef.current?.focus()
25915
+ }),
25916
+ /* @__PURE__ */ jsx(IconButton, {
25917
+ icon: "chevrons-down-up",
25918
+ size: "sm",
25919
+ label: "Collapse all",
25920
+ onClick: () => setExpanded(/* @__PURE__ */ new Set([""]))
25921
+ }),
25922
+ /* @__PURE__ */ jsx(IconButton, {
25923
+ ref: foldRef,
25924
+ icon: "panel-left-close",
25925
+ size: "sm",
25926
+ label: "Hide hierarchy",
25927
+ "aria-expanded": true,
25928
+ "aria-controls": "uhd-hierarchy",
25929
+ onClick: () => setKept({ open: false })
25930
+ })
25931
+ ]
25932
+ }),
25933
+ /* @__PURE__ */ jsxs("label", {
25934
+ className: L.filterBox,
25935
+ children: [
25936
+ /* @__PURE__ */ jsx(Icon, {
25937
+ name: "search",
25439
25938
  size: 12,
25939
+ strokeWidth: 1.75,
25940
+ className: "shrink-0 text-fg-faint",
25440
25941
  "aria-hidden": true
25942
+ }),
25943
+ /* @__PURE__ */ jsx("input", {
25944
+ ref: filterRef,
25945
+ type: "text",
25946
+ value: filter,
25947
+ onChange: (e) => setFilter(e.target.value),
25948
+ onKeyDown: onFilterKey,
25949
+ placeholder: "Filter modules",
25950
+ "aria-label": "Filter the hierarchy",
25951
+ "aria-controls": "uhd-hierarchy-tree",
25952
+ spellCheck: false,
25953
+ className: "min-w-0 flex-1 border-0 bg-transparent p-0 font-ui text-[12px] text-fg outline-none placeholder:text-fg-faint"
25954
+ }),
25955
+ filter && /* @__PURE__ */ jsx("button", {
25956
+ type: "button",
25957
+ "aria-label": "Clear the filter",
25958
+ "data-tooltip": "Clear the filter",
25959
+ onClick: () => (setFilter(""), filterRef.current?.focus()),
25960
+ className: "grid size-5 cursor-pointer place-items-center rounded border-0 bg-transparent p-0 text-fg-muted hover:bg-hover hover:text-fg",
25961
+ children: /* @__PURE__ */ jsx(Icon, {
25962
+ name: "x",
25963
+ size: 12,
25964
+ "aria-hidden": true
25965
+ })
25441
25966
  })
25967
+ ]
25968
+ }),
25969
+ /* @__PURE__ */ jsx(ScrollArea, {
25970
+ className: "min-h-0 flex-1",
25971
+ axis: "vertical",
25972
+ children: /* @__PURE__ */ jsxs("div", {
25973
+ ref: treeRef,
25974
+ id: "uhd-hierarchy-tree",
25975
+ role: "tree",
25976
+ "aria-label": `Hierarchy of ${rootLabel}`,
25977
+ onKeyDown: onTreeKey,
25978
+ className: "pb-1",
25979
+ children: [rows.map((r) => /* @__PURE__ */ jsx(Row, {
25980
+ row: r,
25981
+ focus: focus === r.node.path,
25982
+ selected: selected === r.node.path || together.has(r.node.path),
25983
+ current: level === r.node.path,
25984
+ match: filtered.matches.has(r.node.path),
25985
+ dim: filtered.matches.size > 0 && !filtered.matches.has(r.node.path) && !r.node.group,
25986
+ attributes: r.node.group ? void 0 : rowAttributes?.(r.node),
25987
+ onSelect: select,
25988
+ onDrill: drill,
25989
+ onToggle: toggle,
25990
+ onContext: onRowContext
25991
+ }, r.node.path)), !rows.length && /* @__PURE__ */ jsxs("p", {
25992
+ role: "status",
25993
+ className: "m-0 px-3 py-2 text-[12px] text-fg-muted",
25994
+ children: [
25995
+ "Nothing matches “",
25996
+ filter,
25997
+ "”."
25998
+ ]
25999
+ })]
25442
26000
  })
25443
- ]
25444
- }),
25445
- /* @__PURE__ */ jsx(ScrollArea, {
25446
- className: "min-h-0 flex-1",
25447
- axis: "vertical",
25448
- children: /* @__PURE__ */ jsxs("div", {
25449
- ref: treeRef,
25450
- id: "uhd-hierarchy-tree",
25451
- role: "tree",
25452
- "aria-label": `Hierarchy of ${rootLabel}`,
25453
- onKeyDown: onTreeKey,
25454
- className: "pb-1",
25455
- children: [rows.map((r) => /* @__PURE__ */ jsx(Row, {
25456
- row: r,
25457
- focus: focus === r.node.path,
25458
- selected: selected === r.node.path || together.has(r.node.path),
25459
- current: level === r.node.path,
25460
- match: filtered.matches.has(r.node.path),
25461
- dim: filtered.matches.size > 0 && !filtered.matches.has(r.node.path) && !r.node.group,
25462
- attributes: r.node.group ? void 0 : rowAttributes?.(r.node),
25463
- onSelect: select,
25464
- onDrill: drill,
25465
- onToggle: toggle,
25466
- onContext: onRowContext
25467
- }, r.node.path)), !rows.length && /* @__PURE__ */ jsxs("p", {
25468
- role: "status",
25469
- className: "m-0 px-3 py-2 text-[12px] text-fg-muted",
25470
- children: [
25471
- "Nothing matches “",
25472
- filter,
25473
- "”."
25474
- ]
25475
- })]
26001
+ }),
26002
+ legend.length > 0 && /* @__PURE__ */ jsx("footer", {
26003
+ className: L.legend,
26004
+ "aria-label": "Part status",
26005
+ children: legend.map((l) => /* @__PURE__ */ jsxs("span", {
26006
+ className: "flex items-center gap-1.5",
26007
+ children: [/* @__PURE__ */ jsx("span", {
26008
+ "aria-hidden": true,
26009
+ className: "uhd-tree-mark",
26010
+ "data-tone": l.tone
26011
+ }), l.label]
26012
+ }, l.tone))
25476
26013
  })
25477
- }),
25478
- legend.length > 0 && /* @__PURE__ */ jsx("footer", {
25479
- className: L.legend,
25480
- "aria-label": "Part status",
25481
- children: legend.map((l) => /* @__PURE__ */ jsxs("span", {
25482
- className: "flex items-center gap-1.5",
25483
- children: [/* @__PURE__ */ jsx("span", {
25484
- "aria-hidden": true,
25485
- className: "uhd-tree-mark",
25486
- "data-tone": l.tone
25487
- }), l.label]
25488
- }, l.tone))
26014
+ ]
26015
+ }),
26016
+ /* @__PURE__ */ jsx("div", {
26017
+ role: "separator",
26018
+ "aria-orientation": "vertical",
26019
+ "aria-label": "Resize the hierarchy",
26020
+ "aria-valuemin": HIERARCHY_WIDTH.min,
26021
+ "aria-valuemax": HIERARCHY_WIDTH.max,
26022
+ "aria-valuenow": shownWidth,
26023
+ "aria-controls": "uhd-hierarchy",
26024
+ tabIndex: 0,
26025
+ onPointerDown: onResizeStart,
26026
+ onKeyDown: onResizeKey,
26027
+ onDoubleClick: () => setKept({ width: HIERARCHY_WIDTH.default }),
26028
+ className: "group absolute inset-y-2 -right-1 z-10 w-2 cursor-col-resize touch-none focus-visible:outline-none",
26029
+ children: /* @__PURE__ */ jsx("span", {
26030
+ "aria-hidden": true,
26031
+ className: "absolute inset-y-0 left-[3px] w-0.5 bg-transparent transition-colors group-hover:bg-focus group-focus-visible:bg-focus group-active:bg-focus"
25489
26032
  })
25490
- ]
25491
- }),
25492
- /* @__PURE__ */ jsx("div", {
25493
- role: "separator",
25494
- "aria-orientation": "vertical",
25495
- "aria-label": "Resize the hierarchy",
25496
- "aria-valuemin": HIERARCHY_WIDTH.min,
25497
- "aria-valuemax": HIERARCHY_WIDTH.max,
25498
- "aria-valuenow": shownWidth,
25499
- "aria-controls": "uhd-hierarchy",
25500
- tabIndex: 0,
25501
- onPointerDown: onResizeStart,
25502
- onKeyDown: onResizeKey,
25503
- onDoubleClick: () => setKept({ width: HIERARCHY_WIDTH.default }),
25504
- className: "group absolute top-[var(--uhd-inset-top,0px)] -right-1 bottom-0 z-10 w-2 cursor-col-resize touch-none focus-visible:outline-none",
25505
- children: /* @__PURE__ */ jsx("span", {
25506
- "aria-hidden": true,
25507
- className: "absolute inset-y-0 left-[3px] w-0.5 bg-transparent transition-colors group-hover:bg-focus group-focus-visible:bg-focus group-active:bg-focus"
26033
+ }),
26034
+ menuNode && ownMenu && /* @__PURE__ */ jsx(NodeMenu, {
26035
+ x: menu.x,
26036
+ y: menu.y,
26037
+ items: menuItems,
26038
+ onClose: closeMenu
25508
26039
  })
25509
- }),
25510
- menuNode && ownMenu && /* @__PURE__ */ jsx(NodeMenu, {
25511
- x: menu.x,
25512
- y: menu.y,
25513
- items: menuItems,
25514
- onClose: closeMenu
25515
- })
25516
- ]
26040
+ ]
26041
+ })]
25517
26042
  });
25518
26043
  }
26044
+ /**
26045
+ * The way in and out of the card (and of the button that opens it): mounted
26046
+ * while open and while it leaves, and shown (in place) one frame after it
26047
+ * mounts, so the transition runs both ways. With reduced motion it simply
26048
+ * appears and goes.
26049
+ */
26050
+ function useFold(open) {
26051
+ const [mounted, setMounted] = useState(open);
26052
+ const [shown, setShown] = useState(open);
26053
+ useEffect(() => {
26054
+ const still = typeof window === "undefined" || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
26055
+ if (open) {
26056
+ setMounted(true);
26057
+ if (still) return setShown(true);
26058
+ let inner = 0;
26059
+ const outer = requestAnimationFrame(() => inner = requestAnimationFrame(() => setShown(true)));
26060
+ return () => {
26061
+ cancelAnimationFrame(outer);
26062
+ cancelAnimationFrame(inner);
26063
+ };
26064
+ }
26065
+ setShown(false);
26066
+ if (still) return setMounted(false);
26067
+ const t = setTimeout(() => setMounted(false), 200);
26068
+ return () => clearTimeout(t);
26069
+ }, [open]);
26070
+ return {
26071
+ mounted: mounted || open,
26072
+ shown: shown && open
26073
+ };
26074
+ }
25519
26075
  var Row = memo(function Row({ row, focus, selected, current, match, dim, attributes, onSelect, onDrill, onToggle, onContext }) {
25520
26076
  const n = row.node;
25521
26077
  const L = HIERARCHY_LOOK;
@@ -25610,7 +26166,13 @@ var Row = memo(function Row({ row, focus, selected, current, match, dim, attribu
25610
26166
  ]
25611
26167
  });
25612
26168
  });
25613
- /** The toolbar's Hierarchy button (left of a shell's view tabs): it opens and folds the panel. */
26169
+ /**
26170
+ * A toolbar button that opens and folds the panel from a host's header.
26171
+ *
26172
+ * @deprecated The panel has its own fold button and, folded, its own button to
26173
+ * open it again (PB-1070); a host needs no toggle of its own. Kept for hosts
26174
+ * written against 0.2.0; it will be removed in a later minor release.
26175
+ */
25614
26176
  function HierarchyToggle({ open, onClick, disabled, className }) {
25615
26177
  return /* @__PURE__ */ jsxs("button", {
25616
26178
  type: "button",
@@ -25776,7 +26338,7 @@ function SelectionDetails({ bundle, definitions, target, resolveArtifact = NO_FI
25776
26338
  bundle
25777
26339
  ]);
25778
26340
  const named = useMemo(() => found ? levelNames(found.vm, found.levelDef, lookup) : void 0, [found, lookup]);
25779
- if (!found || !found.node && !found.link && !found.derived) return /* @__PURE__ */ jsx(Fragment, { children: missing });
26341
+ if (!found || !found.node && !found.link && !found.derived) return /* @__PURE__ */ jsx(Fragment$1, { children: missing });
25780
26342
  const { vm, drill, node, link, derived } = found;
25781
26343
  const defFor = (id) => id ? lookup(id) : void 0;
25782
26344
  const select = (s) => s && onSelect?.({
@@ -25851,11 +26413,10 @@ function SelectionDetails({ bundle, definitions, target, resolveArtifact = NO_FI
25851
26413
  onToggleExpand: () => void 0,
25852
26414
  onSelect: select,
25853
26415
  board: boardFor(node),
25854
- artifacts: /* @__PURE__ */ jsx(AssemblyArtifacts, {
26416
+ artifacts: /* @__PURE__ */ jsx(ArtifactsSection, {
25855
26417
  def: defFor(node.defId),
25856
- lookup: defFor,
25857
26418
  resolve: resolveArtifact,
25858
- appearance,
26419
+ appearance: node.defId ? appearance?.[node.defId] : void 0,
25859
26420
  titled: false
25860
26421
  }, node.defId),
25861
26422
  lookup: defFor,
@@ -26043,6 +26604,6 @@ function resolveTheme(option) {
26043
26604
  return staticTheme(completeTheme(option));
26044
26605
  }
26045
26606
  //#endregion
26046
- export { ARTIFACT_GROUPS, ArtifactCard, ArtifactCards, ArtifactGrid, Badge, CARD_LOOK, CARD_W, CARD_W_MAX, CLASSIFICATIONS, CLASSIFICATION_OPTIONS, DARK_DATA_TOKENS, DERIVED, DETAIL_OPTIONS, DRC_TOKENS, DetailsSection, DiagramCanvas, DiagramLegend, DiagramLegendBody, DiagramLegendItems, DiagramToolbar, EmptyState, EvidenceSection, FittedLegend, KIND_LABEL as HIERARCHY_KIND_LABEL, HIERARCHY_LOOK, HIERARCHY_WIDTH, PREFIX as HOST_MESSAGE_PREFIX, HierarchyToggle, HostContext, Icon, IconButton, InspectorPanel, InterfaceCard, InterfaceCards, IsolationClear, KIND_ICON, KeyValues, LIGHT_DATA_TOKENS, Legend, LegendItem, LegendSwatches, LegendToolbarButton, MissingNotice, ModelView, PropertyRow, RAIL_WIDTH, SECTION_LOOK, SLOT_COLOR, ScenarioBanner, Scene3D, ScrollArea, Section, Sections, SelectionDetails, SelectionKey, SlotGlyph, StatusDot, TOKEN_NAMES, TOOL_OPTIONS, ThemeProvider, ToggleGroup, ToolToggle, Toolbar, ToolbarSeparator, Tooltips, UhdViewer, ViewButtons, ViewerBoundary, ViewerHierarchy, ViewerInspector, ViewerNodeMenu, ViewerProvider, WIRING_PATH, ancestorsOf, applySelectChanges, applyTheme, artifactCard, artifactCards, buildHierarchy, canvasCommand, cardWidth, checkFindings, classKey, classifyArtifact, colorFor, completeTheme, cx, defaultDark, defaultLight, defaultTheme, defaultThemeAdapter, defineTheme, drillInto, evidenceSourceCards, filterHierarchy, firstView, groupArtifactCards, groupSlots, groupWiring, hierarchyIndex, humanize, initialInteraction, interactionProps, interactionReducer, interfaceSlots, isInternalArtifact, isPlaceholder, isViewerEvent, isolate, keptInspectorState, kindOf, labelIsCut, legendFor, legendRoom, legendStage, levelNames, linkKeys, markLegend, mergeFormats, mix, navTarget, onNavKey, openAction, originAllowed, panelState, paramValue, parseHostMessage, pickTheme, presentKeys, readToken, renderViewer, resolveTheme, revealPaths, rowTarget, selectedPath, sentence, slotSummary, staticTheme, themeCss, themeDeclarations, toggleIsolated, token, tokenColor, tokenVar, tolerantSystem, treeKey, useExpandedCards, useSectionOpen, useShowAll, useTheme, useViewer, useViewerController, useViewerSource, visibleRows, vscodeScheme, vscodeTheme, vscodeThemeAdapter, walkHierarchy };
26607
+ export { ARTIFACT_GROUPS, ArtifactCard, ArtifactCards, ArtifactGrid, Badge, CARD_LOOK, CARD_W, CARD_W_MAX, CLASSIFICATIONS, CLASSIFICATION_OPTIONS, ClampedText, DARK_DATA_TOKENS, DERIVED, DETAIL_OPTIONS, DRC_TOKENS, DetailsSection, DiagramCanvas, DiagramLegend, DiagramLegendBody, DiagramLegendItems, DiagramToolbar, EmptyState, EvidenceSection, FittedLegend, KIND_LABEL as HIERARCHY_KIND_LABEL, HIERARCHY_LOOK, HIERARCHY_WIDTH, PREFIX as HOST_MESSAGE_PREFIX, HierarchyToggle, HostContext, Icon, IconButton, InspectorPanel, InterfaceCard, InterfaceCards, IsolationClear, KIND_ICON, KeyValues, LEGEND_SHORT, LIGHT_DATA_TOKENS, LIST_STEP, Legend, LegendFold, LegendItem, LegendSwatches, LegendTab, LegendToolbarButton, MissingNotice, ModelView, PropertyRow, RAIL_WIDTH, SECTION_LOOK, SLOT_COLOR, ScenarioBanner, Scene3D, ScrollArea, Section, Sections, SelectionDetails, SelectionKey, SlotGlyph, StatusDot, TOKEN_NAMES, TOOL_OPTIONS, ThemeProvider, ToggleGroup, ToolToggle, Toolbar, ToolbarSeparator, Tooltips, UhdViewer, ViewButtons, ViewerBoundary, ViewerHierarchy, ViewerInspector, ViewerNodeMenu, ViewerProvider, WIRING_PATH, ancestorsOf, applySelectChanges, applyTheme, artifactCard, artifactCards, buildHierarchy, canvasCommand, cardWidth, checkFindings, classKey, classifyArtifact, colorFor, completeTheme, cx, defaultDark, defaultLight, defaultTheme, defaultThemeAdapter, defineTheme, drillInto, evidenceSourceCards, filterHierarchy, firstView, groupArtifactCards, groupSlots, groupWiring, hierarchyIndex, humanize, initialInteraction, interactionProps, interactionReducer, interfaceSlots, isInternalArtifact, isPlaceholder, isViewerEvent, isolate, keptInspectorState, kindOf, labelIsCut, legendFolded, legendFor, legendRoom, legendStage, levelNames, linkKeys, markLegend, mergeFormats, mix, navTarget, onNavKey, openAction, originAllowed, panelState, paramValue, parseHostMessage, pickTheme, presentKeys, readToken, renderViewer, resolveTheme, revealPaths, rowTarget, selectedPath, sentence, slotSummary, staticTheme, themeCss, themeDeclarations, toggleIsolated, token, tokenColor, tokenVar, tolerantSystem, treeKey, useExpandedCards, useLoadMore, useSectionOpen, useShowAll, useTheme, useViewer, useViewerController, useViewerSource, visibleRows, vscodeScheme, vscodeTheme, vscodeThemeAdapter, walkHierarchy };
26047
26608
 
26048
26609
  //# sourceMappingURL=index.js.map