@camstack/ui-library 1.2.244 → 1.2.246

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 (27) hide show
  1. package/dist/composites/accelerator-label.d.ts +17 -0
  2. package/dist/composites/agent-step-editor.d.ts +41 -16
  3. package/dist/composites/device-step-matrix.d.ts +26 -26
  4. package/dist/composites/index.d.ts +10 -12
  5. package/dist/composites/model-bound-slider.d.ts +8 -1
  6. package/dist/composites/pipeline-builder.d.ts +2 -37
  7. package/dist/composites/pipeline-matrix-shared.d.ts +11 -1
  8. package/dist/composites/pipeline-step.d.ts +4 -44
  9. package/dist/composites/step-inspector/accelerator-tabs.d.ts +12 -0
  10. package/dist/composites/step-inspector/gate-control.d.ts +20 -0
  11. package/dist/composites/step-inspector/index.d.ts +10 -0
  12. package/dist/composites/step-inspector/layered-settings-form.d.ts +38 -0
  13. package/dist/composites/step-inspector/segmented-choice.d.ts +11 -0
  14. package/dist/composites/step-inspector/setting-values.d.ts +19 -0
  15. package/dist/composites/step-inspector/step-detail-dock.d.ts +33 -0
  16. package/dist/composites/step-inspector/step-inspector-panel.d.ts +26 -0
  17. package/dist/composites/step-inspector/step-model-list.d.ts +9 -0
  18. package/dist/composites/step-inspector/step-model-row.d.ts +27 -0
  19. package/dist/composites/step-inspector/step-search.d.ts +18 -0
  20. package/dist/composites/{step-tree-master.d.ts → step-tree.d.ts} +0 -12
  21. package/dist/generated/system-hooks.d.ts +0 -16
  22. package/dist/index.cjs +1431 -1286
  23. package/dist/index.js +1407 -1278
  24. package/package.json +1 -1
  25. package/dist/composites/pipeline-tree-matrix.d.ts +0 -18
  26. package/dist/composites/slide-over-panel.d.ts +0 -35
  27. package/dist/lib/validate-template.d.ts +0 -2
package/dist/index.cjs CHANGED
@@ -1707,41 +1707,6 @@ var CookingPot = createLucideIcon("cooking-pot", [
1707
1707
  * This source code is licensed under the ISC license.
1708
1708
  * See the LICENSE file in the root directory of this source tree.
1709
1709
  */
1710
- var CopyPlus = createLucideIcon("copy-plus", [
1711
- ["line", {
1712
- x1: "15",
1713
- x2: "15",
1714
- y1: "12",
1715
- y2: "18",
1716
- key: "1p7wdc"
1717
- }],
1718
- ["line", {
1719
- x1: "12",
1720
- x2: "18",
1721
- y1: "15",
1722
- y2: "15",
1723
- key: "1nscbv"
1724
- }],
1725
- ["rect", {
1726
- width: "14",
1727
- height: "14",
1728
- x: "8",
1729
- y: "8",
1730
- rx: "2",
1731
- ry: "2",
1732
- key: "17jyea"
1733
- }],
1734
- ["path", {
1735
- d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
1736
- key: "zix9uf"
1737
- }]
1738
- ]);
1739
- /**
1740
- * @license lucide-react v0.576.0 - ISC
1741
- *
1742
- * This source code is licensed under the ISC license.
1743
- * See the LICENSE file in the root directory of this source tree.
1744
- */
1745
1710
  var Copy = createLucideIcon("copy", [["rect", {
1746
1711
  width: "14",
1747
1712
  height: "14",
@@ -7914,14 +7879,6 @@ var usePipelineExecutorGetGlobalPipelineConfig = trpc.pipelineExecutor.getGlobal
7914
7879
  var usePipelineExecutorGetOrchestratorConfigSchema = trpc.pipelineExecutor.getOrchestratorConfigSchema.useQuery;
7915
7880
  /** Generated alias around `trpc.pipelineExecutor.validatePipeline.useQuery`. */
7916
7881
  var usePipelineExecutorValidatePipeline = trpc.pipelineExecutor.validatePipeline.useQuery;
7917
- /** Generated alias around `trpc.pipelineExecutor.listTemplates.useQuery`. */
7918
- var usePipelineExecutorListTemplates = trpc.pipelineExecutor.listTemplates.useQuery;
7919
- /** Generated alias around `trpc.pipelineExecutor.saveTemplate.useMutation`. */
7920
- var usePipelineExecutorSaveTemplate = trpc.pipelineExecutor.saveTemplate.useMutation;
7921
- /** Generated alias around `trpc.pipelineExecutor.updateTemplate.useMutation`. */
7922
- var usePipelineExecutorUpdateTemplate = trpc.pipelineExecutor.updateTemplate.useMutation;
7923
- /** Generated alias around `trpc.pipelineExecutor.deleteTemplate.useMutation`. */
7924
- var usePipelineExecutorDeleteTemplate = trpc.pipelineExecutor.deleteTemplate.useMutation;
7925
7882
  /** Generated alias around `trpc.pipelineExecutor.getCapabilities.useQuery`. */
7926
7883
  var usePipelineExecutorGetCapabilities = trpc.pipelineExecutor.getCapabilities.useQuery;
7927
7884
  /** Generated alias around `trpc.pipelineExecutor.getAddonModels.useQuery`. */
@@ -8068,14 +8025,6 @@ var usePipelineOrchestratorSetCameraSensors = trpc.pipelineOrchestrator.setCamer
8068
8025
  var usePipelineOrchestratorReconcileFromDisk = trpc.pipelineOrchestrator.reconcileFromDisk.useMutation;
8069
8026
  /** Generated alias around `trpc.pipelineOrchestrator.getReconcileFromDiskStatus.useQuery`. */
8070
8027
  var usePipelineOrchestratorGetReconcileFromDiskStatus = trpc.pipelineOrchestrator.getReconcileFromDiskStatus.useQuery;
8071
- /** Generated alias around `trpc.pipelineOrchestrator.listTemplates.useQuery`. */
8072
- var usePipelineOrchestratorListTemplates = trpc.pipelineOrchestrator.listTemplates.useQuery;
8073
- /** Generated alias around `trpc.pipelineOrchestrator.saveTemplate.useMutation`. */
8074
- var usePipelineOrchestratorSaveTemplate = trpc.pipelineOrchestrator.saveTemplate.useMutation;
8075
- /** Generated alias around `trpc.pipelineOrchestrator.updateTemplate.useMutation`. */
8076
- var usePipelineOrchestratorUpdateTemplate = trpc.pipelineOrchestrator.updateTemplate.useMutation;
8077
- /** Generated alias around `trpc.pipelineOrchestrator.deleteTemplate.useMutation`. */
8078
- var usePipelineOrchestratorDeleteTemplate = trpc.pipelineOrchestrator.deleteTemplate.useMutation;
8079
8028
  /** Generated alias around `trpc.pipelineOrchestrator.getDeviceSettingsContribution.useQuery`. */
8080
8029
  var usePipelineOrchestratorGetDeviceSettingsContribution = trpc.pipelineOrchestrator.getDeviceSettingsContribution.useQuery;
8081
8030
  /** Generated alias around `trpc.pipelineOrchestrator.getDeviceLiveContribution.useQuery`. */
@@ -28746,30 +28695,43 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
28746
28695
  });
28747
28696
  }
28748
28697
  //#endregion
28749
- //#region src/primitives/collapsible-card.tsx
28750
- function CollapsibleCard({ expanded, onExpandedChange, header, children, dimmed = false, hideChevron = false, className }) {
28751
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28752
- className: cn("rounded-lg border border-border bg-surface overflow-hidden", dimmed && "opacity-[0.45]", className),
28753
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28754
- role: "button",
28755
- tabIndex: 0,
28756
- className: "flex items-center gap-2.5 px-3 py-2.5 cursor-pointer select-none",
28757
- onClick: () => onExpandedChange(!expanded),
28758
- onKeyDown: (e) => {
28759
- if (e.key === "Enter" || e.key === " ") {
28760
- e.preventDefault();
28761
- onExpandedChange(!expanded);
28762
- }
28763
- },
28764
- children: [!hideChevron && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28765
- className: "text-foreground-subtle shrink-0",
28766
- children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-4 w-4" })
28767
- }), header]
28768
- }), expanded && children && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28769
- className: "border-t border-border px-3 py-3",
28770
- children
28771
- })]
28772
- });
28698
+ //#region src/composites/accelerator-label.ts
28699
+ function names(device) {
28700
+ const backend = device.backend.toLowerCase();
28701
+ const dev = device.device.toLowerCase();
28702
+ if (device.key === "cpu" || dev === "cpu" || backend === "cpu") return {
28703
+ label: "CPU",
28704
+ short: "CPU"
28705
+ };
28706
+ if (dev === "npu") return {
28707
+ label: "Intel NPU",
28708
+ short: "NPU"
28709
+ };
28710
+ if (dev === "gpu" || dev === "igpu") return {
28711
+ label: "Intel iGPU",
28712
+ short: "iGPU"
28713
+ };
28714
+ if (backend === "edgetpu") return {
28715
+ label: "Coral Edge TPU",
28716
+ short: "Coral"
28717
+ };
28718
+ if (backend === "coreml") return {
28719
+ label: "Apple Neural Engine",
28720
+ short: "ANE"
28721
+ };
28722
+ const pretty = `${device.backend} ${device.device}`.trim().replace(/\b\w/g, (c) => c.toUpperCase());
28723
+ return {
28724
+ label: pretty.length > 0 ? pretty : device.key,
28725
+ short: device.backend || device.key
28726
+ };
28727
+ }
28728
+ /** "Intel NPU", "Apple Neural Engine", "Coral Edge TPU", "CPU" — Auto tune's names too. */
28729
+ function acceleratorLabel(device) {
28730
+ return names(device).label;
28731
+ }
28732
+ /** "NPU", "ANE", "Coral", "CPU" — chips and tabs. */
28733
+ function acceleratorShortLabel(device) {
28734
+ return names(device).short;
28773
28735
  }
28774
28736
  //#endregion
28775
28737
  //#region src/composites/grouped-model-selector.tsx
@@ -29052,6 +29014,257 @@ function ModelPicker({ models, value, onChange, disabled, defaultModelId }) {
29052
29014
  });
29053
29015
  }
29054
29016
  //#endregion
29017
+ //#region src/composites/resolve-step-default-model.ts
29018
+ function resolveStepDefaultModel(catalog, format) {
29019
+ const hasFormatBuild = (modelId) => catalog.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
29020
+ const declared = catalog.defaultModelIdByFormat?.[format];
29021
+ if (declared !== void 0 && hasFormatBuild(declared)) return declared;
29022
+ if (hasFormatBuild(catalog.defaultModelId)) return catalog.defaultModelId;
29023
+ const available = catalog.models.filter((m) => m.formats[format] !== void 0 && m.legacy !== true);
29024
+ if (available.length === 0) return catalog.defaultModelId;
29025
+ return [...available].toSorted((a, b) => (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity))[0].id;
29026
+ }
29027
+ /**
29028
+ * The model a matrix CELL shows as this step's effective default on `device`.
29029
+ *
29030
+ * The device's ELECTED model (`device.defaultModelId`, the per-device
29031
+ * object-detection election) applies ONLY when this step's catalog actually
29032
+ * contains it — `slot === 'detector'` alone is too wide, because
29033
+ * package-detection shares the detector slot with the root step. Bit live
29034
+ * 2026-08-08: rfdetr-package became the package step's default and every
29035
+ * matrix cell still showed the device's yolo as the "selected" model, because
29036
+ * the election was applied slot-wide. Membership is the principled gate: an
29037
+ * elected model this step cannot run is never displayed for it.
29038
+ */
29039
+ function resolveEffectiveDefaultModel(catalog, device) {
29040
+ const format = device?.format;
29041
+ if (format === void 0) return void 0;
29042
+ const elected = device?.defaultModelId;
29043
+ return catalog.slot === "detector" && elected !== void 0 && catalog.models.some((m) => m.id === elected) ? elected : resolveStepDefaultModel(catalog, format);
29044
+ }
29045
+ /**
29046
+ * The models a step may run on an engine `format` — the ONE derivation of the
29047
+ * option list. An unknown format yields NOTHING rather than everything: a
29048
+ * picker that offered a model with no build for the device would write a pin
29049
+ * the runner cannot honour.
29050
+ */
29051
+ function stepModelOptions(models, format) {
29052
+ if (format === void 0) return [];
29053
+ return models.filter((m) => m.formats[format] !== void 0);
29054
+ }
29055
+ /** Can this step run on `format` at all? (`false` ⇒ the caller renders "⚠ skip"/"—".) */
29056
+ function stepHasModelForFormat(models, format) {
29057
+ return stepModelOptions(models, format).length > 0;
29058
+ }
29059
+ /**
29060
+ * The model a step ACTUALLY runs for a (device, camera) — the whole effective
29061
+ * chain in one place: per-camera override → per-(node,device) base → the
29062
+ * resolved default (`resolveEffectiveDefaultModel`).
29063
+ *
29064
+ * Every surface calls this: the matrix cell, and the drawer's picker seed. They
29065
+ * are the same number, and the only way to keep them the same number is to have
29066
+ * one function produce it. A surface that owns fewer layers omits them (the node
29067
+ * matrix passes no `override`) — it does NOT get its own resolver.
29068
+ */
29069
+ function resolveEffectiveStepModel(input) {
29070
+ return input.override ?? input.base ?? resolveEffectiveDefaultModel(input.catalog, input.device);
29071
+ }
29072
+ //#endregion
29073
+ //#region src/composites/root-model-source.tsx
29074
+ /**
29075
+ * "Use the root model" — the step editor's control for a step the camera's
29076
+ * ROOT detection model can supply (D730).
29077
+ *
29078
+ * The flag (`useRootModel`, in the step's own settings) is an operator INTENT
29079
+ * kept beside the step's model: checking it never changes the model, and a
29080
+ * root that cannot supply the step leaves the step running its own model while
29081
+ * the flag waits, inactive, for a root that can. Whether it is in force is
29082
+ * `resolveRootSupply` — the same function the runner decides with.
29083
+ */
29084
+ /** The editor's view of the flag for one step, or `null` for a step no root can supply. */
29085
+ function rootSourceState(stepId, settings, rootModel) {
29086
+ if (!(0, _camstack_types.isRootSuppliableStepId)(stepId)) return null;
29087
+ return (0, _camstack_types.resolveRootSupply)(stepId, settings, rootModel?.supplies);
29088
+ }
29089
+ /** The settings bag with the flag set — every other key untouched. */
29090
+ function withUseRootModel(settings, value) {
29091
+ return {
29092
+ ...settings,
29093
+ [_camstack_types.USE_ROOT_MODEL_SETTING_KEY]: value
29094
+ };
29095
+ }
29096
+ /** What the operator must know about a supply, beyond "it is on". */
29097
+ function supplyNotes(supply) {
29098
+ if (supply.kind === "label") {
29099
+ const notes = supply.uncovered.length > 0 ? [`Not produced by this model: ${supply.uncovered.join(", ")}.`] : [];
29100
+ if (supply.stepId === "package-detection") return [...notes, "The root model's own package boxes become the package result at this step's threshold; this step's zone-crop view is not used, so small parcels the full-frame model misses stay missed."];
29101
+ return [...notes, "This step's class list decides which of the root's labels become the fine class; a switched-off class stays at its macro."];
29102
+ }
29103
+ if (supply.kind === "mask") return ["Masks come from the root model: each detection carries the root's own instance mask, so this step's model does not run. A root mask follows the model's mask grid (a quarter of its input), coarser than this step's own per-crop mask."];
29104
+ if (supply.stepId === "face-detection") return ["Face boxes come from the root model. Each is re-found by this step's face detector in a square around it before recognition, so only a face it finds is recognised; a box with no face in it is dropped."];
29105
+ return ["Plate boxes come from the root model and the plate reader reads them. The crop margin around a root plate box has not been measured for this model yet."];
29106
+ }
29107
+ /** The reason an intended supply is not in force, in the operator's words. */
29108
+ function inactiveText(state, rootModel) {
29109
+ switch (state.inactiveReason) {
29110
+ case "root-input-too-small": return `the current root model (${rootModel.name}) sees the frame too small to find faces reliably (it needs an input of 640 px or more).`;
29111
+ case "root-does-not-declare":
29112
+ case void 0: return `the current root model (${rootModel.name}) cannot supply this.`;
29113
+ }
29114
+ }
29115
+ function RootModelSourceControl({ stepId, settings, rootModel, disabled, onChange }) {
29116
+ if (rootModel === void 0) return null;
29117
+ const state = rootSourceState(stepId, settings, rootModel);
29118
+ if (state === null) return null;
29119
+ const inactiveNote = state.inactive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29120
+ className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29121
+ "data-testid": "root-model-inactive",
29122
+ children: [
29123
+ "Use the root model: inactive — ",
29124
+ inactiveText(state, rootModel),
29125
+ " The step runs its own model."
29126
+ ]
29127
+ }) : null;
29128
+ if (state.supply === void 0) return inactiveNote;
29129
+ const checked = state.intent;
29130
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29131
+ className: "space-y-1",
29132
+ "data-testid": "root-model-source",
29133
+ children: [
29134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29135
+ className: "flex items-center gap-2 text-xs text-foreground",
29136
+ children: [
29137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29138
+ type: "checkbox",
29139
+ checked,
29140
+ disabled,
29141
+ onChange: (e) => onChange(e.target.checked)
29142
+ }),
29143
+ "Use the root model (",
29144
+ rootModel.name,
29145
+ ")"
29146
+ ]
29147
+ }),
29148
+ inactiveNote,
29149
+ state.effective && supplyNotes(state.supply).map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29150
+ className: "text-[10px] leading-snug text-foreground-subtle",
29151
+ children: note
29152
+ }, note))
29153
+ ]
29154
+ });
29155
+ }
29156
+ //#endregion
29157
+ //#region src/lib/responsive.ts
29158
+ /**
29159
+ * Responsive design tokens — three-tier system.
29160
+ *
29161
+ * The admin-ui targets three width tiers; every component should pick
29162
+ * the right token from this file instead of hand-coding breakpoints.
29163
+ * Adopting the tokens uniformly is what makes the app feel consistent
29164
+ * across operator devices (mobile / tablet / desktop) and what unlocks
29165
+ * targeted tuning (e.g. shrink every label by 1px in one place).
29166
+ *
29167
+ * Tier definitions (Tailwind breakpoints):
29168
+ *
29169
+ * • **L1 — Compact** (`< 640px`, default = mobile portrait)
29170
+ * Single column, tight padding, smallest text, icon-only buttons
29171
+ * where labels can be inferred. The operator can still see every
29172
+ * stat / action — no horizontal scroll, no clipped text. The user
29173
+ * directive: "molta informazione in poco spazio quando la width è
29174
+ * bassa." [a lot of information in little space when the width is low.]
29175
+ *
29176
+ * • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
29177
+ * 2-column grids, side panels become visible, button labels return.
29178
+ * Targets ~768px–1024px viewports — the most common operator sizes.
29179
+ *
29180
+ * • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
29181
+ * 4-column grids, generous padding, full-width header text,
29182
+ * side-by-side composite layouts. The operator has elbow room.
29183
+ *
29184
+ * Conventions:
29185
+ * - **Always start from L1** — the bare class is always the compact
29186
+ * variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
29187
+ * desktop-first breakpoints; no max-width queries.
29188
+ * - **Token strings are static** — kept as `as const` so callers can
29189
+ * compose them via `cn()` / template literals without running a
29190
+ * class generator at runtime.
29191
+ * - **Pair the token with a comment** — the comment in the file
29192
+ * names every breakpoint it crosses, so a reader can predict the
29193
+ * layout without going to Tailwind docs.
29194
+ */
29195
+ /** Container padding for a card-style section (header + body). */
29196
+ var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
29197
+ /** Header strip inside a card (between border-b lines). */
29198
+ var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
29199
+ /** Body padding for a card-style section. */
29200
+ var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
29201
+ /** Gap between cards in a vertical stack. */
29202
+ var STACK_GAP = "space-y-3 sm:space-y-4";
29203
+ /** Gap between cards in a grid (4 cells / row at lg). */
29204
+ var GRID_GAP = "gap-2 sm:gap-3";
29205
+ /** Row padding inside a divided list (e.g. InfoRow). */
29206
+ var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
29207
+ /** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
29208
+ var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
29209
+ /** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
29210
+ var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
29211
+ /** Side-by-side layout that stacks below md (canvas + side panel). */
29212
+ var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
29213
+ /** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
29214
+ var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
29215
+ /**
29216
+ * The tier boundary at which a settings label/value row stops stacking.
29217
+ *
29218
+ * Below it (L1 — compact) the row is ONE column: label on its own line, value
29219
+ * on the next at full width. Above it the row is label-left / value-right as
29220
+ * before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
29221
+ * value beside it; a phone has neither, and the failure mode is not "cramped"
29222
+ * but "the value is the part that gets cut" — the one thing the operator
29223
+ * opened the page to read.
29224
+ */
29225
+ var SETTING_ROW_STACK_BREAKPOINT = "sm";
29226
+ /** Outer row: single column at L1, label/value columns from `sm` up. */
29227
+ var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
29228
+ /** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
29229
+ var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
29230
+ /**
29231
+ * Value lane — the value text plus its trailing affordances (copy / reveal).
29232
+ * `w-full` at L1 so the value owns the whole row once the label is above it;
29233
+ * `sm:flex-1` makes it share the row again from `sm` up.
29234
+ */
29235
+ var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
29236
+ /**
29237
+ * The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
29238
+ * at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
29239
+ * desktop form, where the row is wide enough for truncation to be a choice
29240
+ * rather than data loss.
29241
+ */
29242
+ var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
29243
+ /** Section header label (uppercase tracking-wider). */
29244
+ var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
29245
+ /** Field label inside a row. */
29246
+ var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
29247
+ /** Sub-label / hint text. */
29248
+ var TEXT_HINT = "text-[10px] text-foreground-subtle";
29249
+ /** Primary value (a metric, badge, …). */
29250
+ var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
29251
+ /** Big metric value (StatCard). */
29252
+ var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
29253
+ /** Default compact button. */
29254
+ var BTN_COMPACT = "inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40";
29255
+ /** Primary compact button. */
29256
+ var BTN_COMPACT_PRIMARY = "inline-flex items-center justify-center gap-1 rounded border border-primary/40 bg-primary/15 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-primary hover:bg-primary/25 transition-colors disabled:opacity-40";
29257
+ /** Danger compact button. */
29258
+ var BTN_COMPACT_DANGER = "inline-flex items-center justify-center gap-1 rounded border border-danger/40 bg-danger/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-danger hover:bg-danger/20 transition-colors disabled:opacity-40";
29259
+ /** Warning compact button (e.g. cancel-drawing). */
29260
+ var BTN_COMPACT_WARNING = "inline-flex items-center justify-center gap-1 rounded border border-warning/40 bg-warning/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-warning hover:bg-warning/20 transition-colors disabled:opacity-40";
29261
+ /** Compact text/select/number input. */
29262
+ var INPUT_COMPACT = "w-full rounded border border-border bg-background px-1.5 py-0.5 sm:px-2 sm:py-1 text-[11px] sm:text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none";
29263
+ /** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
29264
+ var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
29265
+ var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
29266
+ var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
29267
+ //#endregion
29055
29268
  //#region src/composites/model-bound-slider.tsx
29056
29269
  var SOURCE_LABEL = {
29057
29270
  override: "Override for this model",
@@ -29061,7 +29274,7 @@ var SOURCE_LABEL = {
29061
29274
  function formatNumber(value, scale) {
29062
29275
  return (scale !== void 0 ? value / scale : value).toFixed(2).replace(/\.?0+$/, "");
29063
29276
  }
29064
- function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings }) {
29277
+ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings, hideSourceRow = false }) {
29065
29278
  const modelDefault = field.modelApplicability?.defaultByModel?.[modelId];
29066
29279
  const state = (0, _camstack_types.resolveModelBoundSetting)(settings, field.key, modelId.length > 0 ? modelId : void 0, modelDefault, field.default);
29067
29280
  const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
@@ -29101,7 +29314,7 @@ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings
29101
29314
  onChange: (e) => handleChange(Number(e.target.value)),
29102
29315
  className: "w-full accent-primary h-1.5"
29103
29316
  }),
29104
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29317
+ !hideSourceRow && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29105
29318
  className: "mt-1 flex items-center justify-between gap-2 text-[10px]",
29106
29319
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29107
29320
  "data-testid": "model-bound-source",
@@ -29152,6 +29365,32 @@ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset
29152
29365
  });
29153
29366
  }
29154
29367
  //#endregion
29368
+ //#region src/primitives/collapsible-card.tsx
29369
+ function CollapsibleCard({ expanded, onExpandedChange, header, children, dimmed = false, hideChevron = false, className }) {
29370
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29371
+ className: cn("rounded-lg border border-border bg-surface overflow-hidden", dimmed && "opacity-[0.45]", className),
29372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29373
+ role: "button",
29374
+ tabIndex: 0,
29375
+ className: "flex items-center gap-2.5 px-3 py-2.5 cursor-pointer select-none",
29376
+ onClick: () => onExpandedChange(!expanded),
29377
+ onKeyDown: (e) => {
29378
+ if (e.key === "Enter" || e.key === " ") {
29379
+ e.preventDefault();
29380
+ onExpandedChange(!expanded);
29381
+ }
29382
+ },
29383
+ children: [!hideChevron && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29384
+ className: "text-foreground-subtle shrink-0",
29385
+ children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-4 w-4" })
29386
+ }), header]
29387
+ }), expanded && children && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29388
+ className: "border-t border-border px-3 py-3",
29389
+ children
29390
+ })]
29391
+ });
29392
+ }
29393
+ //#endregion
29155
29394
  //#region src/composites/pipeline-step.tsx
29156
29395
  /**
29157
29396
  * PipelineStep — single step card in the pipeline builder.
@@ -29163,124 +29402,77 @@ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset
29163
29402
  function modelsForStep(schema) {
29164
29403
  return schema?.models ?? [];
29165
29404
  }
29166
- function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false, toggleMode = "simple", overrideState = null, onOverrideChange, inheritedEnabled, hideModelAndSettings = false, allowAutoModel = false }) {
29405
+ function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false }) {
29167
29406
  const [expanded, setExpanded] = (0, react$1.useState)(false);
29168
29407
  const models = modelsForStep(schema);
29169
- const isAutoSentinel = allowAutoModel && step.modelId === "";
29170
- if (models.length > 0 && !isAutoSentinel && !models.some((m) => m.id === step.modelId) && !readOnly) {
29408
+ if (models.length > 0 && !models.some((m) => m.id === step.modelId) && !readOnly) {
29171
29409
  const fallback = (schema?.defaultModelId ? models.find((m) => m.id === schema.defaultModelId) : null) ?? models[0];
29172
29410
  if (fallback && fallback.id !== step.modelId) queueMicrotask(() => onChange({
29173
29411
  ...step,
29174
29412
  modelId: fallback.id
29175
29413
  }));
29176
29414
  }
29177
- const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
29178
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29179
- className: "flex-1 min-w-0",
29180
- children: [
29181
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29182
- className: "text-[10px] uppercase tracking-wider font-medium text-foreground-subtle/60 block leading-none",
29183
- children: step.slot
29184
- }),
29185
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29186
- className: "text-sm font-semibold text-foreground truncate block leading-tight",
29187
- children: step.addonName
29188
- }),
29189
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29190
- className: "flex items-center gap-1 mt-0.5 flex-wrap",
29191
- children: [
29192
- step.inputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29193
- className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/12 text-blue-400",
29194
- children: c
29195
- }, c)),
29196
- step.inputClasses.length > 0 && step.outputClasses.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29197
- className: "text-foreground-subtle/40 text-[10px]",
29198
- children: "→"
29199
- }),
29200
- step.outputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29201
- className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/12 text-green-400",
29202
- children: c
29203
- }, c))
29204
- ]
29205
- })
29206
- ]
29207
- }),
29208
- !readOnly && toggleMode === "simple" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29209
- onClick: (e) => {
29210
- e.stopPropagation();
29211
- onChange({
29212
- ...step,
29213
- enabled: !step.enabled
29214
- });
29215
- },
29216
- className: cn("relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors", step.enabled ? "bg-success" : "bg-foreground-subtle/30"),
29217
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-4 w-4 rounded-full bg-white shadow transition-transform", step.enabled ? "translate-x-6" : "translate-x-1") })
29218
- }),
29219
- !readOnly && toggleMode === "threeState" && onOverrideChange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29220
- className: "flex items-center gap-0.5 rounded-md border border-border p-0.5",
29221
- onClick: (e) => e.stopPropagation(),
29222
- children: [
29223
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29224
- label: "Inherit",
29225
- active: overrideState === null,
29226
- variant: "inherit",
29227
- subtext: inheritedEnabled === void 0 ? void 0 : inheritedEnabled ? "on" : "off",
29228
- onClick: () => onOverrideChange(null)
29229
- }),
29230
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29231
- label: "On",
29232
- active: overrideState === true,
29233
- variant: "on",
29234
- onClick: () => onOverrideChange(true)
29235
- }),
29236
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29237
- label: "Off",
29238
- active: overrideState === false,
29239
- variant: "off",
29240
- onClick: () => onOverrideChange(false)
29241
- })
29242
- ]
29243
- })
29244
- ] });
29415
+ const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29416
+ className: "flex-1 min-w-0",
29417
+ children: [
29418
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29419
+ className: "text-[10px] uppercase tracking-wider font-medium text-foreground-subtle/60 block leading-none",
29420
+ children: step.slot
29421
+ }),
29422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29423
+ className: "text-sm font-semibold text-foreground truncate block leading-tight",
29424
+ children: step.addonName
29425
+ }),
29426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29427
+ className: "flex items-center gap-1 mt-0.5 flex-wrap",
29428
+ children: [
29429
+ step.inputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29430
+ className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/12 text-blue-400",
29431
+ children: c
29432
+ }, c)),
29433
+ step.inputClasses.length > 0 && step.outputClasses.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29434
+ className: "text-foreground-subtle/40 text-[10px]",
29435
+ children: "→"
29436
+ }),
29437
+ step.outputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29438
+ className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/12 text-green-400",
29439
+ children: c
29440
+ }, c))
29441
+ ]
29442
+ })
29443
+ ]
29444
+ }), !readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29445
+ onClick: (e) => {
29446
+ e.stopPropagation();
29447
+ onChange({
29448
+ ...step,
29449
+ enabled: !step.enabled
29450
+ });
29451
+ },
29452
+ className: cn("relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors", step.enabled ? "bg-success" : "bg-foreground-subtle/30"),
29453
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-4 w-4 rounded-full bg-white shadow transition-transform", step.enabled ? "translate-x-6" : "translate-x-1") })
29454
+ })] });
29245
29455
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleCard, {
29246
29456
  expanded,
29247
29457
  onExpandedChange: setExpanded,
29248
29458
  dimmed: !step.enabled,
29249
29459
  header,
29250
- children: hideModelAndSettings ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29251
- className: "text-[11px] text-foreground-subtle",
29252
- children: "Model and detection settings are managed per agent — open the Pipeline page for this camera's agent to edit them."
29253
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29460
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29254
29461
  className: "space-y-3",
29255
29462
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29256
29463
  className: "space-y-2",
29257
- children: [
29258
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
29259
- className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
29260
- children: "Model"
29261
- }),
29262
- allowAutoModel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29263
- className: "flex items-center gap-2 text-[11px] text-foreground-subtle",
29264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29265
- type: "checkbox",
29266
- checked: isAutoSentinel,
29267
- disabled: readOnly,
29268
- onChange: (e) => onChange({
29269
- ...step,
29270
- modelId: e.target.checked ? "" : schema?.defaultModelId ?? models[0]?.id ?? ""
29271
- })
29272
- }), "Auto (node default)"]
29273
- }),
29274
- !isAutoSentinel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29275
- models,
29276
- value: step.modelId,
29277
- disabled: readOnly,
29278
- onChange: (id) => onChange({
29279
- ...step,
29280
- modelId: id
29281
- })
29464
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
29465
+ className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
29466
+ children: "Model"
29467
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29468
+ models,
29469
+ value: step.modelId,
29470
+ disabled: readOnly,
29471
+ onChange: (id) => onChange({
29472
+ ...step,
29473
+ modelId: id
29282
29474
  })
29283
- ]
29475
+ })]
29284
29476
  }), schema?.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29285
29477
  field,
29286
29478
  allFields: schema.configSchema ?? [],
@@ -29347,7 +29539,7 @@ function resolveSettingValue(key, step, allFields) {
29347
29539
  const sibling = allFields.find((f) => "key" in f && f.key === key);
29348
29540
  return sibling && "default" in sibling ? sibling.default : void 0;
29349
29541
  }
29350
- function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29542
+ function ConfigSchemaField({ field, allFields, step, disabled, onChange, hideModelBoundSourceRow }) {
29351
29543
  const applicability = "modelApplicability" in field ? field.modelApplicability : void 0;
29352
29544
  const inapplicableReason = applicability?.inapplicable?.[step.modelId];
29353
29545
  const control = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaFieldControl, {
@@ -29356,7 +29548,8 @@ function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29356
29548
  step,
29357
29549
  disabled: disabled || inapplicableReason !== void 0,
29358
29550
  modelScopedOptions: applicability?.optionsByModel?.[step.modelId],
29359
- onChange
29551
+ onChange,
29552
+ hideModelBoundSourceRow: hideModelBoundSourceRow === true
29360
29553
  });
29361
29554
  if (inapplicableReason === void 0) return control;
29362
29555
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -29367,7 +29560,7 @@ function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29367
29560
  })]
29368
29561
  });
29369
29562
  }
29370
- function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScopedOptions, onChange }) {
29563
+ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScopedOptions, onChange, hideModelBoundSourceRow }) {
29371
29564
  const showWhen = field.type !== "separator" && field.type !== "info" ? field.showWhen : void 0;
29372
29565
  if (showWhen) {
29373
29566
  const { field: condField, equals, notEquals, in: inList, notIn } = showWhen;
@@ -29417,7 +29610,8 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29417
29610
  onChangeSettings: (settings) => onChange({
29418
29611
  ...step,
29419
29612
  settings
29420
- })
29613
+ }),
29614
+ hideSourceRow: hideModelBoundSourceRow
29421
29615
  });
29422
29616
  if (field.type === "slider" || field.type === "number") {
29423
29617
  const raw = step.settings?.[field.key];
@@ -29503,204 +29697,247 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29503
29697
  });
29504
29698
  return null;
29505
29699
  }
29506
- function ThreeStateButton({ label, active, variant, subtext, onClick }) {
29507
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
29508
- type: "button",
29509
- onClick,
29510
- className: cn("px-2 py-1 text-[10px] font-medium rounded transition-colors", active ? variant === "on" ? "bg-green-500/20 text-green-400" : variant === "off" ? "bg-red-500/20 text-red-400" : "bg-primary/20 text-primary" : "text-foreground-subtle hover:bg-surface-hover"),
29511
- children: [label, subtext && active && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29512
- className: "ml-1 opacity-60",
29513
- children: [
29514
- "(",
29515
- subtext,
29516
- ")"
29517
- ]
29518
- })]
29519
- });
29520
- }
29521
29700
  //#endregion
29522
- //#region src/composites/resolve-step-default-model.ts
29523
- function resolveStepDefaultModel(catalog, format) {
29524
- const hasFormatBuild = (modelId) => catalog.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
29525
- const declared = catalog.defaultModelIdByFormat?.[format];
29526
- if (declared !== void 0 && hasFormatBuild(declared)) return declared;
29527
- if (hasFormatBuild(catalog.defaultModelId)) return catalog.defaultModelId;
29528
- const available = catalog.models.filter((m) => m.formats[format] !== void 0 && m.legacy !== true);
29529
- if (available.length === 0) return catalog.defaultModelId;
29530
- return [...available].toSorted((a, b) => (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity))[0].id;
29701
+ //#region src/composites/step-inspector/setting-values.ts
29702
+ function isValueField(field) {
29703
+ return field.type !== "separator" && field.type !== "info" && field.type !== "group";
29531
29704
  }
29532
- /**
29533
- * The model a matrix CELL shows as this step's effective default on `device`.
29534
- *
29535
- * The device's ELECTED model (`device.defaultModelId`, the per-device
29536
- * object-detection election) applies ONLY when this step's catalog actually
29537
- * contains it — `slot === 'detector'` alone is too wide, because
29538
- * package-detection shares the detector slot with the root step. Bit live
29539
- * 2026-08-08: rfdetr-package became the package step's default and every
29540
- * matrix cell still showed the device's yolo as the "selected" model, because
29541
- * the election was applied slot-wide. Membership is the principled gate: an
29542
- * elected model this step cannot run is never displayed for it.
29543
- */
29544
- function resolveEffectiveDefaultModel(catalog, device) {
29545
- const format = device?.format;
29546
- if (format === void 0) return void 0;
29547
- const elected = device?.defaultModelId;
29548
- return catalog.slot === "detector" && elected !== void 0 && catalog.models.some((m) => m.id === elected) ? elected : resolveStepDefaultModel(catalog, format);
29705
+ /** True for a D738 field whose value is stored with the model it was made for. */
29706
+ function bindsToModel(field) {
29707
+ return (field.type === "slider" || field.type === "number") && field.bindsToModel === true;
29549
29708
  }
29550
29709
  /**
29551
- * The models a step may run on an engine `format` — the ONE derivation of the
29552
- * option list. An unknown format yields NOTHING rather than everything: a
29553
- * picker that offered a model with no build for the device would write a pin
29554
- * the runner cannot honour.
29710
+ * What runs when no layer stores the field: the running model's default for a
29711
+ * model-bound field that the model declares one for (D738), else the field's
29712
+ * own default. `fromModel` says which.
29555
29713
  */
29556
- function stepModelOptions(models, format) {
29557
- if (format === void 0) return [];
29558
- return models.filter((m) => m.formats[format] !== void 0);
29714
+ function defaultOfField(field, modelId) {
29715
+ const byModel = "modelApplicability" in field ? field.modelApplicability?.defaultByModel?.[modelId] : void 0;
29716
+ if (byModel !== void 0) return {
29717
+ value: byModel,
29718
+ fromModel: true
29719
+ };
29720
+ return {
29721
+ value: "default" in field ? field.default : void 0,
29722
+ fromModel: false
29723
+ };
29559
29724
  }
29560
- /** Can this step run on `format` at all? (`false` ⇒ the caller renders "⚠ skip"/"—".) */
29561
- function stepHasModelForFormat(models, format) {
29562
- return stepModelOptions(models, format).length > 0;
29725
+ function trimNumber(n) {
29726
+ return n.toFixed(2).replace(/\.?0+$/, "");
29563
29727
  }
29564
- /**
29565
- * The model a step ACTUALLY runs for a (device, camera) — the whole effective
29566
- * chain in one place: per-camera override → per-(node,device) base → the
29567
- * resolved default (`resolveEffectiveDefaultModel`).
29568
- *
29569
- * Every surface calls this: the matrix cell, and the drawer's picker seed. They
29570
- * are the same number, and the only way to keep them the same number is to have
29571
- * one function produce it. A surface that owns fewer layers omits them (the node
29572
- * matrix passes no `override`) — it does NOT get its own resolver.
29573
- */
29574
- function resolveEffectiveStepModel(input) {
29575
- return input.override ?? input.base ?? resolveEffectiveDefaultModel(input.catalog, input.device);
29728
+ /** One stored value as the operator reads it (`0.65`, `on`, `Person, Vehicle`, `—`). */
29729
+ function formatSettingValue(field, value) {
29730
+ if (value === void 0 || value === null) return "—";
29731
+ if (typeof value === "boolean") return value ? "on" : "off";
29732
+ if (typeof value === "number") {
29733
+ const scale = field.type === "slider" ? field.displayScale : void 0;
29734
+ const unit = "unit" in field && typeof field.unit === "string" ? ` ${field.unit}` : "";
29735
+ return `${trimNumber(scale !== void 0 ? value / scale : value)}${unit}`;
29736
+ }
29737
+ const options = "options" in field && Array.isArray(field.options) ? field.options : [];
29738
+ const labelOf = (v) => {
29739
+ const hit = options.find((o) => o.value === v);
29740
+ return hit ? hit.label : String(v);
29741
+ };
29742
+ if (Array.isArray(value)) return value.length === 0 ? "none" : value.map(labelOf).join(", ");
29743
+ if (typeof value === "string") return labelOf(value);
29744
+ return JSON.stringify(value);
29576
29745
  }
29577
29746
  //#endregion
29578
- //#region src/composites/root-model-source.tsx
29579
- /**
29580
- * "Use the root model" — the step editor's control for a step the camera's
29581
- * ROOT detection model can supply (D730).
29582
- *
29583
- * The flag (`useRootModel`, in the step's own settings) is an operator INTENT
29584
- * kept beside the step's model: checking it never changes the model, and a
29585
- * root that cannot supply the step leaves the step running its own model while
29586
- * the flag waits, inactive, for a root that can. Whether it is in force is
29587
- * `resolveRootSupply` — the same function the runner decides with.
29588
- */
29589
- /** The editor's view of the flag for one step, or `null` for a step no root can supply. */
29590
- function rootSourceState(stepId, settings, rootModel) {
29591
- if (!(0, _camstack_types.isRootSuppliableStepId)(stepId)) return null;
29592
- return (0, _camstack_types.resolveRootSupply)(stepId, settings, rootModel?.supplies);
29747
+ //#region src/composites/step-inspector/layered-settings-form.tsx
29748
+ var SOURCE_TONE$1 = {
29749
+ accelerator: "border-amber-500/40 bg-amber-500/10 text-amber-500",
29750
+ camera: "border-primary/40 bg-primary/10 text-primary",
29751
+ node: "border-border bg-muted/40 text-foreground",
29752
+ default: "border-border/60 text-foreground-subtle"
29753
+ };
29754
+ /** DOM id of a field's box — the target of a search hit. */
29755
+ function stepFieldDomId(key) {
29756
+ return `step-field-${key}`;
29593
29757
  }
29594
- /** The settings bag with the flag set — every other key untouched. */
29595
- function withUseRootModel(settings, value) {
29758
+ function displayStep(addon, modelId, settings) {
29596
29759
  return {
29597
- ...settings,
29598
- [_camstack_types.USE_ROOT_MODEL_SETTING_KEY]: value
29760
+ addonId: addon.id,
29761
+ addonName: addon.name,
29762
+ slot: addon.slot,
29763
+ inputClasses: [...addon.inputClasses],
29764
+ outputClasses: [...addon.outputClasses],
29765
+ enabled: true,
29766
+ modelId,
29767
+ children: [],
29768
+ settings: { ...settings }
29599
29769
  };
29600
29770
  }
29601
- /** What the operator must know about a supply, beyond "it is on". */
29602
- function supplyNotes(supply) {
29603
- if (supply.kind === "label") {
29604
- const notes = supply.uncovered.length > 0 ? [`Not produced by this model: ${supply.uncovered.join(", ")}.`] : [];
29605
- if (supply.stepId === "package-detection") return [...notes, "The root model's own package boxes become the package result at this step's threshold; this step's zone-crop view is not used, so small parcels the full-frame model misses stay missed."];
29606
- return [...notes, "This step's class list decides which of the root's labels become the fine class; a switched-off class stays at its macro."];
29607
- }
29608
- if (supply.kind === "mask") return ["Masks come from the root model: each detection carries the root's own instance mask, so this step's model does not run. A root mask follows the model's mask grid (a quarter of its input), coarser than this step's own per-crop mask."];
29609
- if (supply.stepId === "face-detection") return ["Face boxes come from the root model. Each is re-found by this step's face detector in a square around it before recognition, so only a face it finds is recognised; a box with no face in it is dropped."];
29610
- return ["Plate boxes come from the root model and the plate reader reads them. The crop margin around a root plate box has not been measured for this model yet."];
29611
- }
29612
- /** The reason an intended supply is not in force, in the operator's words. */
29613
- function inactiveText(state, rootModel) {
29614
- switch (state.inactiveReason) {
29615
- case "root-input-too-small": return `the current root model (${rootModel.name}) sees the frame too small to find faces reliably (it needs an input of 640 px or more).`;
29616
- case "root-does-not-declare":
29617
- case void 0: return `the current root model (${rootModel.name}) cannot supply this.`;
29618
- }
29619
- }
29620
- function RootModelSourceControl({ stepId, settings, rootModel, disabled, onChange }) {
29621
- if (rootModel === void 0) return null;
29622
- const state = rootSourceState(stepId, settings, rootModel);
29623
- if (state === null) return null;
29624
- const inactiveNote = state.inactive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29625
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29626
- "data-testid": "root-model-inactive",
29771
+ var LAYER_IDS = [
29772
+ "accelerator",
29773
+ "camera",
29774
+ "node"
29775
+ ];
29776
+ /** "Camera · node 0.65 · model default 0.78 · [Reset to node]" under one field. */
29777
+ function SettingProvenance({ field, layers, target, labels, boundKeys, modelId, pinned, disabled, onReset, action }) {
29778
+ const effective = (0, _camstack_types.effectiveStepSettings)(layers);
29779
+ const fallback = defaultOfField(field, modelId);
29780
+ const defaultLabel = fallback.fromModel ? "model default" : "default";
29781
+ const stored = (0, _camstack_types.stepSettingSource)(layers, field.key, boundKeys);
29782
+ const bound = bindsToModel(field);
29783
+ const state = bound ? (0, _camstack_types.resolveModelBoundSetting)(effective, field.key, modelId, fallback.value, fallback.value) : null;
29784
+ const source = state === null || state.source === "override" ? stored : "default";
29785
+ const sourceText = source === "default" ? defaultLabel : labels[source] ?? source;
29786
+ const levels = LAYER_IDS.filter((id) => id !== source && labels[id] !== void 0).filter((id) => layers[id]?.[field.key] !== void 0).map((id) => {
29787
+ const binding = bound ? layers[id]?.[(0, _camstack_types.modelBindingKey)(field.key)] : void 0;
29788
+ const forOther = typeof binding === "string" && binding !== modelId ? ` (for ${binding})` : "";
29789
+ return `${labels[id]} ${formatSettingValue(field, layers[id]?.[field.key])}${forOther}`;
29790
+ });
29791
+ const withDefault = source === "default" ? levels : [...levels, `${defaultLabel} ${formatSettingValue(field, fallback.value)}`];
29792
+ const targetStores = (0, _camstack_types.stepSettingSource)({ [target]: layers[target] }, field.key, boundKeys) !== "default";
29793
+ const below = LAYER_IDS.slice(LAYER_IDS.indexOf(target) + 1).find((id) => labels[id] !== void 0 && layers[id]?.[field.key] !== void 0);
29794
+ const resetTo = below !== void 0 ? labels[below] ?? below : defaultLabel;
29795
+ const belowSource = stored === "default" ? "default" : (0, _camstack_types.stepSettingSource)({
29796
+ ...layers,
29797
+ [stored]: void 0
29798
+ }, field.key, boundKeys);
29799
+ const pinnedAt = belowSource === "default" ? defaultLabel : labels[belowSource] ?? belowSource;
29800
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29801
+ className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px]",
29627
29802
  children: [
29628
- "Use the root model: inactive — ",
29629
- inactiveText(state, rootModel),
29630
- " The step runs its own model."
29803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29804
+ "data-testid": "setting-source",
29805
+ className: cn("rounded border px-1.5 py-px font-medium", SOURCE_TONE$1[source]),
29806
+ children: sourceText
29807
+ }),
29808
+ withDefault.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29809
+ "data-testid": "setting-levels",
29810
+ className: "text-foreground-subtle",
29811
+ children: withDefault.join(" · ")
29812
+ }),
29813
+ pinned && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29814
+ className: "rounded border border-amber-500/40 px-1.5 py-px text-amber-500",
29815
+ title: "Stored by an older editor as a copy of the value below. It changes nothing today, but it stops later changes below from reaching this camera.",
29816
+ children: [
29817
+ "pinned at the ",
29818
+ pinnedAt.toLowerCase(),
29819
+ " value"
29820
+ ]
29821
+ }),
29822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29823
+ className: "ml-auto flex items-center gap-2",
29824
+ children: [action, targetStores && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
29825
+ type: "button",
29826
+ disabled,
29827
+ onClick: onReset,
29828
+ className: "rounded px-1 text-foreground-subtle hover:bg-muted hover:text-foreground disabled:opacity-50",
29829
+ children: ["Reset to ", resetTo.toLowerCase()]
29830
+ })]
29831
+ })
29631
29832
  ]
29632
- }) : null;
29633
- if (state.supply === void 0) return inactiveNote;
29634
- const checked = state.intent;
29833
+ });
29834
+ }
29835
+ function LayeredSettingsForm({ addon, fields, layers, target, labels, modelId, onChangeLayer, rootModel, pinnedKeys = [], fieldActions, headerActions, highlightKey = null, disabled = false }) {
29836
+ const boundKeys = (0, _camstack_types.collectModelBoundKeys)(fields);
29837
+ const effective = (0, _camstack_types.effectiveStepSettings)(layers);
29838
+ const targetLayer = layers[target];
29839
+ const step = displayStep(addon, modelId, effective);
29840
+ const rootState = rootSourceState(addon.id, effective, rootModel);
29635
29841
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29636
- className: "space-y-1",
29637
- "data-testid": "root-model-source",
29842
+ className: "space-y-2 text-xs",
29638
29843
  children: [
29639
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29640
- className: "flex items-center gap-2 text-xs text-foreground",
29844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29845
+ className: "flex flex-wrap items-center gap-2",
29641
29846
  children: [
29642
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29643
- type: "checkbox",
29644
- checked,
29645
- disabled,
29646
- onChange: (e) => onChange(e.target.checked)
29847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
29848
+ className: TEXT_SECTION_LABEL,
29849
+ children: "Settings"
29647
29850
  }),
29648
- "Use the root model (",
29649
- rootModel.name,
29650
- ")"
29851
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29852
+ className: TEXT_HINT,
29853
+ children: ["editing: ", labels[target] ?? target]
29854
+ }),
29855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29856
+ className: "ml-auto flex flex-wrap items-center gap-3",
29857
+ children: [headerActions, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29858
+ fields,
29859
+ modelId,
29860
+ settings: targetLayer,
29861
+ disabled,
29862
+ onReset: (next) => onChangeLayer(next)
29863
+ })]
29864
+ })
29651
29865
  ]
29652
29866
  }),
29653
- inactiveNote,
29654
- state.effective && supplyNotes(state.supply).map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29655
- className: "text-[10px] leading-snug text-foreground-subtle",
29656
- children: note
29657
- }, note))
29867
+ rootState !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29868
+ stepId: addon.id,
29869
+ settings: effective,
29870
+ rootModel,
29871
+ disabled,
29872
+ onChange: (v) => onChangeLayer((0, _camstack_types.withFieldFrom)(targetLayer, _camstack_types.USE_ROOT_MODEL_SETTING_KEY, withUseRootModel(effective, v), []))
29873
+ }),
29874
+ fields.map((field) => {
29875
+ if (!isValueField(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29876
+ field,
29877
+ allFields: fields,
29878
+ step,
29879
+ disabled,
29880
+ onChange: () => {}
29881
+ }, field.key);
29882
+ const highlighted = highlightKey === field.key;
29883
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29884
+ id: stepFieldDomId(field.key),
29885
+ "data-field-key": field.key,
29886
+ className: cn("rounded-md border border-transparent px-2 py-1 -mx-2", highlighted && "border-primary/50 bg-primary/5"),
29887
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29888
+ field,
29889
+ allFields: fields,
29890
+ step,
29891
+ disabled,
29892
+ hideModelBoundSourceRow: true,
29893
+ onChange: (next) => onChangeLayer((0, _camstack_types.withFieldFrom)(targetLayer, field.key, next.settings ?? {}, boundKeys))
29894
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingProvenance, {
29895
+ field,
29896
+ layers,
29897
+ target,
29898
+ labels,
29899
+ boundKeys,
29900
+ modelId,
29901
+ pinned: pinnedKeys.includes(field.key),
29902
+ disabled,
29903
+ onReset: () => onChangeLayer((0, _camstack_types.withoutField)(targetLayer, field.key, boundKeys)),
29904
+ action: fieldActions?.(field.key, (0, _camstack_types.stepSettingSource)(layers, field.key, boundKeys))
29905
+ })]
29906
+ }, field.key);
29907
+ })
29658
29908
  ]
29659
29909
  });
29660
29910
  }
29661
29911
  //#endregion
29662
29912
  //#region src/composites/agent-step-editor.tsx
29663
29913
  /**
29664
- * Per-(node,device) and per-camera editors must not surface knobs that the
29665
- * cluster already owns. Those fields still exist on the step's catalog
29666
- * schema so the runner can hydrate `child.settings`, but writing them here
29914
+ * A step's node-local fields: the catalog schema minus the knobs the CLUSTER
29915
+ * owns (`clusterStepSettingFieldsFor`). Those still exist on the schema so the
29916
+ * runner can hydrate `child.settings`, but writing them per node or per camera
29667
29917
  * would be a second store the overlay silently ignores.
29668
29918
  */
29669
- function nodeLocalConfigSchema(addon) {
29919
+ function stepLocalFields(addon) {
29670
29920
  const clusterKeys = new Set((0, _camstack_types.clusterStepSettingFieldsFor)(addon.id).map((field) => field.key));
29671
29921
  return (addon.configSchema ?? []).filter((field) => !clusterKeys.has(field.key));
29672
29922
  }
29673
- function ClusterSettingsHint({ addonId }) {
29674
- if ((0, _camstack_types.clusterStepSettingFieldsFor)(addonId).length === 0) return null;
29675
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29676
- className: "text-[10px] leading-snug text-foreground-subtle",
29677
- children: "Admission knobs for this step live in Pipeline → Cluster → Settings → Enrichment models. They apply to every camera — a per-device copy would fill the shared index from two policies."
29678
- });
29679
- }
29680
29923
  /**
29681
- * The model the step's settings are read and written for — what a model-bound
29924
+ * The model a step's settings are read and written for — what a model-bound
29682
29925
  * field (D738) binds to. While the ROOT supplies the step's fine label (D741),
29683
29926
  * that is the root model: the step's threshold then gates the root's label, on
29684
29927
  * the root's score scale. Otherwise the step's own model: its pin, else the
29685
29928
  * model the row really runs (`effectiveModelId` — the device's hardware-aware
29686
29929
  * default when unpinned), else the step's flat catalog default.
29687
29930
  */
29688
- function settingsModelIdFor(addon, cfg, effectiveModelId, rootModel) {
29689
- if (rootModel !== void 0 && rootSourceState(addon.id, cfg.settings, rootModel)?.effective) return rootModel.modelId;
29690
- return cfg.modelId ?? effectiveModelId ?? addon.defaultModelId;
29931
+ function settingsModelIdFor(addon, modelId, settings, rootModel) {
29932
+ if (rootModel !== void 0 && rootSourceState(addon.id, settings, rootModel)?.effective) return rootModel.modelId;
29933
+ return modelId ?? addon.defaultModelId;
29691
29934
  }
29692
- function buildDisplayStep(addon, cfg, modelId) {
29693
- return {
29694
- addonId: addon.id,
29695
- addonName: addon.name,
29696
- slot: addon.slot,
29697
- inputClasses: [...addon.inputClasses],
29698
- outputClasses: [...addon.outputClasses],
29699
- enabled: cfg.enabled,
29700
- modelId,
29701
- children: [],
29702
- settings: { ...cfg.settings }
29703
- };
29935
+ function ClusterSettingsHint({ addonId }) {
29936
+ if ((0, _camstack_types.clusterStepSettingFieldsFor)(addonId).length === 0) return null;
29937
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29938
+ className: "text-[10px] leading-snug text-foreground-subtle",
29939
+ children: "Admission knobs for this step live in Pipeline → Cluster → Settings → Enrichment models. They apply to every camera — a per-device copy would fill the shared index from two policies."
29940
+ });
29704
29941
  }
29705
29942
  /**
29706
29943
  * Shown in place of the model picker while the root model supplies the step:
@@ -29721,233 +29958,57 @@ function RootSuppliedModelNote({ rootModel, stepModelId }) {
29721
29958
  ]
29722
29959
  });
29723
29960
  }
29724
- function computeEffective(mode, agentDefault, patch) {
29725
- if (mode !== "device" || !patch) return agentDefault;
29726
- return {
29727
- enabled: agentDefault.enabled,
29728
- modelId: patch.modelId ?? agentDefault.modelId,
29729
- settings: patch.settings ?? agentDefault.settings
29730
- };
29731
- }
29732
- function AgentModeEditor({ addon, agentDefault, modelsForFormat, engineFormat, effectiveModelId, rootModel, onChangeAgentConfig }) {
29961
+ /**
29962
+ * AgentStepEditor — one step on ONE accelerator: its model and its settings,
29963
+ * as a single node-level layer. Model Studio's "settings for this installed
29964
+ * model" dialog uses it. The pipeline pages use the step matrix's panel, which lays
29965
+ * the same parts out across accelerators and layers (D754/D757); the settings
29966
+ * form here is that panel's `LayeredSettingsForm` with one layer.
29967
+ */
29968
+ function AgentStepEditor({ addon, agentDefault, onChangeAgentConfig, engineFormat, effectiveModelId, rootModel }) {
29969
+ const modelsForFormat = (0, react$1.useMemo)(() => stepModelOptions(addon.models, engineFormat), [addon.models, engineFormat]);
29733
29970
  const update = (patch) => {
29734
29971
  onChangeAgentConfig?.({
29735
29972
  ...agentDefault,
29736
29973
  ...patch
29737
29974
  });
29738
29975
  };
29739
- const localSchema = nodeLocalConfigSchema(addon);
29976
+ const fields = stepLocalFields(addon);
29740
29977
  const formatDefault = addon.defaultModelIdByFormat?.[engineFormat] ?? addon.defaultModelId;
29741
29978
  const rootSupplied = rootSourceState(addon.id, agentDefault.settings, rootModel)?.effective === true;
29979
+ const runningModel = agentDefault.modelId ?? effectiveModelId;
29742
29980
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29743
29981
  className: "space-y-4 text-xs",
29744
29982
  children: [
29745
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29746
- stepId: addon.id,
29747
- settings: agentDefault.settings,
29748
- rootModel,
29749
- onChange: (v) => update({ settings: withUseRootModel(agentDefault.settings, v) })
29750
- }),
29751
29983
  rootSupplied && rootModel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootSuppliedModelNote, {
29752
29984
  rootModel,
29753
- stepModelId: agentDefault.modelId ?? effectiveModelId
29985
+ stepModelId: runningModel
29754
29986
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29755
29987
  className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
29756
29988
  children: "Model"
29757
29989
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29758
29990
  models: modelsForFormat,
29759
- value: agentDefault.modelId ?? effectiveModelId ?? "",
29991
+ value: runningModel ?? "",
29760
29992
  defaultModelId: formatDefault,
29761
29993
  onChange: (modelId) => update({ modelId })
29762
29994
  })] }),
29763
- localSchema.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29764
- className: "pt-2 border-t border-border/50 space-y-3",
29765
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29766
- className: "flex items-center justify-between",
29767
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29768
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29769
- children: "Settings"
29770
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29771
- fields: localSchema,
29772
- modelId: settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel),
29773
- settings: agentDefault.settings,
29774
- onReset: (settings) => update({ settings })
29775
- })]
29776
- }), localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29777
- field,
29778
- allFields: localSchema,
29779
- step: buildDisplayStep(addon, agentDefault, settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel)),
29780
- disabled: false,
29781
- onChange: (next) => update({ settings: { ...next.settings } })
29782
- }, field.key))]
29995
+ (fields.length > 0 || rootSourceState(addon.id, agentDefault.settings, rootModel)) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29996
+ className: "pt-2 border-t border-border/50",
29997
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayeredSettingsForm, {
29998
+ addon,
29999
+ fields,
30000
+ layers: { node: agentDefault.settings },
30001
+ target: "node",
30002
+ labels: { node: "This node" },
30003
+ modelId: settingsModelIdFor(addon, runningModel, agentDefault.settings, rootModel),
30004
+ ...rootModel !== void 0 ? { rootModel } : {},
30005
+ onChangeLayer: (settings) => update({ settings })
30006
+ })
29783
30007
  }),
29784
30008
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterSettingsHint, { addonId: addon.id })
29785
30009
  ]
29786
30010
  });
29787
30011
  }
29788
- function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, modelsForFormat, engineFormat, effectiveModelId, rootModel, onChangePatch }) {
29789
- const effective = computeEffective("device", agentDefault, currentPatch);
29790
- const inheritedModelId = agentDefault.modelId ?? effectiveModelId;
29791
- const formatDefault = addon.defaultModelIdByFormat?.[engineFormat] ?? addon.defaultModelId;
29792
- const commitPatch = (next) => {
29793
- const hasAnyField = next.modelId !== void 0 || next.settings !== void 0;
29794
- onChangePatch?.(hasAnyField ? next : null);
29795
- };
29796
- const setModel = (modelId) => {
29797
- const { modelId: _prev, ...rest } = currentPatch ?? {};
29798
- commitPatch(modelId === void 0 ? rest : {
29799
- ...rest,
29800
- modelId
29801
- });
29802
- };
29803
- const setSettings = (settings) => {
29804
- const { settings: _prev, ...rest } = currentPatch ?? {};
29805
- commitPatch(settings === void 0 ? rest : {
29806
- ...rest,
29807
- settings
29808
- });
29809
- };
29810
- const modelOverridden = currentPatch?.modelId !== void 0;
29811
- const localSchema = nodeLocalConfigSchema(addon);
29812
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29813
- className: "space-y-4 text-xs",
29814
- children: [
29815
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29816
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29817
- children: [
29818
- "Provisioning for ",
29819
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29820
- className: "font-semibold text-foreground",
29821
- children: agentNodeId
29822
- }),
29823
- " — model + settings only. Whether this camera runs the step is set by its per-camera gate above."
29824
- ]
29825
- }),
29826
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29827
- stepId: addon.id,
29828
- settings: effective.settings,
29829
- rootModel,
29830
- onChange: (v) => setSettings(withUseRootModel(effective.settings, v))
29831
- }),
29832
- rootSourceState(addon.id, effective.settings, rootModel)?.effective === true && rootModel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootSuppliedModelNote, {
29833
- rootModel,
29834
- stepModelId: effective.modelId ?? inheritedModelId
29835
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
29836
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29837
- className: "flex items-center justify-between mb-1",
29838
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29839
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29840
- children: "Model"
29841
- }), modelOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29842
- type: "button",
29843
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29844
- onClick: () => setModel(void 0),
29845
- children: "Clear"
29846
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29847
- type: "button",
29848
- className: "text-[10px] text-primary hover:underline",
29849
- onClick: () => setModel(inheritedModelId),
29850
- children: "Override"
29851
- })]
29852
- }),
29853
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29854
- models: modelsForFormat,
29855
- disabled: !modelOverridden,
29856
- value: (modelOverridden ? currentPatch?.modelId ?? inheritedModelId : inheritedModelId) ?? "",
29857
- defaultModelId: formatDefault,
29858
- onChange: (modelId) => setModel(modelId)
29859
- }),
29860
- !modelOverridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29861
- className: "text-[10px] text-foreground-subtle mt-1",
29862
- children: ["inherits from ", agentNodeId]
29863
- })
29864
- ] }),
29865
- localSchema.length > 0 && (() => {
29866
- const settingsOverridden = currentPatch?.settings !== void 0;
29867
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29868
- className: "pt-2 border-t border-border/50 space-y-3",
29869
- children: [
29870
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29871
- className: "flex items-center justify-between",
29872
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29873
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29874
- children: "Settings"
29875
- }), settingsOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29876
- className: "flex items-center gap-3",
29877
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29878
- fields: localSchema,
29879
- modelId: settingsModelIdFor(addon, effective, inheritedModelId, rootModel),
29880
- settings: currentPatch?.settings,
29881
- onReset: (settings) => setSettings(settings)
29882
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29883
- type: "button",
29884
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29885
- onClick: () => setSettings(void 0),
29886
- children: "Clear"
29887
- })]
29888
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29889
- type: "button",
29890
- className: "text-[10px] text-primary hover:underline",
29891
- onClick: () => setSettings({ ...agentDefault.settings }),
29892
- children: "Override"
29893
- })]
29894
- }),
29895
- localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29896
- field,
29897
- allFields: localSchema,
29898
- step: buildDisplayStep(addon, effective, settingsModelIdFor(addon, effective, inheritedModelId, rootModel)),
29899
- disabled: !settingsOverridden,
29900
- onChange: (next) => setSettings({ ...next.settings })
29901
- }, field.key)),
29902
- !settingsOverridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29903
- className: "text-[10px] text-foreground-subtle",
29904
- children: ["inherits from ", agentNodeId]
29905
- })
29906
- ]
29907
- });
29908
- })(),
29909
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterSettingsHint, { addonId: addon.id }),
29910
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29911
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29912
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29913
- className: "font-semibold text-foreground mb-0.5",
29914
- children: "Effective provisioning preview"
29915
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29916
- className: "font-mono truncate",
29917
- children: [
29918
- addon.id,
29919
- " · model: ",
29920
- effective.modelId ?? inheritedModelId ?? "Auto"
29921
- ]
29922
- })]
29923
- })
29924
- ]
29925
- });
29926
- }
29927
- function AgentStepEditor(props) {
29928
- const { mode, addon, agentDefault, agentNodeId, currentPatch, onChangeAgentConfig, onChangePatch, engineFormat, effectiveModelId, rootModel } = props;
29929
- const modelsForFormat = (0, react$1.useMemo)(() => stepModelOptions(addon.models, engineFormat), [addon.models, engineFormat]);
29930
- if (mode === "agent") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentModeEditor, {
29931
- addon,
29932
- agentDefault,
29933
- modelsForFormat,
29934
- engineFormat,
29935
- effectiveModelId,
29936
- ...rootModel !== void 0 ? { rootModel } : {},
29937
- onChangeAgentConfig
29938
- });
29939
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceModeEditor, {
29940
- addon,
29941
- agentDefault,
29942
- agentNodeId,
29943
- currentPatch,
29944
- modelsForFormat,
29945
- engineFormat,
29946
- effectiveModelId,
29947
- ...rootModel !== void 0 ? { rootModel } : {},
29948
- onChangePatch
29949
- });
29950
- }
29951
30012
  //#endregion
29952
30013
  //#region src/primitives/icon-button.tsx
29953
30014
  var iconButtonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
@@ -33862,117 +33923,6 @@ async function waitForIceGathering(pc) {
33862
33923
  });
33863
33924
  }
33864
33925
  //#endregion
33865
- //#region src/lib/responsive.ts
33866
- /**
33867
- * Responsive design tokens — three-tier system.
33868
- *
33869
- * The admin-ui targets three width tiers; every component should pick
33870
- * the right token from this file instead of hand-coding breakpoints.
33871
- * Adopting the tokens uniformly is what makes the app feel consistent
33872
- * across operator devices (mobile / tablet / desktop) and what unlocks
33873
- * targeted tuning (e.g. shrink every label by 1px in one place).
33874
- *
33875
- * Tier definitions (Tailwind breakpoints):
33876
- *
33877
- * • **L1 — Compact** (`< 640px`, default = mobile portrait)
33878
- * Single column, tight padding, smallest text, icon-only buttons
33879
- * where labels can be inferred. The operator can still see every
33880
- * stat / action — no horizontal scroll, no clipped text. The user
33881
- * directive: "molta informazione in poco spazio quando la width è
33882
- * bassa." [a lot of information in little space when the width is low.]
33883
- *
33884
- * • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
33885
- * 2-column grids, side panels become visible, button labels return.
33886
- * Targets ~768px–1024px viewports — the most common operator sizes.
33887
- *
33888
- * • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
33889
- * 4-column grids, generous padding, full-width header text,
33890
- * side-by-side composite layouts. The operator has elbow room.
33891
- *
33892
- * Conventions:
33893
- * - **Always start from L1** — the bare class is always the compact
33894
- * variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
33895
- * desktop-first breakpoints; no max-width queries.
33896
- * - **Token strings are static** — kept as `as const` so callers can
33897
- * compose them via `cn()` / template literals without running a
33898
- * class generator at runtime.
33899
- * - **Pair the token with a comment** — the comment in the file
33900
- * names every breakpoint it crosses, so a reader can predict the
33901
- * layout without going to Tailwind docs.
33902
- */
33903
- /** Container padding for a card-style section (header + body). */
33904
- var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
33905
- /** Header strip inside a card (between border-b lines). */
33906
- var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
33907
- /** Body padding for a card-style section. */
33908
- var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
33909
- /** Gap between cards in a vertical stack. */
33910
- var STACK_GAP = "space-y-3 sm:space-y-4";
33911
- /** Gap between cards in a grid (4 cells / row at lg). */
33912
- var GRID_GAP = "gap-2 sm:gap-3";
33913
- /** Row padding inside a divided list (e.g. InfoRow). */
33914
- var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
33915
- /** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
33916
- var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
33917
- /** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
33918
- var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
33919
- /** Side-by-side layout that stacks below md (canvas + side panel). */
33920
- var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
33921
- /** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
33922
- var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
33923
- /**
33924
- * The tier boundary at which a settings label/value row stops stacking.
33925
- *
33926
- * Below it (L1 — compact) the row is ONE column: label on its own line, value
33927
- * on the next at full width. Above it the row is label-left / value-right as
33928
- * before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
33929
- * value beside it; a phone has neither, and the failure mode is not "cramped"
33930
- * but "the value is the part that gets cut" — the one thing the operator
33931
- * opened the page to read.
33932
- */
33933
- var SETTING_ROW_STACK_BREAKPOINT = "sm";
33934
- /** Outer row: single column at L1, label/value columns from `sm` up. */
33935
- var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
33936
- /** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
33937
- var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
33938
- /**
33939
- * Value lane — the value text plus its trailing affordances (copy / reveal).
33940
- * `w-full` at L1 so the value owns the whole row once the label is above it;
33941
- * `sm:flex-1` makes it share the row again from `sm` up.
33942
- */
33943
- var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
33944
- /**
33945
- * The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
33946
- * at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
33947
- * desktop form, where the row is wide enough for truncation to be a choice
33948
- * rather than data loss.
33949
- */
33950
- var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
33951
- /** Section header label (uppercase tracking-wider). */
33952
- var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
33953
- /** Field label inside a row. */
33954
- var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
33955
- /** Sub-label / hint text. */
33956
- var TEXT_HINT = "text-[10px] text-foreground-subtle";
33957
- /** Primary value (a metric, badge, …). */
33958
- var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
33959
- /** Big metric value (StatCard). */
33960
- var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
33961
- /** Default compact button. */
33962
- var BTN_COMPACT = "inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40";
33963
- /** Primary compact button. */
33964
- var BTN_COMPACT_PRIMARY = "inline-flex items-center justify-center gap-1 rounded border border-primary/40 bg-primary/15 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-primary hover:bg-primary/25 transition-colors disabled:opacity-40";
33965
- /** Danger compact button. */
33966
- var BTN_COMPACT_DANGER = "inline-flex items-center justify-center gap-1 rounded border border-danger/40 bg-danger/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-danger hover:bg-danger/20 transition-colors disabled:opacity-40";
33967
- /** Warning compact button (e.g. cancel-drawing). */
33968
- var BTN_COMPACT_WARNING = "inline-flex items-center justify-center gap-1 rounded border border-warning/40 bg-warning/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-warning hover:bg-warning/20 transition-colors disabled:opacity-40";
33969
- /** Compact text/select/number input. */
33970
- var INPUT_COMPACT = "w-full rounded border border-border bg-background px-1.5 py-0.5 sm:px-2 sm:py-1 text-[11px] sm:text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none";
33971
- /** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
33972
- var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
33973
- var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
33974
- var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
33975
- //#endregion
33976
33926
  //#region src/composites/data-table-layout.ts
33977
33927
  /**
33978
33928
  * At this many columns and above, `auto` switches a narrow viewport to cards.
@@ -55443,25 +55393,46 @@ function ClassChips({ inputs, outputs }) {
55443
55393
  * the first column; each device's real format is shown per sub-column instead
55444
55394
  * ("Coral Edge TPU · tflite").
55445
55395
  */
55396
+ /** The per-accelerator lines of a column header: name, format, model, off. */
55397
+ function AgentColumnLines({ column }) {
55398
+ const name = column.deviceLabel ?? column.deviceKey;
55399
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
55400
+ (name !== void 0 || column.disabled === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55401
+ className: "flex min-w-0 items-center gap-1",
55402
+ children: [name !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55403
+ className: "truncate text-[10px] font-medium text-foreground/90",
55404
+ children: name
55405
+ }), column.disabled === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55406
+ className: "shrink-0 rounded bg-muted px-1 text-[8px] font-semibold uppercase text-foreground-subtle",
55407
+ children: "off"
55408
+ })]
55409
+ }),
55410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55411
+ className: "truncate text-[9px] text-foreground-subtle",
55412
+ children: column.engineLabel
55413
+ }),
55414
+ column.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55415
+ className: "truncate font-mono text-[9px] text-foreground-subtle",
55416
+ title: column.detail,
55417
+ children: column.detail
55418
+ })
55419
+ ] });
55420
+ }
55446
55421
  function GroupedAgentHeader({ group }) {
55422
+ const nodeLabel = group.columns[0]?.nodeLabel ?? group.agentNodeId;
55447
55423
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55424
+ role: "columnheader",
55448
55425
  className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60",
55449
55426
  style: { gridColumn: `span ${group.columns.length}` },
55450
55427
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55451
55428
  className: "truncate px-3 pt-2 pb-1 text-xs font-semibold text-foreground",
55452
- children: group.agentNodeId
55429
+ children: nodeLabel
55453
55430
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55454
55431
  className: "mt-1 flex border-t border-border/50",
55455
- children: group.columns.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55456
- className: cn("min-w-0 flex-1 px-3 py-1", i > 0 && "border-l border-border/50"),
55457
- title: `${c.deviceLabel ?? c.deviceKey} · ${c.engineLabel}`,
55458
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55459
- className: "truncate text-[10px] font-medium text-foreground/90",
55460
- children: c.deviceLabel ?? c.deviceKey
55461
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55462
- className: "truncate text-[9px] text-foreground-subtle",
55463
- children: c.engineLabel
55464
- })]
55432
+ children: group.columns.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55433
+ className: cn("min-w-0 flex-1 px-3 py-1", i > 0 && "border-l border-border/50", c.disabled === true && "opacity-70"),
55434
+ title: `${c.deviceLabel ?? c.deviceKey} · ${c.engineLabel}${c.detail ? ` · ${c.detail}` : ""}`,
55435
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentColumnLines, { column: c })
55465
55436
  }, agentColumnKey(c)))
55466
55437
  })]
55467
55438
  });
@@ -55565,97 +55536,75 @@ function cellButtonClass(state, isSelected, extra) {
55565
55536
  //#endregion
55566
55537
  //#region src/composites/device-step-matrix.tsx
55567
55538
  /**
55568
- * DeviceStepMatrix — the per-camera "Detection pipeline → Steps" surface (#33)
55569
- * rendered as ONE hierarchical steps×nodes matrix.
55570
- *
55571
- * Column 1 (STICKY): the indented step tree. Each step carries its slot +
55572
- * name + class-flow chips PLUS a per-camera, node-independent GATE control
55573
- * with THREE states — On / Off / Default (inherit). `package-detection` is
55574
- * zone-driven, so it shows a "Zones" badge instead of a gate.
55575
- * Columns 2..N (one per online node): the provisioned model chip (click →
55576
- * the model/config picker), a clickable "⚠ skip" when the step is gated ON
55577
- * but the node has no compatible model, or "—" when N/A.
55578
- *
55579
- * Responsive, container-measured (never a global media query, so it also works
55580
- * embedded):
55581
- * A) wide — sticky step column + node columns in an `overflow-x:auto` box, so
55582
- * narrow viewports scroll the NODES sideways while col-1 stays pinned and
55583
- * the page body never scrolls horizontally.
55584
- * B) narrow — below the width needed for ≥2 node columns, collapse to a
55585
- * single-node view: a node selector picks ONE node and the matrix renders
55586
- * `step-tree × that-one-node`.
55587
- */
55588
- /** Wider than the cluster matrix's 18rem: col-1 also hosts the gate control. */
55589
- var STEP_COL_REM$1 = 21;
55590
- var STEP_COL_PX = STEP_COL_REM$1 * 16;
55591
- /**
55592
- * Narrow (mobile) step-column width. On a phone the wide 21rem column would
55593
- * occlude nearly the whole viewport, so the single-node layout pins a slimmer
55594
- * sticky step column and lets the device columns scroll horizontally beside it
55595
- * (both fully readable) rather than cramming every column into the viewport.
55539
+ * DeviceStepMatrix — the step × accelerator TABLE of a detection pipeline
55540
+ * (D757): the node's Pipeline tab and the camera's Detection pipeline → Steps
55541
+ * are this matrix, and a click on a cell (or a step's header) opens that step's
55542
+ * settings in a docked panel (`StepDetailDock`) that overlays the page instead
55543
+ * of reflowing the matrix.
55544
+ *
55545
+ * Column 1 (STICKY): the indented step tree — slot, name, class-flow chips,
55546
+ * and an optional badge (the camera's gate). The header is a button: it
55547
+ * opens the step on the selected accelerator.
55548
+ * Columns 2..N: one per accelerator, grouped under their node; the header
55549
+ * names the accelerator, its format, the model it elects, and "off" for a
55550
+ * switched-off one. A cell shows the model that runs there, or "⚠ skip",
55551
+ * "→ iGPU", "root", "cluster", "ovr", "—".
55552
+ *
55553
+ * Keyboard: ONE Tab stop (a roving tabindex over the step headers and cells);
55554
+ * arrows move, Home / End go to the row's ends, Enter or Space opens.
55555
+ *
55556
+ * `reserveRightPx`: the width a docked panel covers at the right of the
55557
+ * viewport. The matrix adds a trailing spacer that wide (so any column can
55558
+ * scroll clear of it) and scrolls the selected cell into the uncovered part —
55559
+ * it never shrinks, so nothing reflows when the panel opens.
55560
+ *
55561
+ * Responsive, container-measured: narrower than the step column plus two
55562
+ * columns, the matrix shows ONE node at a time with a node selector.
55596
55563
  */
55597
- var STEP_COL_SINGLE_REM = 11;
55598
- function activeChoice(gate) {
55599
- if (!gate.explicit) return "default";
55600
- return gate.effectiveOn ? "on" : "off";
55601
- }
55564
+ /** The sticky step column. */
55565
+ var STEP_COL_REM = 18;
55566
+ var STEP_COL_PX = STEP_COL_REM * 16;
55602
55567
  /**
55603
- * Tri-state segmented control. `Default` inherits the catalog value (shown in
55604
- * the caption); `On`/`Off` pin an explicit per-camera override. Touch-friendly
55605
- * 28px segments.
55568
+ * Narrow (mobile) step-column width. On a phone the wide column would occlude
55569
+ * nearly the whole viewport, so the single-node layout pins a slimmer sticky
55570
+ * step column and lets the device columns scroll horizontally beside it.
55606
55571
  */
55607
- function GateControl({ gate, busy, onSet }) {
55608
- const active = activeChoice(gate);
55609
- const seg = (choice, label, value, activeClass) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55610
- type: "button",
55611
- "aria-pressed": active === choice,
55612
- disabled: busy,
55613
- onClick: () => onSet(value),
55614
- className: cn("flex-1 px-2 py-1 text-[11px] font-medium leading-none transition-colors", "min-h-[26px]", active === choice ? activeClass : "text-foreground-subtle hover:bg-muted/60", busy && "opacity-50 cursor-not-allowed"),
55615
- children: label
55616
- });
55617
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55618
- className: "flex flex-col gap-1",
55619
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55620
- className: "inline-flex w-fit max-w-full self-start overflow-hidden rounded-md border border-border",
55621
- children: [
55622
- seg("default", "Auto", null, "bg-primary/15 text-primary"),
55623
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
55624
- seg("on", "On", true, "bg-emerald-500 text-white"),
55625
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
55626
- seg("off", "Off", false, "bg-foreground-subtle/70 text-white")
55627
- ]
55628
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55629
- className: "text-[10px] leading-none text-foreground-subtle",
55630
- children: active === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
55631
- "= default (",
55632
- gate.defaultOn ? "on" : "off",
55633
- ")"
55634
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: ["overridden · default ", gate.defaultOn ? "on" : "off"] })
55635
- })]
55636
- });
55637
- }
55638
- function ZoneBadge() {
55639
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55640
- className: "flex flex-col gap-1",
55641
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55642
- className: "inline-flex w-fit items-center gap-1 rounded-md border border-violet-500/40 bg-violet-500/10 px-2 py-1 text-[11px] font-medium text-violet-400",
55643
- children: "Zones"
55644
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55645
- className: "text-[10px] leading-none text-foreground-subtle",
55646
- children: "driven by package zones"
55647
- })]
55648
- });
55572
+ var STEP_COL_SINGLE_REM = 11;
55573
+ /** Where a key press moves the roving focus in a `rows × cols` grid. */
55574
+ function nextGridPosition(key, pos, rows, cols) {
55575
+ const clamp = (r, c) => ({
55576
+ r: Math.max(0, Math.min(rows - 1, r)),
55577
+ c: Math.max(0, Math.min(cols - 1, c))
55578
+ });
55579
+ switch (key) {
55580
+ case "ArrowRight": return clamp(pos.r, pos.c + 1);
55581
+ case "ArrowLeft": return clamp(pos.r, pos.c - 1);
55582
+ case "ArrowDown": return clamp(pos.r + 1, pos.c);
55583
+ case "ArrowUp": return clamp(pos.r - 1, pos.c);
55584
+ case "Home": return clamp(pos.r, 0);
55585
+ case "End": return clamp(pos.r, cols - 1);
55586
+ case "PageDown": return clamp(rows - 1, pos.c);
55587
+ case "PageUp": return clamp(0, pos.c);
55588
+ default: return null;
55589
+ }
55649
55590
  }
55650
- function StepGateCell({ node, depth, gate, busy, onSet, hideGate }) {
55651
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55652
- className: "sticky left-0 z-10 flex flex-col gap-2 border-b border-border bg-surface px-3 py-2.5 text-xs min-w-0",
55653
- style: { paddingLeft: `${12 + depth * 14}px` },
55654
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), hideGate ? null : gate.kind === "zone-driven" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoneBadge, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GateControl, {
55655
- gate,
55656
- busy,
55657
- onSet
55658
- })]
55591
+ function StepCell({ node, depth, r, tabbable, selected, badge, onClick, sticky }) {
55592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55593
+ role: "rowheader",
55594
+ className: cn("min-w-0 border-b border-border bg-surface", sticky && "sticky left-0 z-10"),
55595
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
55596
+ type: "button",
55597
+ "data-r": r,
55598
+ "data-c": 0,
55599
+ "data-step": node.addonId,
55600
+ "data-col": "",
55601
+ tabIndex: tabbable ? 0 : -1,
55602
+ "aria-current": selected ? "true" : void 0,
55603
+ onClick: onClick ? (e) => onClick(e.detail === 0) : void 0,
55604
+ className: cn("flex h-full w-full min-w-0 items-start gap-2 px-3 py-2 text-left text-xs outline-none", "hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary", selected && "bg-primary/10 shadow-[inset_3px_0_0_var(--color-primary)]"),
55605
+ style: { paddingLeft: `${12 + depth * 14}px` },
55606
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), badge]
55607
+ })
55659
55608
  });
55660
55609
  }
55661
55610
  /**
@@ -55685,76 +55634,179 @@ function NodeSelectorBar({ nodes, selected, onSelect }) {
55685
55634
  role: "tab",
55686
55635
  "aria-selected": isSelected,
55687
55636
  onClick: () => onSelect(a.agentNodeId),
55688
- title: `${a.agentNodeId} · ${a.engineLabel}`,
55637
+ title: `${a.columns[0]?.nodeLabel ?? a.agentNodeId} · ${a.engineLabel}`,
55689
55638
  className: cn("px-2.5 py-1 transition-colors", isSelected ? "bg-primary/15 text-primary" : "bg-transparent text-foreground-subtle hover:bg-surface-hover hover:text-foreground", idx > 0 && "border-l border-border"),
55690
- children: a.agentNodeId
55639
+ children: a.columns[0]?.nodeLabel ?? a.agentNodeId
55691
55640
  }, a.agentNodeId);
55692
55641
  })
55693
55642
  })
55694
55643
  ]
55695
55644
  });
55696
55645
  }
55697
- function MatrixGrid$1({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single, hideGate }) {
55646
+ var MatrixGrid = (0, react$1.forwardRef)(function MatrixGrid({ tree, agents, getCellState, onCellClick, onStepClick, stepBadge, selectedCell, single, reserveRightPx, label }, ref) {
55698
55647
  const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
55699
55648
  const groups = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
55649
+ const boxRef = (0, react$1.useRef)(null);
55650
+ const stepColRem = single ? STEP_COL_SINGLE_REM : STEP_COL_REM;
55651
+ const gridTemplate = `${stepColRem}rem repeat(${agents.length}, 13rem)`;
55652
+ const cols = agents.length + 1;
55653
+ const [stickySteps, setStickySteps] = (0, react$1.useState)(true);
55654
+ (0, react$1.useLayoutEffect)(() => {
55655
+ const box = boxRef.current;
55656
+ if (!box || reserveRightPx <= 0) {
55657
+ setStickySteps(true);
55658
+ return;
55659
+ }
55660
+ const measure = () => {
55661
+ const left = box.getBoundingClientRect().left;
55662
+ setStickySteps(window.innerWidth - reserveRightPx - left >= stepColRem * 16 + 208 + 24);
55663
+ };
55664
+ measure();
55665
+ window.addEventListener("resize", measure);
55666
+ return () => window.removeEventListener("resize", measure);
55667
+ }, [reserveRightPx, stepColRem]);
55668
+ const selectedPos = (0, react$1.useMemo)(() => {
55669
+ if (selectedCell === null) return null;
55670
+ const r = rows.findIndex((row) => row.node.addonId === selectedCell.addonId);
55671
+ if (r < 0) return null;
55672
+ const c = agents.findIndex((a) => agentColumnKey(a) === agentColumnKey(selectedCell));
55673
+ return {
55674
+ r,
55675
+ c: c < 0 ? 0 : c + 1
55676
+ };
55677
+ }, [
55678
+ selectedCell,
55679
+ rows,
55680
+ agents
55681
+ ]);
55682
+ const [focusPos, setFocusPos] = (0, react$1.useState)({
55683
+ r: 0,
55684
+ c: 0
55685
+ });
55686
+ (0, react$1.useEffect)(() => {
55687
+ if (selectedPos !== null) setFocusPos(selectedPos);
55688
+ }, [selectedPos]);
55689
+ const tab = focusPos.r < rows.length && focusPos.c < cols ? focusPos : {
55690
+ r: 0,
55691
+ c: 0
55692
+ };
55693
+ const focusAt = (r, c) => {
55694
+ boxRef.current?.querySelector(`[data-r="${r}"][data-c="${c}"]`)?.focus();
55695
+ };
55696
+ (0, react$1.useImperativeHandle)(ref, () => ({ focusCell: (addonId, columnKey) => {
55697
+ const r = rows.findIndex((row) => row.node.addonId === addonId);
55698
+ if (r < 0) return;
55699
+ const index = columnKey === null ? -1 : agents.findIndex((a) => agentColumnKey(a) === columnKey);
55700
+ const c = index < 0 ? 0 : index + 1;
55701
+ setFocusPos({
55702
+ r,
55703
+ c
55704
+ });
55705
+ focusAt(r, c);
55706
+ } }), [rows, agents]);
55707
+ (0, react$1.useLayoutEffect)(() => {
55708
+ if (selectedPos === null) return;
55709
+ const box = boxRef.current;
55710
+ const el = box?.querySelector(`[data-r="${selectedPos.r}"][data-c="${selectedPos.c}"]`);
55711
+ if (!box || !el) return;
55712
+ if (typeof el.scrollIntoView === "function") el.scrollIntoView({
55713
+ block: "nearest",
55714
+ inline: "nearest"
55715
+ });
55716
+ if (reserveRightPx <= 0) return;
55717
+ const limit = window.innerWidth - reserveRightPx - 12;
55718
+ const right = el.getBoundingClientRect().right;
55719
+ if (right > limit) box.scrollLeft += right - limit;
55720
+ }, [
55721
+ selectedPos,
55722
+ reserveRightPx,
55723
+ stickySteps
55724
+ ]);
55725
+ const onKeyDown = (e) => {
55726
+ if (!(e.target instanceof HTMLElement) || e.target.dataset.r === void 0) return;
55727
+ const pos = {
55728
+ r: Number(e.target.dataset.r),
55729
+ c: Number(e.target.dataset.c)
55730
+ };
55731
+ const next = nextGridPosition(e.key, pos, rows.length, cols);
55732
+ if (next === null) return;
55733
+ e.preventDefault();
55734
+ setFocusPos(next);
55735
+ focusAt(next.r, next.c);
55736
+ };
55700
55737
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55738
+ ref: boxRef,
55701
55739
  className: "overflow-auto rounded border border-border",
55702
55740
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55703
- className: "grid w-max",
55704
- style: { gridTemplateColumns: `${single ? STEP_COL_SINGLE_REM : STEP_COL_REM$1}rem repeat(${agents.length}, 13rem)` },
55705
- children: [
55706
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55707
- className: "sticky left-0 top-0 z-20 border-b border-border bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle",
55708
- children: hideGate ? "Step" : "Step · gate"
55709
- }),
55710
- groups.map((group) => group.grouped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedAgentHeader, { group }, group.agentNodeId) : group.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55711
- className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2",
55741
+ className: "flex w-max",
55742
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55743
+ role: "grid",
55744
+ "aria-label": label,
55745
+ "aria-rowcount": rows.length + 1,
55746
+ className: "grid w-max",
55747
+ style: { gridTemplateColumns: gridTemplate },
55748
+ onKeyDown,
55749
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55750
+ role: "row",
55751
+ className: "contents",
55712
55752
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55713
- className: "truncate text-xs font-semibold text-foreground",
55714
- children: col.agentNodeId
55715
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55716
- className: "truncate text-[10px] text-foreground-subtle",
55717
- children: col.engineLabel
55753
+ role: "columnheader",
55754
+ className: cn("top-0 z-20 border-b border-border bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle", stickySteps ? "sticky left-0" : "sticky"),
55755
+ children: "Step"
55756
+ }), groups.map((group) => group.grouped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedAgentHeader, { group }, group.agentNodeId) : group.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55757
+ role: "columnheader",
55758
+ className: cn("sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2", col.disabled === true && "opacity-70"),
55759
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55760
+ className: "truncate text-xs font-semibold text-foreground",
55761
+ children: col.nodeLabel ?? col.agentNodeId
55762
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentColumnLines, { column: col })]
55763
+ }, agentColumnKey(col))))]
55764
+ }), rows.map(({ node, depth }, r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55765
+ role: "row",
55766
+ className: "contents",
55767
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepCell, {
55768
+ node,
55769
+ depth,
55770
+ r,
55771
+ tabbable: tab.r === r && tab.c === 0,
55772
+ selected: selectedCell?.addonId === node.addonId,
55773
+ badge: stepBadge?.(node.addonId),
55774
+ onClick: onStepClick ? (kb) => onStepClick(node.addonId, kb) : void 0,
55775
+ sticky: stickySteps
55776
+ }), agents.map((a, i) => {
55777
+ const state = getCellState(node.addonId, a);
55778
+ const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
55779
+ const actionable = state.kind === "enabled" || state.kind === "skip";
55780
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55781
+ role: "gridcell",
55782
+ className: "min-w-0 border-b border-l border-border",
55783
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55784
+ type: "button",
55785
+ "data-r": r,
55786
+ "data-c": i + 1,
55787
+ "data-step": node.addonId,
55788
+ "data-col": agentColumnKey(a),
55789
+ tabIndex: tab.r === r && tab.c === i + 1 ? 0 : -1,
55790
+ "aria-disabled": !actionable,
55791
+ "aria-selected": isSelected,
55792
+ onClick: (e) => {
55793
+ if (actionable) onCellClick(node.addonId, a, e.detail === 0);
55794
+ },
55795
+ className: cellButtonClass(state, isSelected, cn("h-full w-full min-w-0 overflow-hidden px-3 py-2 outline-none", "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary", !actionable && "cursor-default hover:bg-transparent", a.disabled === true && "opacity-70")),
55796
+ children: renderCellContent(state)
55797
+ })
55798
+ }, agentColumnKey(a));
55718
55799
  })]
55719
- }, agentColumnKey(col)))),
55720
- rows.map(({ node, depth }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixRow, {
55721
- node,
55722
- depth,
55723
- agents,
55724
- gate: gateFor(node.addonId),
55725
- getCellState,
55726
- onCellClick,
55727
- selectedCell,
55728
- onSetGate,
55729
- gateBusy,
55730
- hideGate
55731
- }, node.addonId))
55732
- ]
55800
+ }, node.addonId))]
55801
+ }), reserveRightPx > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55802
+ "aria-hidden": true,
55803
+ style: { width: reserveRightPx },
55804
+ className: "shrink-0"
55805
+ })]
55733
55806
  })
55734
55807
  });
55735
- }
55736
- function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, hideGate }) {
55737
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepGateCell, {
55738
- node,
55739
- depth,
55740
- gate,
55741
- busy: gateBusy,
55742
- onSet: (next) => onSetGate(node.addonId, next),
55743
- hideGate
55744
- }), agents.map((a) => {
55745
- const state = getCellState(node.addonId, a);
55746
- const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
55747
- const actionable = state.kind === "enabled" || state.kind === "skip";
55748
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55749
- type: "button",
55750
- disabled: !actionable,
55751
- onClick: () => onCellClick(node.addonId, a),
55752
- className: cellButtonClass(state, isSelected, cn("min-w-0 overflow-hidden border-b border-l border-border px-3 py-2", !actionable && "cursor-default hover:bg-transparent")),
55753
- children: renderCellContent(state)
55754
- }, agentColumnKey(a));
55755
- })] });
55756
- }
55757
- function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange, hideGate }) {
55808
+ });
55809
+ var DeviceStepMatrix = (0, react$1.forwardRef)(function DeviceStepMatrix({ tree, agents, getCellState, onCellClick, selectedCell, fallbackNodeId, onFallbackNodeChange, onStepClick, stepBadge, reserveRightPx = 0, label = "Detection steps" }, ref) {
55758
55810
  const containerRef = (0, react$1.useRef)(null);
55759
55811
  const [width, setWidth] = (0, react$1.useState)(0);
55760
55812
  (0, react$1.useLayoutEffect)(() => {
@@ -55772,6 +55824,15 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
55772
55824
  if (nodes.length === 0) return null;
55773
55825
  return nodes.find((n) => n.agentNodeId === fallbackNodeId) ?? nodes[0] ?? null;
55774
55826
  }, [nodes, fallbackNodeId]);
55827
+ const selectedNodeId = selectedCell?.agentNodeId;
55828
+ (0, react$1.useEffect)(() => {
55829
+ if (single && selectedNodeId !== void 0 && selectedNodeId !== fallbackNodeId) onFallbackNodeChange(selectedNodeId);
55830
+ }, [
55831
+ single,
55832
+ selectedNodeId,
55833
+ fallbackNodeId,
55834
+ onFallbackNodeChange
55835
+ ]);
55775
55836
  const shownAgents = single && selectedNode ? selectedNode.columns : agents;
55776
55837
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55777
55838
  ref: containerRef,
@@ -55780,20 +55841,21 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
55780
55841
  nodes,
55781
55842
  selected: selectedNode.agentNodeId,
55782
55843
  onSelect: onFallbackNodeChange
55783
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid$1, {
55844
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
55845
+ ref,
55784
55846
  tree,
55785
55847
  agents: shownAgents,
55786
- gateFor,
55787
55848
  getCellState,
55788
55849
  onCellClick,
55850
+ onStepClick,
55851
+ stepBadge,
55789
55852
  selectedCell,
55790
- onSetGate,
55791
- gateBusy,
55792
55853
  single: single && selectedNode !== null,
55793
- hideGate
55854
+ reserveRightPx,
55855
+ label
55794
55856
  })]
55795
55857
  });
55796
- }
55858
+ });
55797
55859
  //#endregion
55798
55860
  //#region src/composites/discovery-panel.tsx
55799
55861
  function DiscoveryPanel({ deviceId, className = "", onOpenChild }) {
@@ -56732,43 +56794,13 @@ function PhaseIcon({ phase, ...props }) {
56732
56794
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { ...props });
56733
56795
  }
56734
56796
  //#endregion
56735
- //#region src/lib/validate-template.ts
56736
- function validateTemplate(steps, schema) {
56737
- const availableAddonIds = new Set(schema.slots.flatMap((s) => s.addons.map((a) => a.id)));
56738
- const warnings = [];
56739
- function validateStep(step) {
56740
- if (!availableAddonIds.has(step.addonId)) {
56741
- warnings.push(`Addon "${step.addonId}" is no longer available — step removed`);
56742
- return null;
56743
- }
56744
- const addon = schema.slots.flatMap((s) => s.addons).find((a) => a.id === step.addonId);
56745
- let modelId = step.modelId;
56746
- if (addon && !addon.models.some((m) => m.id === modelId)) {
56747
- const fallback = addon.defaultModelId;
56748
- warnings.push(`Model "${modelId}" not available for ${step.addonId} — using "${fallback}"`);
56749
- modelId = fallback;
56750
- }
56751
- const validChildren = step.children.map((c) => validateStep(c)).filter((c) => c !== null);
56752
- return {
56753
- ...step,
56754
- modelId,
56755
- children: validChildren
56756
- };
56757
- }
56758
- const validSteps = steps.map((s) => validateStep(s)).filter((s) => s !== null);
56759
- return {
56760
- valid: warnings.length === 0,
56761
- steps: validSteps,
56762
- warnings
56763
- };
56764
- }
56765
- //#endregion
56766
56797
  //#region src/composites/pipeline-builder.tsx
56767
56798
  /**
56768
- * PipelineBuilder — orchestrates the pipeline step tree with template management.
56799
+ * PipelineBuilder — orchestrates the pipeline step tree.
56769
56800
  *
56770
- * Renders: template bar (save/load/delete), step tree from schema with nested
56771
- * children, placeholder steps for disabled addons, all driven by API schema.
56801
+ * Renders the step tree from schema with nested children and placeholder steps
56802
+ * for disabled addons, all driven by API schema. There are no saved pipeline
56803
+ * templates (D759): the tree is the caller's state.
56772
56804
  */
56773
56805
  function buildSchemaMap(schema) {
56774
56806
  const map = /* @__PURE__ */ new Map();
@@ -56820,41 +56852,9 @@ function PlaceholderStep({ addon, onClick }) {
56820
56852
  })
56821
56853
  });
56822
56854
  }
56823
- function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateId, onSelectTemplate, onSaveTemplate, onUpdateTemplate, onDeleteTemplate, readOnly = false, excludeAddons = [], hideTemplates = false, showOnlyEnabled = false, showSlotLabels = false, toggleMode = "simple", overrides = {}, onOverrideChange, inheritedEnabledByAddon = {}, hideModelAndSettings = false, allowAutoModel = false }) {
56824
- const stepExtraProps = (addonId) => ({
56825
- toggleMode,
56826
- hideModelAndSettings,
56827
- allowAutoModel,
56828
- ...toggleMode === "threeState" ? {
56829
- overrideState: overrides[addonId] !== void 0 ? overrides[addonId] : null,
56830
- onOverrideChange: onOverrideChange ? (next) => onOverrideChange(addonId, next) : void 0,
56831
- inheritedEnabled: inheritedEnabledByAddon[addonId]
56832
- } : {}
56833
- });
56855
+ function PipelineBuilder({ schema, steps, onChange, readOnly = false, excludeAddons = [], showOnlyEnabled = false, showSlotLabels = false }) {
56834
56856
  const excluded = (0, react$1.useMemo)(() => new Set(excludeAddons), [excludeAddons]);
56835
56857
  const schemaMap = (0, react$1.useMemo)(() => buildSchemaMap(schema), [schema]);
56836
- const [warnings, setWarnings] = (0, react$1.useState)([]);
56837
- const dirty = selectedTemplateId ? JSON.stringify(steps) !== JSON.stringify(templates.find((t) => t.id === selectedTemplateId)?.steps) : false;
56838
- function handleSelectTemplate(e) {
56839
- const id = e.target.value || null;
56840
- if (id) {
56841
- const tpl = templates.find((t) => t.id === id);
56842
- if (tpl) setWarnings([...validateTemplate(tpl.steps, schema).warnings]);
56843
- } else setWarnings([]);
56844
- onSelectTemplate(id);
56845
- }
56846
- function handleSave() {
56847
- if (selectedTemplateId) onUpdateTemplate(selectedTemplateId, steps);
56848
- }
56849
- function handleSaveAs() {
56850
- const name = window.prompt("Template name:");
56851
- if (name?.trim()) onSaveTemplate(name.trim(), steps);
56852
- }
56853
- function handleDelete() {
56854
- if (!selectedTemplateId) return;
56855
- const tpl = templates.find((t) => t.id === selectedTemplateId);
56856
- if (tpl && window.confirm(`Delete template "${tpl.name}"?`)) onDeleteTemplate(selectedTemplateId);
56857
- }
56858
56858
  /**
56859
56859
  * When a step is updated, auto-enable the entire ancestor chain if a
56860
56860
  * child/grandchild was just enabled. This ensures enabling face-recognition
@@ -56937,8 +56937,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56937
56937
  allSchemas: schemaMap,
56938
56938
  onChange: handleStepChange,
56939
56939
  onDelete: readOnly ? void 0 : (id) => onChange(steps.filter((s) => s.addonId !== id)),
56940
- readOnly,
56941
- ...stepExtraProps(step.addonId)
56940
+ readOnly
56942
56941
  }), (step.children.length > 0 || childPlaceholders.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56943
56942
  className: "ml-4 pl-3 border-l border-border/60 space-y-1.5",
56944
56943
  children: [
@@ -56967,8 +56966,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56967
56966
  children: step.children.filter((c) => c.addonId !== id)
56968
56967
  });
56969
56968
  },
56970
- readOnly,
56971
- ...stepExtraProps(child.addonId)
56969
+ readOnly
56972
56970
  }), (child.children.length > 0 || childChildPlaceholders.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56973
56971
  className: "ml-4 pl-3 border-l border-border/40 space-y-1.5",
56974
56972
  children: [
@@ -56999,8 +56997,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56999
56997
  } : c)
57000
56998
  });
57001
56999
  },
57002
- readOnly,
57003
- ...stepExtraProps(grandchild.addonId)
57000
+ readOnly
57004
57001
  }, grandchild.addonId)),
57005
57002
  !readOnly && childChildPlaceholders.map((addon) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlaceholderStep, {
57006
57003
  addon,
@@ -57027,104 +57024,42 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
57027
57024
  })]
57028
57025
  }, step.addonId);
57029
57026
  }
57030
- const rootSlots = schema.slots.filter((s) => s.parentSlot === null).toSorted((a, b) => a.priority - b.priority);
57031
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57027
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57032
57028
  className: "space-y-4",
57033
- children: [
57034
- !hideTemplates && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57035
- className: "rounded-lg border border-border bg-surface p-3",
57036
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57037
- className: "flex items-center gap-2",
57038
- children: [
57039
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57040
- className: "relative flex-1 min-w-0",
57041
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
57042
- value: selectedTemplateId ?? "",
57043
- onChange: handleSelectTemplate,
57044
- className: "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary/50",
57045
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
57046
- value: "",
57047
- children: "No template"
57048
- }), templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
57049
- value: t.id,
57050
- children: t.name
57051
- }, t.id))]
57052
- })
57053
- }),
57054
- dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }),
57055
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57056
- onClick: handleSave,
57057
- disabled: !selectedTemplateId || readOnly,
57058
- title: "Save",
57059
- className: cn("p-2 rounded-lg border border-border transition-colors", selectedTemplateId && !readOnly ? "text-foreground-subtle hover:bg-surface-hover" : "text-foreground-subtle/30 cursor-not-allowed"),
57060
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Save, { className: "h-4 w-4" })
57061
- }),
57062
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57063
- onClick: handleSaveAs,
57064
- disabled: readOnly,
57065
- title: "Save As",
57066
- className: cn("p-2 rounded-lg border border-border transition-colors", !readOnly ? "text-foreground-subtle hover:bg-surface-hover" : "text-foreground-subtle/30 cursor-not-allowed"),
57067
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyPlus, { className: "h-4 w-4" })
57068
- }),
57069
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57070
- onClick: handleDelete,
57071
- disabled: !selectedTemplateId || readOnly,
57072
- title: "Delete",
57073
- className: cn("p-2 rounded-lg border border-border transition-colors", selectedTemplateId && !readOnly ? "text-foreground-subtle hover:text-danger" : "text-foreground-subtle/30 cursor-not-allowed"),
57074
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-4 w-4" })
57075
- })
57076
- ]
57077
- })
57078
- }),
57079
- warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57080
- className: "rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-amber-400 space-y-1",
57081
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57082
- className: "flex items-center justify-between",
57083
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57084
- className: "font-medium",
57085
- children: "Template loaded with warnings:"
57086
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57087
- onClick: () => setWarnings([]),
57088
- className: "text-amber-400/60 hover:text-amber-400",
57089
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3.5 w-3.5" })
57090
- })]
57091
- }), warnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: ["• ", w] }, i))]
57092
- }),
57093
- rootSlots.map((slot) => {
57094
- const allSlotSteps = steps.filter((s) => s.slot === slot.id && !excluded.has(s.addonId));
57095
- const slotSteps = showOnlyEnabled ? allSlotSteps.filter((s) => s.enabled) : allSlotSteps;
57096
- const missingRootAddons = showOnlyEnabled ? [] : slot.addons.filter((a) => !existingIds.has(a.id) && !excluded.has(a.id));
57097
- const groups = /* @__PURE__ */ new Map();
57098
- const ungrouped = [];
57099
- for (const step of slotSteps) if (step.group) {
57100
- if (!groups.has(step.group)) groups.set(step.group, []);
57101
- groups.get(step.group).push(step);
57102
- } else ungrouped.push(step);
57103
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57104
- className: "space-y-2",
57105
- children: [
57106
- showSlotLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57107
- className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/50",
57108
- children: ["Slot: ", slot.label]
57109
- }),
57110
- ungrouped.map((step) => renderStep(step)),
57111
- [...groups.entries()].map(([groupName, groupSteps]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57112
- className: "space-y-2 rounded-lg border border-border/40 p-2",
57113
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57114
- className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/50 px-1",
57115
- children: groupName
57116
- }), groupSteps.map((step) => renderStep(step))]
57117
- }, groupName)),
57118
- !readOnly && missingRootAddons.map((addon) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlaceholderStep, {
57119
- addon,
57120
- onClick: () => {
57121
- onChange([...steps, createDefaultStep(addon)]);
57122
- }
57123
- }, addon.id))
57124
- ]
57125
- }, slot.id);
57126
- })
57127
- ]
57029
+ children: schema.slots.filter((s) => s.parentSlot === null).toSorted((a, b) => a.priority - b.priority).map((slot) => {
57030
+ const allSlotSteps = steps.filter((s) => s.slot === slot.id && !excluded.has(s.addonId));
57031
+ const slotSteps = showOnlyEnabled ? allSlotSteps.filter((s) => s.enabled) : allSlotSteps;
57032
+ const missingRootAddons = showOnlyEnabled ? [] : slot.addons.filter((a) => !existingIds.has(a.id) && !excluded.has(a.id));
57033
+ const groups = /* @__PURE__ */ new Map();
57034
+ const ungrouped = [];
57035
+ for (const step of slotSteps) if (step.group) {
57036
+ if (!groups.has(step.group)) groups.set(step.group, []);
57037
+ groups.get(step.group).push(step);
57038
+ } else ungrouped.push(step);
57039
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57040
+ className: "space-y-2",
57041
+ children: [
57042
+ showSlotLabels && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57043
+ className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/50",
57044
+ children: ["Slot: ", slot.label]
57045
+ }),
57046
+ ungrouped.map((step) => renderStep(step)),
57047
+ [...groups.entries()].map(([groupName, groupSteps]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57048
+ className: "space-y-2 rounded-lg border border-border/40 p-2",
57049
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57050
+ className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/50 px-1",
57051
+ children: groupName
57052
+ }), groupSteps.map((step) => renderStep(step))]
57053
+ }, groupName)),
57054
+ !readOnly && missingRootAddons.map((addon) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlaceholderStep, {
57055
+ addon,
57056
+ onClick: () => {
57057
+ onChange([...steps, createDefaultStep(addon)]);
57058
+ }
57059
+ }, addon.id))
57060
+ ]
57061
+ }, slot.id);
57062
+ })
57128
57063
  });
57129
57064
  }
57130
57065
  //#endregion
@@ -57160,179 +57095,6 @@ function PipelineRuntimeSelector({ options, value, onChange }) {
57160
57095
  });
57161
57096
  }
57162
57097
  //#endregion
57163
- //#region src/composites/pipeline-tree-matrix.tsx
57164
- var STEP_COL_REM = 18;
57165
- function QuickToggle({ enabled, onChange, disabled }) {
57166
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57167
- type: "button",
57168
- role: "switch",
57169
- "aria-checked": enabled,
57170
- disabled,
57171
- onClick: (e) => {
57172
- e.stopPropagation();
57173
- onChange(!enabled);
57174
- },
57175
- className: cn("relative inline-flex h-3.5 w-7 shrink-0 items-center rounded-full transition-colors", enabled ? "bg-emerald-500" : "bg-foreground-subtle/30", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
57176
- title: enabled ? "Click to disable" : "Click to enable",
57177
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-3 w-3 rounded-full bg-white shadow transition-transform", enabled ? "translate-x-3.5" : "translate-x-0.5") })
57178
- });
57179
- }
57180
- function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell, toggleProps }) {
57181
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57182
- className: "sticky left-0 z-10 bg-surface px-3 py-2 border-b border-border text-xs flex items-start gap-2 min-w-0",
57183
- style: { paddingLeft: `${12 + depth * 14}px` },
57184
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), toggleProps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57185
- className: "pl-2 pt-0.5 shrink-0",
57186
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickToggle, {
57187
- enabled: toggleProps.enabled,
57188
- onChange: toggleProps.onChange,
57189
- disabled: toggleProps.disabled
57190
- })
57191
- })]
57192
- }), agents.map((a) => {
57193
- const state = getCellState(node.addonId, a);
57194
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57195
- type: "button",
57196
- onClick: () => onCellClick(node.addonId, a),
57197
- className: cellButtonClass(state, selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a), "min-w-0 overflow-hidden px-3 py-1.5 border-b border-l border-border"),
57198
- children: renderCellContent(state)
57199
- }, agentColumnKey(a));
57200
- })] });
57201
- }
57202
- /**
57203
- * Wide layout: horizontally self-scrolling matrix. The scroll lives INSIDE
57204
- * this `overflow-auto` box (sticky first column + sticky header row) so the
57205
- * page body never scrolls sideways. Column tracks are fixed px widths so the
57206
- * grid is static regardless of content.
57207
- */
57208
- function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57209
- const gridTemplate = `${STEP_COL_REM}rem repeat(${agents.length}, 13rem)`;
57210
- const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
57211
- const onlyAgent = agents[0];
57212
- const groups = groupAgentColumns(agents);
57213
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57214
- className: "overflow-auto border border-border rounded",
57215
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57216
- className: "grid w-max",
57217
- style: { gridTemplateColumns: gridTemplate },
57218
- children: [
57219
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57220
- className: "sticky top-0 left-0 z-20 bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle border-b border-border",
57221
- children: "Step"
57222
- }),
57223
- groups.map((group) => group.grouped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedAgentHeader, { group }, group.agentNodeId) : group.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57224
- className: "sticky top-0 z-10 min-w-0 bg-muted/60 px-3 py-2 border-b border-l border-border",
57225
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57226
- className: "text-xs font-semibold text-foreground truncate",
57227
- children: col.agentNodeId
57228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57229
- className: "text-[10px] text-foreground-subtle truncate",
57230
- children: col.engineLabel
57231
- })]
57232
- }, agentColumnKey(col)))),
57233
- rows.map(({ node, depth }) => {
57234
- const cellState = showToggle && onlyAgent !== void 0 ? getCellState(node.addonId, onlyAgent) : null;
57235
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridRow, {
57236
- node,
57237
- depth,
57238
- agents,
57239
- getCellState,
57240
- onCellClick,
57241
- selectedCell,
57242
- toggleProps: showToggle && cellState !== null ? {
57243
- enabled: cellState.kind === "enabled",
57244
- onChange: (next) => onToggleEnabled?.(node.addonId, next),
57245
- disabled: cellState.kind === "na"
57246
- } : void 0
57247
- }, node.addonId);
57248
- })
57249
- ]
57250
- })
57251
- });
57252
- }
57253
- /**
57254
- * Narrow layout: one card per agent column, each listing every step stacked
57255
- * vertically. Feature-parity with the matrix — same `onCellClick`, same
57256
- * `renderCellContent`, same single-agent `onToggleEnabled`.
57257
- */
57258
- function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57259
- const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
57260
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57261
- className: "space-y-3",
57262
- children: agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57263
- className: "border border-border rounded overflow-hidden",
57264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57265
- className: "bg-muted/60 px-3 py-2 border-b border-border",
57266
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57267
- className: "text-xs font-semibold text-foreground truncate",
57268
- children: a.deviceLabel ? `${a.agentNodeId} · ${a.deviceLabel}` : a.agentNodeId
57269
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57270
- className: "text-[10px] text-foreground-subtle truncate",
57271
- children: a.engineLabel
57272
- })]
57273
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: rows.map(({ node, depth }) => {
57274
- const state = getCellState(node.addonId, a);
57275
- const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
57276
- const toggleProps = showToggle && state !== null ? {
57277
- enabled: state.kind === "enabled",
57278
- onChange: (next) => onToggleEnabled?.(node.addonId, next),
57279
- disabled: state.kind === "na"
57280
- } : void 0;
57281
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
57282
- className: "border-b border-border last:border-b-0",
57283
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57284
- className: "flex items-center gap-2",
57285
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57286
- type: "button",
57287
- onClick: () => onCellClick(node.addonId, a),
57288
- className: cellButtonClass(state, isSelected, "flex flex-1 min-w-0 items-start justify-between gap-3 px-3 py-2"),
57289
- style: { paddingLeft: `${12 + depth * 14}px` },
57290
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57291
- className: "shrink-0 max-w-[45%] pt-0.5",
57292
- children: renderCellContent(state)
57293
- })]
57294
- }), toggleProps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57295
- className: "pr-3 shrink-0",
57296
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickToggle, {
57297
- enabled: toggleProps.enabled,
57298
- onChange: toggleProps.onChange,
57299
- disabled: toggleProps.disabled
57300
- })
57301
- })]
57302
- })
57303
- }, node.addonId);
57304
- }) })]
57305
- }, agentColumnKey(a)))
57306
- });
57307
- }
57308
- function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57309
- const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
57310
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57311
- className: "@container/matrix w-full",
57312
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57313
- className: "@2xl/matrix:hidden",
57314
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StackedCards, {
57315
- rows,
57316
- agents,
57317
- getCellState,
57318
- onCellClick,
57319
- selectedCell,
57320
- onToggleEnabled
57321
- })
57322
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57323
- className: "hidden @2xl/matrix:block",
57324
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
57325
- rows,
57326
- agents,
57327
- getCellState,
57328
- onCellClick,
57329
- selectedCell,
57330
- onToggleEnabled
57331
- })
57332
- })]
57333
- });
57334
- }
57335
- //#endregion
57336
57098
  //#region src/composites/provider-badge.tsx
57337
57099
  var providerConfig = {
57338
57100
  onvif: {
@@ -57776,89 +57538,6 @@ function validateSelector(selector) {
57776
57538
  }
57777
57539
  }
57778
57540
  //#endregion
57779
- //#region src/composites/slide-over-panel.tsx
57780
- /**
57781
- * SlideOverPanel — a detail surface anchored to context on desktop and to the
57782
- * viewport on mobile.
57783
- *
57784
- * - **Desktop (`md+`):** an in-container, NON-modal slide-over pinned to the
57785
- * right edge of the nearest positioned ancestor. The host reserves space with
57786
- * a matching `md:pr-*` so the panel sits beside its content (e.g. the
57787
- * multi-node pipeline matrix), not over it.
57788
- * - **Mobile (`< md`):** a MODAL bottom sheet pinned to the viewport
57789
- * (`position: fixed`). It never derives its size from the surrounding
57790
- * container, so it can't stretch across a horizontally-laid-out node matrix
57791
- * and overflow the page — the previous behaviour when it was a fixed-width
57792
- * `absolute` panel anchored to the wide matrix wrapper.
57793
- *
57794
- * The whole desktop⇄mobile transform is pure Tailwind responsive classes on one
57795
- * element — no layout-shift on hydration, no portal. JS is used only to lock
57796
- * body scroll behind the mobile sheet and to wire Escape-to-close.
57797
- */
57798
- function SlideOverPanel({ open, onClose, title, subtitle, footer, children, widthClass = "md:w-[360px]" }) {
57799
- const isMobile = useIsMobile();
57800
- (0, react$1.useEffect)(() => {
57801
- if (!open) return;
57802
- const onEsc = (e) => {
57803
- if (e.key === "Escape") onClose();
57804
- };
57805
- window.addEventListener("keydown", onEsc);
57806
- return () => window.removeEventListener("keydown", onEsc);
57807
- }, [open, onClose]);
57808
- (0, react$1.useEffect)(() => {
57809
- if (!open || !isMobile) return;
57810
- const previous = document.body.style.overflow;
57811
- document.body.style.overflow = "hidden";
57812
- return () => {
57813
- document.body.style.overflow = previous;
57814
- };
57815
- }, [open, isMobile]);
57816
- if (!open) return null;
57817
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57818
- className: "fixed inset-0 z-40 bg-black/50 md:hidden",
57819
- onClick: onClose,
57820
- "aria-hidden": "true"
57821
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
57822
- className: cn("fixed inset-x-0 bottom-0 z-50 flex flex-col", "max-h-[85dvh] rounded-t-2xl border-t border-border", "bg-surface shadow-2xl pb-[env(safe-area-inset-bottom)]", "md:absolute md:inset-x-auto md:top-0 md:right-0 md:bottom-0 md:z-20", "md:max-h-none md:rounded-none md:border-t-0 md:border-l md:pb-0", widthClass),
57823
- role: "dialog",
57824
- "aria-modal": isMobile ? "true" : "false",
57825
- children: [
57826
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57827
- className: "flex shrink-0 justify-center pt-2 pb-1 md:hidden",
57828
- "aria-hidden": "true",
57829
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-1 w-9 rounded-full bg-foreground-subtle/30" })
57830
- }),
57831
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
57832
- className: "flex shrink-0 items-start justify-between gap-2 px-4 py-3 border-b border-border",
57833
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57834
- className: "min-w-0",
57835
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57836
- className: "text-sm font-semibold text-foreground truncate",
57837
- children: title
57838
- }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57839
- className: "text-[11px] text-foreground-subtle mt-0.5 truncate",
57840
- children: subtitle
57841
- })]
57842
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57843
- type: "button",
57844
- onClick: onClose,
57845
- className: "p-1 rounded hover:bg-muted text-foreground-subtle hover:text-foreground",
57846
- "aria-label": "Close panel",
57847
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "w-4 h-4" })
57848
- })]
57849
- }),
57850
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57851
- className: "flex-1 min-h-0 overflow-y-auto overscroll-contain px-4 py-3",
57852
- children
57853
- }),
57854
- footer !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
57855
- className: "flex shrink-0 items-center gap-2 justify-end px-4 py-3 border-t border-border",
57856
- children: footer
57857
- })
57858
- ]
57859
- })] });
57860
- }
57861
- //#endregion
57862
57541
  //#region src/composites/snapshot-button.tsx
57863
57542
  /**
57864
57543
  * SnapshotButton — operator-facing manual snapshot trigger.
@@ -58044,51 +57723,501 @@ function StepTimings({ timings, totalMs, className }) {
58044
57723
  });
58045
57724
  }
58046
57725
  //#endregion
58047
- //#region src/composites/step-tree-master.tsx
58048
- var DOT_COLOR = {
58049
- inherit: "bg-muted",
58050
- on: "bg-emerald-500",
58051
- off: "bg-orange-500",
58052
- na: "bg-gray-300 opacity-50"
58053
- };
58054
- function TreeRow({ node, depth, selectedAddonId, onSelect, agentDots }) {
58055
- const isSelected = node.addonId === selectedAddonId;
58056
- const dots = agentDots?.[node.addonId] ?? [];
58057
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57726
+ //#region src/composites/step-inspector/accelerator-tabs.tsx
57727
+ function AcceleratorTabs({ items, selectedId, onSelect }) {
57728
+ const onKeyDown = (e, index) => {
57729
+ const delta = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
57730
+ if (delta === 0) return;
57731
+ e.preventDefault();
57732
+ const next = items[(index + delta + items.length) % items.length];
57733
+ if (next !== void 0) onSelect(next.id);
57734
+ };
57735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57736
+ role: "tablist",
57737
+ "aria-label": "Accelerator",
57738
+ className: "-mx-1 flex min-w-0 flex-nowrap gap-1 overflow-x-auto px-1 pb-0.5",
57739
+ children: items.map((item, index) => {
57740
+ const selected = item.id === selectedId;
57741
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57742
+ type: "button",
57743
+ role: "tab",
57744
+ "aria-selected": selected,
57745
+ "aria-label": item.marked ? `${item.label} (camera override)` : item.label,
57746
+ tabIndex: selected ? 0 : -1,
57747
+ onClick: () => onSelect(item.id),
57748
+ onKeyDown: (e) => onKeyDown(e, index),
57749
+ className: cn("inline-flex h-9 shrink-0 items-center gap-1 whitespace-nowrap rounded-md border px-2.5 text-[11px] font-medium transition-colors md:h-7", selected ? "border-primary/50 bg-primary/15 text-primary" : "border-border text-foreground-subtle hover:bg-surface-hover hover:text-foreground"),
57750
+ children: [item.label, item.marked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57751
+ className: "h-1.5 w-1.5 rounded-full bg-amber-500",
57752
+ "aria-hidden": true
57753
+ })]
57754
+ }, item.id);
57755
+ })
57756
+ });
57757
+ }
57758
+ //#endregion
57759
+ //#region src/composites/step-inspector/gate-control.tsx
57760
+ /**
57761
+ * GateControl — whether ONE camera runs a step: Auto (the catalog default) /
57762
+ * On / Off. A per-camera, node-independent switch, written at once (it is not
57763
+ * part of the inspector's draft). `package-detection` is driven by package
57764
+ * zones and shows a badge instead.
57765
+ *
57766
+ * Segments are 36 px tall on a phone and 32 px from `md` up — the matrix this
57767
+ * replaced drew 26 px ones.
57768
+ */
57769
+ function activeChoice(gate) {
57770
+ if (!gate.explicit) return "auto";
57771
+ return gate.effectiveOn ? "on" : "off";
57772
+ }
57773
+ function GateControl({ gate, busy = false, onSet }) {
57774
+ if (gate.kind === "zone-driven") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57775
+ className: "inline-flex items-center rounded-md border border-violet-500/40 bg-violet-500/10 px-2 py-1 text-[11px] font-medium text-violet-400",
57776
+ title: "Runs where this camera has package zones — set on the Zones tab",
57777
+ children: "Zones"
57778
+ });
57779
+ const active = activeChoice(gate);
57780
+ const segment = (choice, label, value, on) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58058
57781
  type: "button",
58059
- onClick: () => onSelect(node.addonId),
58060
- className: cn("w-full flex items-center justify-between gap-2 text-left px-3 py-1.5 text-xs", "border-l-2", isSelected ? "bg-accent border-primary text-foreground font-medium" : "border-transparent hover:bg-muted/40 text-foreground"),
58061
- style: { paddingLeft: `${12 + depth * 14}px` },
58062
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58063
- className: "truncate",
58064
- children: node.addonName
58065
- }), dots.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58066
- className: "flex items-center gap-1 shrink-0",
58067
- children: dots.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58068
- title: `${d.agentNodeId}: ${d.state}`,
58069
- className: cn("w-2 h-2 rounded-full", DOT_COLOR[d.state])
58070
- }, d.agentNodeId))
57782
+ role: "radio",
57783
+ "aria-checked": active === choice,
57784
+ disabled: busy,
57785
+ onClick: () => onSet(value),
57786
+ className: cn("h-9 min-w-12 px-3 text-xs font-medium transition-colors md:h-8", active === choice ? on : "text-foreground-subtle hover:bg-muted/60", busy && "cursor-not-allowed opacity-50"),
57787
+ children: label
57788
+ });
57789
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57790
+ className: "flex items-center gap-2",
57791
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57792
+ role: "radiogroup",
57793
+ "aria-label": "Runs on this camera",
57794
+ title: `Catalog default: ${gate.defaultOn ? "on" : "off"}`,
57795
+ className: "inline-flex overflow-hidden rounded-md border border-border",
57796
+ children: [
57797
+ segment("auto", "Auto", null, "bg-primary/15 text-primary"),
57798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57799
+ className: "w-px self-stretch bg-border",
57800
+ "aria-hidden": true
57801
+ }),
57802
+ segment("on", "On", true, "bg-emerald-500 text-white"),
57803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57804
+ className: "w-px self-stretch bg-border",
57805
+ "aria-hidden": true
57806
+ }),
57807
+ segment("off", "Off", false, "bg-foreground-subtle/70 text-white")
57808
+ ]
57809
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57810
+ className: "hidden text-[10px] text-foreground-subtle @3xl/inspector:inline",
57811
+ children: ["default: ", gate.defaultOn ? "on" : "off"]
58071
57812
  })]
58072
- }), node.children.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
58073
- node: child,
58074
- depth: depth + 1,
58075
- selectedAddonId,
58076
- onSelect,
58077
- agentDots
58078
- }, child.addonId))] });
57813
+ });
57814
+ }
57815
+ /** The gate as a short list-row badge. */
57816
+ function GateBadge({ gate }) {
57817
+ if (gate.kind === "zone-driven") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57818
+ className: "shrink-0 text-[9px] font-semibold uppercase text-violet-400",
57819
+ children: "zones"
57820
+ });
57821
+ const on = gate.effectiveOn;
57822
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57823
+ className: cn("shrink-0 rounded px-1.5 py-px text-[9px] font-semibold uppercase", on ? "bg-emerald-500/15 text-emerald-500" : "bg-muted text-foreground-subtle", gate.explicit && "ring-1 ring-inset ring-current"),
57824
+ title: gate.explicit ? "Set for this camera" : "Catalog default",
57825
+ children: on ? "on" : "off"
57826
+ });
58079
57827
  }
58080
- function StepTreeMaster({ tree, selectedAddonId, onSelect, agentDots, className }) {
57828
+ //#endregion
57829
+ //#region src/composites/step-inspector/segmented-choice.tsx
57830
+ /**
57831
+ * SegmentedChoice — a small, labelled single-choice group (a radiogroup of
57832
+ * buttons): which layer the inspector's settings edit ("All accelerators" /
57833
+ * "Only hub · NPU"). 36 px tall on a phone, 28 px from `md` up.
57834
+ */
57835
+ function SegmentedChoice({ label, options, value, onChange }) {
57836
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57837
+ role: "radiogroup",
57838
+ "aria-label": label,
57839
+ className: "flex flex-wrap items-center gap-1",
57840
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57841
+ type: "button",
57842
+ role: "radio",
57843
+ "aria-checked": value === option.value,
57844
+ onClick: () => onChange(option.value),
57845
+ className: cn("h-9 rounded-md border px-2.5 text-[11px] font-medium transition-colors md:h-7", value === option.value ? "border-primary/50 bg-primary/15 text-primary" : "border-border text-foreground-subtle hover:bg-surface-hover hover:text-foreground"),
57846
+ children: option.label
57847
+ }, option.value))
57848
+ });
57849
+ }
57850
+ //#endregion
57851
+ //#region src/composites/step-inspector/step-detail-dock.tsx
57852
+ /**
57853
+ * StepDetailDock — where a step's settings open from the step matrix
57854
+ * (D757): docked to the page's scroll container, never inside the scrolling
57855
+ * content.
57856
+ *
57857
+ * Desktop: a side panel `position: fixed` over the RIGHT edge of the nearest
57858
+ * scroll container (its top, bottom and right are measured from that box), so
57859
+ * it stays where it is while the matrix scrolls under it, and its header and
57860
+ * save bar are always on screen. It overlays — it never reserves width, so the
57861
+ * matrix does not reflow (the slide-over before D756 reserved 360 px, the
57862
+ * matrix re-measured into a single-node layout, and the clicked cell slid
57863
+ * under the panel). The matrix uses {@link STEP_DOCK_WIDTH_PX} to scroll the
57864
+ * selected column clear of it.
57865
+ *
57866
+ * Phone: a full-screen `Dialog` sheet.
57867
+ */
57868
+ /** Width of the desktop dock — what the matrix keeps clear of it. */
57869
+ var STEP_DOCK_WIDTH_PX = 432;
57870
+ /** The dock's viewport box, from its scroll container's box. */
57871
+ function dockBox(input) {
57872
+ const right = Math.max(0, input.viewportWidth - input.scroller.right);
57873
+ const width = Math.min(input.widthPx, input.scroller.right);
57874
+ return {
57875
+ top: Math.max(0, input.scroller.top),
57876
+ height: Math.max(0, input.scroller.bottom - Math.max(0, input.scroller.top)),
57877
+ right,
57878
+ width
57879
+ };
57880
+ }
57881
+ function scrollParentOf(el) {
57882
+ for (let p = el?.parentElement ?? null; p !== null; p = p.parentElement) {
57883
+ const { overflowY } = getComputedStyle(p);
57884
+ if (overflowY === "auto" || overflowY === "scroll") return p;
57885
+ }
57886
+ return null;
57887
+ }
57888
+ function useDockBox(anchor, enabled) {
57889
+ const [box, setBox] = (0, react$1.useState)(null);
57890
+ (0, react$1.useLayoutEffect)(() => {
57891
+ if (!enabled) return;
57892
+ const scroller = scrollParentOf(anchor.current);
57893
+ const measure = () => {
57894
+ setBox(dockBox({
57895
+ scroller: scroller?.getBoundingClientRect() ?? {
57896
+ top: 0,
57897
+ bottom: window.innerHeight,
57898
+ right: window.innerWidth
57899
+ },
57900
+ viewportWidth: window.innerWidth,
57901
+ widthPx: 432
57902
+ }));
57903
+ };
57904
+ measure();
57905
+ const RO = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
57906
+ if (scroller) RO?.observe(scroller);
57907
+ window.addEventListener("resize", measure);
57908
+ return () => {
57909
+ RO?.disconnect();
57910
+ window.removeEventListener("resize", measure);
57911
+ };
57912
+ }, [anchor, enabled]);
57913
+ return box;
57914
+ }
57915
+ function StepDetailDock({ open, anchor, onClose, label, children }) {
57916
+ const isMobile = useIsMobile();
57917
+ const box = useDockBox(anchor, open && !isMobile);
57918
+ if (isMobile) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
57919
+ open,
57920
+ onOpenChange: (next) => !next && onClose(),
57921
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogContent, {
57922
+ placement: "sheet-on-mobile",
57923
+ scroll: "body",
57924
+ "aria-label": label,
57925
+ className: "h-[100dvh] max-h-none w-full max-w-full rounded-none border-0 p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]",
57926
+ children: open && children
57927
+ })
57928
+ });
57929
+ if (!open) return null;
57930
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
57931
+ "aria-label": label,
57932
+ "data-step-dock": true,
57933
+ className: "fixed z-30 flex flex-col border-l border-border bg-surface shadow-2xl",
57934
+ style: box !== null ? {
57935
+ top: box.top,
57936
+ height: box.height,
57937
+ right: box.right,
57938
+ width: box.width
57939
+ } : {
57940
+ top: 0,
57941
+ bottom: 0,
57942
+ right: 0,
57943
+ width: 432
57944
+ },
57945
+ children
57946
+ });
57947
+ }
57948
+ /** "‹ Steps" — the phone sheet's way back to the matrix. */
57949
+ function BackToStepsButton({ onClick }) {
57950
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57951
+ type: "button",
57952
+ onClick,
57953
+ className: "inline-flex h-9 items-center gap-0.5 rounded-md border border-border px-2 text-xs text-foreground hover:bg-surface-hover",
57954
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronLeft, {
57955
+ className: "h-3.5 w-3.5",
57956
+ "aria-hidden": true
57957
+ }), "Steps"]
57958
+ });
57959
+ }
57960
+ //#endregion
57961
+ //#region src/composites/step-inspector/step-inspector-panel.tsx
57962
+ /**
57963
+ * StepInspectorPanel — the detail pane of the step inspector: a pinned header
57964
+ * (the step, its gate, the accelerator tabs), ONE scrolling body, and a pinned
57965
+ * save bar. Nothing in it depends on the page's scroll position — the slide-over
57966
+ * it replaced was anchored to the matrix block and scrolled away with it.
57967
+ */
57968
+ function SaveBar({ dirtyCount, saving, status, error, onSave, onDiscard }) {
57969
+ const dirty = dirtyCount > 0;
57970
+ const message = saving ? "Saving…" : dirty ? `${dirtyCount} unsaved change${dirtyCount === 1 ? "" : "s"}` : status === "saved" ? "Saved" : status === "error" ? error ?? "Save failed" : "No unsaved changes";
57971
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
57972
+ className: "flex shrink-0 items-center gap-2 border-t border-border bg-surface px-4 py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] md:pb-2",
57973
+ children: [
57974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57975
+ role: "status",
57976
+ className: cn("min-w-0 flex-1 truncate text-[11px]", status === "error" && !dirty ? "text-danger" : dirty ? "text-amber-500" : "text-foreground-subtle"),
57977
+ children: message
57978
+ }),
57979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
57980
+ variant: "outline",
57981
+ size: "md",
57982
+ disabled: !dirty || saving,
57983
+ onClick: onDiscard,
57984
+ children: "Discard"
57985
+ }),
57986
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
57987
+ variant: "primary",
57988
+ size: "md",
57989
+ disabled: !dirty || saving,
57990
+ onClick: onSave,
57991
+ children: "Save"
57992
+ })
57993
+ ]
57994
+ });
57995
+ }
57996
+ var StepInspectorPanel = (0, react$1.forwardRef)(function StepInspectorPanel({ title, subtitle, headerActions, headerExtra, leading, saveBar, onEscape, children, className }, headingRef) {
57997
+ const headingId = (0, react$1.useId)();
57998
+ const onKeyDown = (e) => {
57999
+ if (e.key !== "Escape" || onEscape === void 0) return;
58000
+ e.stopPropagation();
58001
+ onEscape();
58002
+ };
58003
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
58004
+ "aria-labelledby": headingId,
58005
+ onKeyDown,
58006
+ className: cn("flex min-h-0 flex-1 flex-col overflow-hidden bg-surface", className),
58007
+ children: [
58008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
58009
+ className: "shrink-0 space-y-2 border-b border-border px-4 py-2.5",
58010
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58011
+ className: "flex flex-wrap items-start gap-x-3 gap-y-2",
58012
+ children: [
58013
+ leading,
58014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58015
+ className: "min-w-[9rem] flex-1",
58016
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
58017
+ id: headingId,
58018
+ ref: headingRef,
58019
+ tabIndex: -1,
58020
+ className: "truncate text-sm font-semibold text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary rounded",
58021
+ children: title
58022
+ }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58023
+ className: "mt-0.5 text-[11px] text-foreground-subtle",
58024
+ children: subtitle
58025
+ })]
58026
+ }),
58027
+ headerActions
58028
+ ]
58029
+ }), headerExtra]
58030
+ }),
58031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58032
+ "data-inspector-body": true,
58033
+ className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3",
58034
+ children
58035
+ }),
58036
+ saveBar !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, { ...saveBar })
58037
+ ]
58038
+ });
58039
+ });
58040
+ //#endregion
58041
+ //#region src/composites/step-inspector/step-model-row.tsx
58042
+ var SOURCE_TONE = {
58043
+ camera: "border-primary/40 bg-primary/10 text-primary",
58044
+ node: "border-border bg-muted/40 text-foreground",
58045
+ default: "border-border/60 text-foreground-subtle",
58046
+ cluster: "border-primary/40 bg-primary/5 text-primary"
58047
+ };
58048
+ function StepModelRow({ label, format, modelId, source, sourceLabel, models, defaultModelId, expanded, onToggle, onChangeModel, onReset, resetLabel = "Reset", note, extra, changed = false, disabled = false }) {
58049
+ const pickable = models.length > 0 && note === void 0;
58050
+ const canExpand = pickable || extra !== void 0;
58051
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58052
+ className: cn("rounded-md border border-border", expanded && "bg-muted/10"),
58053
+ children: [
58054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58055
+ className: cn(LIST_ROW, "space-y-1"),
58056
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58057
+ className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs",
58058
+ children: [
58059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58060
+ className: "font-medium text-foreground",
58061
+ children: label
58062
+ }),
58063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58064
+ className: "text-[10px] text-foreground-subtle",
58065
+ children: format
58066
+ }),
58067
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58068
+ className: cn("ml-auto rounded border px-1.5 py-px text-[10px] font-medium", SOURCE_TONE[source]),
58069
+ children: sourceLabel
58070
+ })
58071
+ ]
58072
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58073
+ className: "flex items-center gap-2",
58074
+ children: [
58075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
58076
+ className: "min-w-0 flex-1 truncate font-mono text-xs text-foreground",
58077
+ children: [modelId ?? "—", changed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58078
+ className: "ml-1 text-amber-500",
58079
+ children: "•"
58080
+ })]
58081
+ }),
58082
+ onReset !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58083
+ type: "button",
58084
+ className: "inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40",
58085
+ disabled,
58086
+ onClick: onReset,
58087
+ children: resetLabel
58088
+ }),
58089
+ canExpand && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58090
+ type: "button",
58091
+ className: cn("inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40", "min-h-9 md:min-h-0"),
58092
+ "aria-expanded": expanded,
58093
+ "aria-label": `Change model for ${label}`,
58094
+ disabled,
58095
+ onClick: onToggle,
58096
+ children: expanded ? "Done" : pickable ? "Change" : "Route"
58097
+ })
58098
+ ]
58099
+ })]
58100
+ }),
58101
+ note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58102
+ className: "border-t border-border/60 px-3 py-1.5 text-[10px] leading-snug text-foreground-subtle",
58103
+ children: note
58104
+ }),
58105
+ expanded && canExpand && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58106
+ className: "space-y-3 border-t border-border/60 px-3 py-3",
58107
+ children: [pickable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
58108
+ models,
58109
+ value: modelId ?? "",
58110
+ defaultModelId,
58111
+ disabled,
58112
+ onChange: onChangeModel
58113
+ }), extra]
58114
+ })
58115
+ ]
58116
+ });
58117
+ }
58118
+ //#endregion
58119
+ //#region src/composites/step-inspector/step-model-list.tsx
58120
+ /**
58121
+ * StepModelList — the model section of the step inspector.
58122
+ *
58123
+ * While nothing is pinned, edited or worth a note, the accelerators are
58124
+ * summarised by model — one line per distinct model, naming the accelerators
58125
+ * that run it ("yolov9m-320-int8 · hub iGPU, hub NPU, little-unraid iGPU") —
58126
+ * with "Per accelerator" to open one `StepModelRow` each. Four near-identical
58127
+ * rows pushed the settings, the thing most edits are about, below the fold.
58128
+ * A pin, a pending change or a note (no build here, root-supplied) shows every
58129
+ * row, so nothing that differs from the default is ever folded away.
58130
+ */
58131
+ /** True when the rows can be summarised: nothing pinned here, edited, or noted. */
58132
+ function modelRowsAreFoldable(rows) {
58133
+ return rows.length >= 2 && rows.every((r) => r.note === void 0 && r.onReset === void 0 && r.changed !== true && !r.expanded);
58134
+ }
58135
+ function groupByModel(rows) {
58136
+ const groups = [];
58137
+ for (const row of rows) {
58138
+ const modelId = row.modelId ?? "—";
58139
+ const index = groups.findIndex((g) => g.modelId === modelId && g.sourceLabel === row.sourceLabel);
58140
+ const existing = groups[index];
58141
+ if (existing !== void 0) groups[index] = {
58142
+ ...existing,
58143
+ labels: [...existing.labels, row.label]
58144
+ };
58145
+ else groups.push({
58146
+ modelId,
58147
+ labels: [row.label],
58148
+ sourceLabel: row.sourceLabel
58149
+ });
58150
+ }
58151
+ return groups;
58152
+ }
58153
+ function StepModelList({ rows }) {
58154
+ const [open, setOpen] = (0, react$1.useState)(false);
58155
+ if (!open && modelRowsAreFoldable(rows)) {
58156
+ const groups = groupByModel(rows);
58157
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58158
+ className: cn("rounded-md border border-border", LIST_ROW),
58159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
58160
+ className: "space-y-1",
58161
+ children: groups.map((g) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
58162
+ className: "flex flex-wrap items-baseline gap-x-2 text-xs",
58163
+ children: [
58164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58165
+ className: "font-mono text-foreground",
58166
+ children: g.modelId
58167
+ }),
58168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58169
+ className: "min-w-0 flex-1 text-[11px] text-foreground-subtle",
58170
+ children: g.labels.length === rows.length ? `all ${rows.length} accelerators` : g.labels.join(", ")
58171
+ }),
58172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58173
+ className: "rounded border border-border px-1.5 py-px text-[10px] font-medium text-foreground-subtle",
58174
+ children: g.sourceLabel
58175
+ })
58176
+ ]
58177
+ }, `${g.modelId}|${g.sourceLabel}`))
58178
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58179
+ className: "mt-1.5 flex justify-end",
58180
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58181
+ type: "button",
58182
+ className: cn(BTN_COMPACT, "min-h-9 md:min-h-0"),
58183
+ "aria-expanded": false,
58184
+ onClick: () => setOpen(true),
58185
+ children: "Per accelerator"
58186
+ })
58187
+ })]
58188
+ });
58189
+ }
58081
58190
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58082
- className: cn("flex flex-col py-2", className),
58083
- children: tree.map((node) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
58084
- node,
58085
- depth: 0,
58086
- selectedAddonId,
58087
- onSelect,
58088
- agentDots
58089
- }, node.addonId))
58191
+ className: "space-y-2",
58192
+ children: rows.map(({ id, ...row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepModelRow, { ...row }, id))
58090
58193
  });
58091
58194
  }
58195
+ //#endregion
58196
+ //#region src/composites/step-inspector/step-search.ts
58197
+ function matches(text, tokens) {
58198
+ const lower = text.toLowerCase();
58199
+ return tokens.every((t) => lower.includes(t));
58200
+ }
58201
+ /** Steps matching `query` (all tokens in the name, or all in one field label). Empty query = all. */
58202
+ function searchSteps(rows, query) {
58203
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
58204
+ if (tokens.length === 0) return rows.map((row) => ({ addonId: row.addonId }));
58205
+ const hits = [];
58206
+ for (const row of rows) {
58207
+ if (matches(row.name, tokens)) {
58208
+ hits.push({ addonId: row.addonId });
58209
+ continue;
58210
+ }
58211
+ const field = row.fieldLabels.find((l) => matches(l, tokens));
58212
+ if (field !== void 0) hits.push({
58213
+ addonId: row.addonId,
58214
+ matchedField: field
58215
+ });
58216
+ }
58217
+ return hits;
58218
+ }
58219
+ //#endregion
58220
+ //#region src/composites/step-tree.ts
58092
58221
  /**
58093
58222
  * Helper: build a StepTreeNode[] from a `PipelineSchema` by chaining on
58094
58223
  * `inputClasses` ⇔ `outputClasses` compatibility. Mirrors the orchestrator
@@ -61033,6 +61162,7 @@ var SceneMonitorEditor = (0, react$1.lazy)(() => Promise.resolve().then(() => re
61033
61162
  /** Lazy-wrapped `PrivacyMaskSettings` — code-split off the main bundle. */
61034
61163
  var PrivacyMaskSettings = (0, react$1.lazy)(() => Promise.resolve().then(() => require("./PrivacyMaskSettings-BqjXsXZR.cjs")).then((m) => ({ default: m.PrivacyMaskSettings })));
61035
61164
  //#endregion
61165
+ exports.AcceleratorTabs = AcceleratorTabs;
61036
61166
  exports.AddonGlobalSettingsForm = AddonGlobalSettingsForm;
61037
61167
  exports.AdminLinkQuickAction = AdminLinkQuickAction;
61038
61168
  exports.AgentStepEditor = AgentStepEditor;
@@ -61050,6 +61180,7 @@ exports.BTN_COMPACT = BTN_COMPACT;
61050
61180
  exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
61051
61181
  exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
61052
61182
  exports.BTN_COMPACT_WARNING = BTN_COMPACT_WARNING;
61183
+ exports.BackToStepsButton = BackToStepsButton;
61053
61184
  exports.Badge = Badge;
61054
61185
  exports.BatteryBadge = BatteryBadge;
61055
61186
  exports.BottomSheet = BottomSheet;
@@ -61171,6 +61302,8 @@ exports.FormField = FormField$1;
61171
61302
  exports.GRID_GAP = GRID_GAP;
61172
61303
  exports.GRID_PAIRED = GRID_PAIRED;
61173
61304
  exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
61305
+ exports.GateBadge = GateBadge;
61306
+ exports.GateControl = GateControl;
61174
61307
  exports.Grid2x2 = Grid2x2;
61175
61308
  exports.GripTrack = GripTrack;
61176
61309
  exports.GroupedModelSelector = GroupedModelSelector;
@@ -61198,6 +61331,7 @@ exports.LIST_ROW = LIST_ROW;
61198
61331
  exports.Label = Label;
61199
61332
  exports.LawnMowerHeroCard = LawnMowerHeroCard;
61200
61333
  exports.LawnMowerInlineControl = LawnMowerInlineControl;
61334
+ exports.LayeredSettingsForm = LayeredSettingsForm;
61201
61335
  exports.LightHeroCard = LightHeroCard;
61202
61336
  exports.LightInlineControl = LightInlineControl;
61203
61337
  exports.LockHeroCard = LockHeroCard;
@@ -61245,7 +61379,6 @@ exports.PhaseIcon = PhaseIcon;
61245
61379
  exports.PipelineBuilder = PipelineBuilder;
61246
61380
  exports.PipelineRuntimeSelector = PipelineRuntimeSelector;
61247
61381
  exports.PipelineStep = PipelineStep;
61248
- exports.PipelineTreeMatrix = PipelineTreeMatrix;
61249
61382
  exports.PlaceholderChipField = PlaceholderChipField;
61250
61383
  exports.PlayerOverlaysProvider = PlayerOverlaysProvider;
61251
61384
  exports.Popover = Popover;
@@ -61268,6 +61401,8 @@ exports.RecordingPanel = RecordingPanel;
61268
61401
  exports.ResponseLog = ResponseLog;
61269
61402
  exports.RoleDisplayDefaultsFromServer = RoleDisplayDefaultsFromServer;
61270
61403
  exports.RoleDisplayDefaultsProvider = RoleDisplayDefaultsProvider;
61404
+ exports.RootModelSourceControl = RootModelSourceControl;
61405
+ exports.RootSuppliedModelNote = RootSuppliedModelNote;
61271
61406
  exports.RotateCcw = RotateCcw;
61272
61407
  exports.RunReport = RunReport;
61273
61408
  exports.SCENE_CAPTURE_VARIANTS = SCENE_CAPTURE_VARIANTS;
@@ -61285,12 +61420,14 @@ exports.SPLIT_PANEL_OUTER = SPLIT_PANEL_OUTER;
61285
61420
  exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
61286
61421
  exports.STACK_GAP = STACK_GAP;
61287
61422
  exports.STATE_COLOR = STATE_COLOR;
61423
+ exports.STEP_DOCK_WIDTH_PX = STEP_DOCK_WIDTH_PX;
61288
61424
  exports.SceneMonitorEditor = SceneMonitorEditor;
61289
61425
  exports.ScopePicker = ScopePicker;
61290
61426
  exports.ScriptHeroCard = ScriptHeroCard;
61291
61427
  exports.ScriptInlineControl = ScriptInlineControl;
61292
61428
  exports.ScrollArea = ScrollArea;
61293
61429
  exports.SectionCard = SectionCard;
61430
+ exports.SegmentedChoice = SegmentedChoice;
61294
61431
  exports.Select = Select;
61295
61432
  exports.SelectedChip = SelectedChip;
61296
61433
  exports.SemanticBadge = SemanticBadge;
@@ -61302,7 +61439,6 @@ exports.SettingRow = SettingRow;
61302
61439
  exports.Sidebar = Sidebar;
61303
61440
  exports.SidebarItem = SidebarItem;
61304
61441
  exports.Skeleton = Skeleton;
61305
- exports.SlideOverPanel = SlideOverPanel;
61306
61442
  exports.SlideToggle = SlideToggle;
61307
61443
  exports.SnapshotButton = SnapshotButton;
61308
61444
  exports.SortHeaderButton = SortHeaderButton;
@@ -61310,8 +61446,11 @@ exports.Square = Square;
61310
61446
  exports.StatCard = StatCard;
61311
61447
  exports.StateValuesStream = StateValuesStream;
61312
61448
  exports.StatusBadge = StatusBadge;
61449
+ exports.StepDetailDock = StepDetailDock;
61450
+ exports.StepInspectorPanel = StepInspectorPanel;
61451
+ exports.StepModelList = StepModelList;
61452
+ exports.StepModelRow = StepModelRow;
61313
61453
  exports.StepTimings = StepTimings;
61314
- exports.StepTreeMaster = StepTreeMaster;
61315
61454
  exports.Stepper = Stepper;
61316
61455
  exports.StreamBrokerSelector = StreamBrokerSelector;
61317
61456
  exports.StreamDebugOverlay = StreamDebugOverlay;
@@ -61358,6 +61497,8 @@ exports.WidgetPanel = WidgetPanel;
61358
61497
  exports.WidgetRegistryProvider = WidgetRegistryProvider;
61359
61498
  exports.WidgetSlot = WidgetSlot;
61360
61499
  exports.ZoneEditingProvider = ZoneEditingProvider;
61500
+ exports.acceleratorLabel = acceleratorLabel;
61501
+ exports.acceleratorShortLabel = acceleratorShortLabel;
61361
61502
  exports.agentColumnKey = agentColumnKey;
61362
61503
  exports.allDeviceTypeFilterOptions = allDeviceTypeFilterOptions;
61363
61504
  exports.applyFleetQueryDefaults = applyFleetQueryDefaults;
@@ -61365,6 +61506,7 @@ exports.applyPickerFilters = applyPickerFilters;
61365
61506
  exports.ariaSortForColumn = ariaSortForColumn;
61366
61507
  exports.badgeFor = badgeFor;
61367
61508
  exports.badgeFor$1 = badgeFor;
61509
+ exports.bindsToModel = bindsToModel;
61368
61510
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
61369
61511
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
61370
61512
  exports.catalogHasDraft = catalogHasDraft;
@@ -61389,6 +61531,7 @@ exports.createTheme = require_theme_index.createTheme$1;
61389
61531
  exports.cursorFractionFor = cursorFractionFor;
61390
61532
  exports.darkColors = require_theme_index.darkColors$1;
61391
61533
  exports.dataTableFacets = dataTableFacets;
61534
+ exports.defaultOfField = defaultOfField;
61392
61535
  exports.defaultTheme = require_theme_index.defaultTheme$1;
61393
61536
  exports.deriveDeviceKind = deriveDeviceKind;
61394
61537
  exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
@@ -61403,6 +61546,7 @@ exports.deviceTypeLabel = deviceTypeLabel;
61403
61546
  exports.deviceTypeMeta = deviceTypeMeta;
61404
61547
  exports.deviceTypeMetaOf = deviceTypeMetaOf;
61405
61548
  exports.devicesToOptions = devicesToOptions;
61549
+ exports.dockBox = dockBox;
61406
61550
  exports.draftFromQuery = draftFromQuery;
61407
61551
  exports.emptyDevicesWidens = emptyDevicesWidens;
61408
61552
  exports.emptySessionRun = emptySessionRun;
@@ -61420,6 +61564,7 @@ exports.fleetViewOf = fleetViewOf;
61420
61564
  exports.formatControlDateTime = formatControlDateTime;
61421
61565
  exports.formatLastSeen = formatLastSeen;
61422
61566
  exports.formatNumeric = formatNumeric;
61567
+ exports.formatSettingValue = formatSettingValue;
61423
61568
  exports.freezeVerdict = freezeVerdict;
61424
61569
  exports.fuzzyMatch = fuzzyMatch;
61425
61570
  exports.getClassColor = getClassColor;
@@ -61443,6 +61588,7 @@ exports.isAbsentProvider$1 = isAbsentProvider;
61443
61588
  exports.isDataTableFilterActive = isDataTableFilterActive;
61444
61589
  exports.isFieldVisible = isFieldVisible;
61445
61590
  exports.isGroupExpanded = isGroupExpanded;
61591
+ exports.isValueField = isValueField;
61446
61592
  exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
61447
61593
  exports.lightColors = require_theme_index.lightColors$1;
61448
61594
  exports.loadRemoteBundle = loadRemoteBundle;
@@ -61450,8 +61596,10 @@ exports.makeScrubBridge = makeScrubBridge;
61450
61596
  exports.metadataEntries = metadataEntries;
61451
61597
  exports.metadataString = metadataString;
61452
61598
  exports.mirror = mirror;
61599
+ exports.modelRowsAreFoldable = modelRowsAreFoldable;
61453
61600
  exports.mountAddonPage = mountAddonPage;
61454
61601
  exports.mountPath = mountPath;
61602
+ exports.nextGridPosition = nextGridPosition;
61455
61603
  exports.nextReconnectAction = nextReconnectAction;
61456
61604
  exports.nextSort = nextSort;
61457
61605
  exports.nextSortDirection = nextSortDirection;
@@ -61507,9 +61655,11 @@ exports.rootSourceState = rootSourceState;
61507
61655
  exports.routerBasename = routerBasename;
61508
61656
  exports.sameLocationSuggestions = sameLocationSuggestions;
61509
61657
  exports.scrubReducer = scrubReducer;
61658
+ exports.searchSteps = searchSteps;
61510
61659
  exports.selectPickerGroup = selectPickerGroup;
61511
61660
  exports.selectedDeviceOptions = selectedDeviceOptions;
61512
61661
  exports.serializeRecordedCommand = serializeRecordedCommand;
61662
+ exports.settingsModelIdFor = settingsModelIdFor;
61513
61663
  exports.shouldCommit = shouldCommit;
61514
61664
  exports.shouldEmit = shouldEmit;
61515
61665
  exports.shouldShowAddNewFooter = shouldShowAddNewFooter;
@@ -61521,7 +61671,9 @@ exports.sortRowsByColumn = sortRowsByColumn;
61521
61671
  exports.statusIcons = statusIcons;
61522
61672
  exports.statusLine = statusLine;
61523
61673
  exports.statusLine$1 = statusLine;
61674
+ exports.stepFieldDomId = stepFieldDomId;
61524
61675
  exports.stepHasModelForFormat = stepHasModelForFormat;
61676
+ exports.stepLocalFields = stepLocalFields;
61525
61677
  exports.stepModelOptions = stepModelOptions;
61526
61678
  exports.stripParentNamePrefix = stripParentNamePrefix;
61527
61679
  exports.summarizeHubCandidates = summarizeHubCandidates;
@@ -62267,7 +62419,6 @@ exports.usePipelineExecutorCacheFrameInPool = usePipelineExecutorCacheFrameInPoo
62267
62419
  exports.usePipelineExecutorClearDeviceOverrides = usePipelineExecutorClearDeviceOverrides;
62268
62420
  exports.usePipelineExecutorClearModelBoundDeviceOverrides = usePipelineExecutorClearModelBoundDeviceOverrides;
62269
62421
  exports.usePipelineExecutorDeleteModel = usePipelineExecutorDeleteModel;
62270
- exports.usePipelineExecutorDeleteTemplate = usePipelineExecutorDeleteTemplate;
62271
62422
  exports.usePipelineExecutorDownloadModel = usePipelineExecutorDownloadModel;
62272
62423
  exports.usePipelineExecutorGetAddonModels = usePipelineExecutorGetAddonModels;
62273
62424
  exports.usePipelineExecutorGetAudioCapabilities = usePipelineExecutorGetAudioCapabilities;
@@ -62291,22 +62442,18 @@ exports.usePipelineExecutorInferCached = usePipelineExecutorInferCached;
62291
62442
  exports.usePipelineExecutorKillEngine = usePipelineExecutorKillEngine;
62292
62443
  exports.usePipelineExecutorListLoadedEngines = usePipelineExecutorListLoadedEngines;
62293
62444
  exports.usePipelineExecutorListReferenceImages = usePipelineExecutorListReferenceImages;
62294
- exports.usePipelineExecutorListTemplates = usePipelineExecutorListTemplates;
62295
62445
  exports.usePipelineExecutorRearmInferenceDevice = usePipelineExecutorRearmInferenceDevice;
62296
62446
  exports.usePipelineExecutorRunAudioTest = usePipelineExecutorRunAudioTest;
62297
62447
  exports.usePipelineExecutorRunPipeline = usePipelineExecutorRunPipeline;
62298
62448
  exports.usePipelineExecutorRunPipelineBatch = usePipelineExecutorRunPipelineBatch;
62299
- exports.usePipelineExecutorSaveTemplate = usePipelineExecutorSaveTemplate;
62300
62449
  exports.usePipelineExecutorSetVideoPipelineSteps = usePipelineExecutorSetVideoPipelineSteps;
62301
62450
  exports.usePipelineExecutorSpinEngine = usePipelineExecutorSpinEngine;
62302
62451
  exports.usePipelineExecutorUncacheFrame = usePipelineExecutorUncacheFrame;
62303
- exports.usePipelineExecutorUpdateTemplate = usePipelineExecutorUpdateTemplate;
62304
62452
  exports.usePipelineExecutorValidatePipeline = usePipelineExecutorValidatePipeline;
62305
62453
  exports.usePipelineOrchestratorApplyAutoTune = usePipelineOrchestratorApplyAutoTune;
62306
62454
  exports.usePipelineOrchestratorApplyDeviceSettingsPatch = usePipelineOrchestratorApplyDeviceSettingsPatch;
62307
62455
  exports.usePipelineOrchestratorAssignAudio = usePipelineOrchestratorAssignAudio;
62308
62456
  exports.usePipelineOrchestratorAssignPipeline = usePipelineOrchestratorAssignPipeline;
62309
- exports.usePipelineOrchestratorDeleteTemplate = usePipelineOrchestratorDeleteTemplate;
62310
62457
  exports.usePipelineOrchestratorGetAgentLoad = usePipelineOrchestratorGetAgentLoad;
62311
62458
  exports.usePipelineOrchestratorGetAgentSettings = usePipelineOrchestratorGetAgentSettings;
62312
62459
  exports.usePipelineOrchestratorGetAudioAssignment = usePipelineOrchestratorGetAudioAssignment;
@@ -62332,7 +62479,6 @@ exports.usePipelineOrchestratorGetPipelineDevicePin = usePipelineOrchestratorGet
62332
62479
  exports.usePipelineOrchestratorGetPlacementStats = usePipelineOrchestratorGetPlacementStats;
62333
62480
  exports.usePipelineOrchestratorGetReconcileFromDiskStatus = usePipelineOrchestratorGetReconcileFromDiskStatus;
62334
62481
  exports.usePipelineOrchestratorListAgentSettings = usePipelineOrchestratorListAgentSettings;
62335
- exports.usePipelineOrchestratorListTemplates = usePipelineOrchestratorListTemplates;
62336
62482
  exports.usePipelineOrchestratorPauseForStorageMigration = usePipelineOrchestratorPauseForStorageMigration;
62337
62483
  exports.usePipelineOrchestratorPlanAutoTune = usePipelineOrchestratorPlanAutoTune;
62338
62484
  exports.usePipelineOrchestratorRebalance = usePipelineOrchestratorRebalance;
@@ -62342,7 +62488,6 @@ exports.usePipelineOrchestratorRequestOccupancyLook = usePipelineOrchestratorReq
62342
62488
  exports.usePipelineOrchestratorResetNodePipelineDefaults = usePipelineOrchestratorResetNodePipelineDefaults;
62343
62489
  exports.usePipelineOrchestratorResolvePipeline = usePipelineOrchestratorResolvePipeline;
62344
62490
  exports.usePipelineOrchestratorResumeForStorageMigration = usePipelineOrchestratorResumeForStorageMigration;
62345
- exports.usePipelineOrchestratorSaveTemplate = usePipelineOrchestratorSaveTemplate;
62346
62491
  exports.usePipelineOrchestratorSetAgentCapabilities = usePipelineOrchestratorSetAgentCapabilities;
62347
62492
  exports.usePipelineOrchestratorSetAgentDecodeLimits = usePipelineOrchestratorSetAgentDecodeLimits;
62348
62493
  exports.usePipelineOrchestratorSetAgentInferenceDevices = usePipelineOrchestratorSetAgentInferenceDevices;
@@ -62361,7 +62506,6 @@ exports.usePipelineOrchestratorSetShareSource = usePipelineOrchestratorSetShareS
62361
62506
  exports.usePipelineOrchestratorSuggestShares = usePipelineOrchestratorSuggestShares;
62362
62507
  exports.usePipelineOrchestratorUnassignAudio = usePipelineOrchestratorUnassignAudio;
62363
62508
  exports.usePipelineOrchestratorUnassignPipeline = usePipelineOrchestratorUnassignPipeline;
62364
- exports.usePipelineOrchestratorUpdateTemplate = usePipelineOrchestratorUpdateTemplate;
62365
62509
  exports.usePipelineRunnerAttachCamera = usePipelineRunnerAttachCamera;
62366
62510
  exports.usePipelineRunnerDetachCamera = usePipelineRunnerDetachCamera;
62367
62511
  exports.usePipelineRunnerGetAllCameraMetrics = usePipelineRunnerGetAllCameraMetrics;
@@ -62737,4 +62881,5 @@ exports.waterHeaterTint = waterHeaterTint;
62737
62881
  exports.weatherConditionMeta = weatherConditionMeta;
62738
62882
  exports.weatherTint = weatherTint;
62739
62883
  exports.withResolvedParentDetail = withResolvedParentDetail;
62884
+ exports.withUseRootModel = withUseRootModel;
62740
62885
  exports.writeStoredGroupBy = writeStoredGroupBy;