@camstack/ui-library 1.2.243 → 1.2.245

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 (26) hide show
  1. package/dist/composites/agent-step-editor.d.ts +41 -16
  2. package/dist/composites/device-step-matrix.d.ts +2 -25
  3. package/dist/composites/index.d.ts +8 -11
  4. package/dist/composites/model-bound-slider.d.ts +8 -1
  5. package/dist/composites/pipeline-builder.d.ts +1 -28
  6. package/dist/composites/pipeline-matrix-shared.d.ts +1 -1
  7. package/dist/composites/pipeline-step.d.ts +4 -44
  8. package/dist/composites/step-inspector/accelerator-tabs.d.ts +12 -0
  9. package/dist/composites/step-inspector/gate-control.d.ts +20 -0
  10. package/dist/composites/step-inspector/index.d.ts +11 -0
  11. package/dist/composites/step-inspector/layered-settings-form.d.ts +38 -0
  12. package/dist/composites/step-inspector/segmented-choice.d.ts +11 -0
  13. package/dist/composites/step-inspector/setting-values.d.ts +19 -0
  14. package/dist/composites/step-inspector/step-chips.d.ts +11 -0
  15. package/dist/composites/step-inspector/step-inspector-layout.d.ts +23 -0
  16. package/dist/composites/step-inspector/step-inspector-panel.d.ts +26 -0
  17. package/dist/composites/step-inspector/step-list.d.ts +39 -0
  18. package/dist/composites/step-inspector/step-model-list.d.ts +9 -0
  19. package/dist/composites/step-inspector/step-model-row.d.ts +27 -0
  20. package/dist/composites/{step-tree-master.d.ts → step-tree.d.ts} +0 -12
  21. package/dist/generated/system-hooks.d.ts +2 -0
  22. package/dist/index.cjs +1339 -1016
  23. package/dist/index.js +1316 -1016
  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
package/dist/index.cjs CHANGED
@@ -8048,6 +8048,8 @@ var usePipelineOrchestratorSetCameraStepToggle = trpc.pipelineOrchestrator.setCa
8048
8048
  var usePipelineOrchestratorGetCameraStepOverrides = trpc.pipelineOrchestrator.getCameraStepOverrides.useQuery;
8049
8049
  /** Generated alias around `trpc.pipelineOrchestrator.setCameraStepOverride.useMutation`. */
8050
8050
  var usePipelineOrchestratorSetCameraStepOverride = trpc.pipelineOrchestrator.setCameraStepOverride.useMutation;
8051
+ /** Generated alias around `trpc.pipelineOrchestrator.setCameraStepSettings.useMutation`. */
8052
+ var usePipelineOrchestratorSetCameraStepSettings = trpc.pipelineOrchestrator.setCameraStepSettings.useMutation;
8051
8053
  /** Generated alias around `trpc.pipelineOrchestrator.setCameraPipelineForAgent.useMutation`. */
8052
8054
  var usePipelineOrchestratorSetCameraPipelineForAgent = trpc.pipelineOrchestrator.setCameraPipelineForAgent.useMutation;
8053
8055
  /** Generated alias around `trpc.pipelineOrchestrator.resolvePipeline.useQuery`. */
@@ -28744,32 +28746,6 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
28744
28746
  });
28745
28747
  }
28746
28748
  //#endregion
28747
- //#region src/primitives/collapsible-card.tsx
28748
- function CollapsibleCard({ expanded, onExpandedChange, header, children, dimmed = false, hideChevron = false, className }) {
28749
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28750
- className: cn("rounded-lg border border-border bg-surface overflow-hidden", dimmed && "opacity-[0.45]", className),
28751
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28752
- role: "button",
28753
- tabIndex: 0,
28754
- className: "flex items-center gap-2.5 px-3 py-2.5 cursor-pointer select-none",
28755
- onClick: () => onExpandedChange(!expanded),
28756
- onKeyDown: (e) => {
28757
- if (e.key === "Enter" || e.key === " ") {
28758
- e.preventDefault();
28759
- onExpandedChange(!expanded);
28760
- }
28761
- },
28762
- children: [!hideChevron && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28763
- className: "text-foreground-subtle shrink-0",
28764
- 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" })
28765
- }), header]
28766
- }), expanded && children && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28767
- className: "border-t border-border px-3 py-3",
28768
- children
28769
- })]
28770
- });
28771
- }
28772
- //#endregion
28773
28749
  //#region src/composites/grouped-model-selector.tsx
28774
28750
  /**
28775
28751
  * GroupedModelSelector — the shared Family → Tier → Variant model picker.
@@ -29050,6 +29026,257 @@ function ModelPicker({ models, value, onChange, disabled, defaultModelId }) {
29050
29026
  });
29051
29027
  }
29052
29028
  //#endregion
29029
+ //#region src/composites/resolve-step-default-model.ts
29030
+ function resolveStepDefaultModel(catalog, format) {
29031
+ const hasFormatBuild = (modelId) => catalog.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
29032
+ const declared = catalog.defaultModelIdByFormat?.[format];
29033
+ if (declared !== void 0 && hasFormatBuild(declared)) return declared;
29034
+ if (hasFormatBuild(catalog.defaultModelId)) return catalog.defaultModelId;
29035
+ const available = catalog.models.filter((m) => m.formats[format] !== void 0 && m.legacy !== true);
29036
+ if (available.length === 0) return catalog.defaultModelId;
29037
+ return [...available].toSorted((a, b) => (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity))[0].id;
29038
+ }
29039
+ /**
29040
+ * The model a matrix CELL shows as this step's effective default on `device`.
29041
+ *
29042
+ * The device's ELECTED model (`device.defaultModelId`, the per-device
29043
+ * object-detection election) applies ONLY when this step's catalog actually
29044
+ * contains it — `slot === 'detector'` alone is too wide, because
29045
+ * package-detection shares the detector slot with the root step. Bit live
29046
+ * 2026-08-08: rfdetr-package became the package step's default and every
29047
+ * matrix cell still showed the device's yolo as the "selected" model, because
29048
+ * the election was applied slot-wide. Membership is the principled gate: an
29049
+ * elected model this step cannot run is never displayed for it.
29050
+ */
29051
+ function resolveEffectiveDefaultModel(catalog, device) {
29052
+ const format = device?.format;
29053
+ if (format === void 0) return void 0;
29054
+ const elected = device?.defaultModelId;
29055
+ return catalog.slot === "detector" && elected !== void 0 && catalog.models.some((m) => m.id === elected) ? elected : resolveStepDefaultModel(catalog, format);
29056
+ }
29057
+ /**
29058
+ * The models a step may run on an engine `format` — the ONE derivation of the
29059
+ * option list. An unknown format yields NOTHING rather than everything: a
29060
+ * picker that offered a model with no build for the device would write a pin
29061
+ * the runner cannot honour.
29062
+ */
29063
+ function stepModelOptions(models, format) {
29064
+ if (format === void 0) return [];
29065
+ return models.filter((m) => m.formats[format] !== void 0);
29066
+ }
29067
+ /** Can this step run on `format` at all? (`false` ⇒ the caller renders "⚠ skip"/"—".) */
29068
+ function stepHasModelForFormat(models, format) {
29069
+ return stepModelOptions(models, format).length > 0;
29070
+ }
29071
+ /**
29072
+ * The model a step ACTUALLY runs for a (device, camera) — the whole effective
29073
+ * chain in one place: per-camera override → per-(node,device) base → the
29074
+ * resolved default (`resolveEffectiveDefaultModel`).
29075
+ *
29076
+ * Every surface calls this: the matrix cell, and the drawer's picker seed. They
29077
+ * are the same number, and the only way to keep them the same number is to have
29078
+ * one function produce it. A surface that owns fewer layers omits them (the node
29079
+ * matrix passes no `override`) — it does NOT get its own resolver.
29080
+ */
29081
+ function resolveEffectiveStepModel(input) {
29082
+ return input.override ?? input.base ?? resolveEffectiveDefaultModel(input.catalog, input.device);
29083
+ }
29084
+ //#endregion
29085
+ //#region src/composites/root-model-source.tsx
29086
+ /**
29087
+ * "Use the root model" — the step editor's control for a step the camera's
29088
+ * ROOT detection model can supply (D730).
29089
+ *
29090
+ * The flag (`useRootModel`, in the step's own settings) is an operator INTENT
29091
+ * kept beside the step's model: checking it never changes the model, and a
29092
+ * root that cannot supply the step leaves the step running its own model while
29093
+ * the flag waits, inactive, for a root that can. Whether it is in force is
29094
+ * `resolveRootSupply` — the same function the runner decides with.
29095
+ */
29096
+ /** The editor's view of the flag for one step, or `null` for a step no root can supply. */
29097
+ function rootSourceState(stepId, settings, rootModel) {
29098
+ if (!(0, _camstack_types.isRootSuppliableStepId)(stepId)) return null;
29099
+ return (0, _camstack_types.resolveRootSupply)(stepId, settings, rootModel?.supplies);
29100
+ }
29101
+ /** The settings bag with the flag set — every other key untouched. */
29102
+ function withUseRootModel(settings, value) {
29103
+ return {
29104
+ ...settings,
29105
+ [_camstack_types.USE_ROOT_MODEL_SETTING_KEY]: value
29106
+ };
29107
+ }
29108
+ /** What the operator must know about a supply, beyond "it is on". */
29109
+ function supplyNotes(supply) {
29110
+ if (supply.kind === "label") {
29111
+ const notes = supply.uncovered.length > 0 ? [`Not produced by this model: ${supply.uncovered.join(", ")}.`] : [];
29112
+ 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."];
29113
+ 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."];
29114
+ }
29115
+ 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."];
29116
+ 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."];
29117
+ 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."];
29118
+ }
29119
+ /** The reason an intended supply is not in force, in the operator's words. */
29120
+ function inactiveText(state, rootModel) {
29121
+ switch (state.inactiveReason) {
29122
+ 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).`;
29123
+ case "root-does-not-declare":
29124
+ case void 0: return `the current root model (${rootModel.name}) cannot supply this.`;
29125
+ }
29126
+ }
29127
+ function RootModelSourceControl({ stepId, settings, rootModel, disabled, onChange }) {
29128
+ if (rootModel === void 0) return null;
29129
+ const state = rootSourceState(stepId, settings, rootModel);
29130
+ if (state === null) return null;
29131
+ const inactiveNote = state.inactive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29132
+ className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29133
+ "data-testid": "root-model-inactive",
29134
+ children: [
29135
+ "Use the root model: inactive — ",
29136
+ inactiveText(state, rootModel),
29137
+ " The step runs its own model."
29138
+ ]
29139
+ }) : null;
29140
+ if (state.supply === void 0) return inactiveNote;
29141
+ const checked = state.intent;
29142
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29143
+ className: "space-y-1",
29144
+ "data-testid": "root-model-source",
29145
+ children: [
29146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29147
+ className: "flex items-center gap-2 text-xs text-foreground",
29148
+ children: [
29149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29150
+ type: "checkbox",
29151
+ checked,
29152
+ disabled,
29153
+ onChange: (e) => onChange(e.target.checked)
29154
+ }),
29155
+ "Use the root model (",
29156
+ rootModel.name,
29157
+ ")"
29158
+ ]
29159
+ }),
29160
+ inactiveNote,
29161
+ state.effective && supplyNotes(state.supply).map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29162
+ className: "text-[10px] leading-snug text-foreground-subtle",
29163
+ children: note
29164
+ }, note))
29165
+ ]
29166
+ });
29167
+ }
29168
+ //#endregion
29169
+ //#region src/lib/responsive.ts
29170
+ /**
29171
+ * Responsive design tokens — three-tier system.
29172
+ *
29173
+ * The admin-ui targets three width tiers; every component should pick
29174
+ * the right token from this file instead of hand-coding breakpoints.
29175
+ * Adopting the tokens uniformly is what makes the app feel consistent
29176
+ * across operator devices (mobile / tablet / desktop) and what unlocks
29177
+ * targeted tuning (e.g. shrink every label by 1px in one place).
29178
+ *
29179
+ * Tier definitions (Tailwind breakpoints):
29180
+ *
29181
+ * • **L1 — Compact** (`< 640px`, default = mobile portrait)
29182
+ * Single column, tight padding, smallest text, icon-only buttons
29183
+ * where labels can be inferred. The operator can still see every
29184
+ * stat / action — no horizontal scroll, no clipped text. The user
29185
+ * directive: "molta informazione in poco spazio quando la width è
29186
+ * bassa." [a lot of information in little space when the width is low.]
29187
+ *
29188
+ * • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
29189
+ * 2-column grids, side panels become visible, button labels return.
29190
+ * Targets ~768px–1024px viewports — the most common operator sizes.
29191
+ *
29192
+ * • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
29193
+ * 4-column grids, generous padding, full-width header text,
29194
+ * side-by-side composite layouts. The operator has elbow room.
29195
+ *
29196
+ * Conventions:
29197
+ * - **Always start from L1** — the bare class is always the compact
29198
+ * variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
29199
+ * desktop-first breakpoints; no max-width queries.
29200
+ * - **Token strings are static** — kept as `as const` so callers can
29201
+ * compose them via `cn()` / template literals without running a
29202
+ * class generator at runtime.
29203
+ * - **Pair the token with a comment** — the comment in the file
29204
+ * names every breakpoint it crosses, so a reader can predict the
29205
+ * layout without going to Tailwind docs.
29206
+ */
29207
+ /** Container padding for a card-style section (header + body). */
29208
+ var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
29209
+ /** Header strip inside a card (between border-b lines). */
29210
+ var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
29211
+ /** Body padding for a card-style section. */
29212
+ var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
29213
+ /** Gap between cards in a vertical stack. */
29214
+ var STACK_GAP = "space-y-3 sm:space-y-4";
29215
+ /** Gap between cards in a grid (4 cells / row at lg). */
29216
+ var GRID_GAP = "gap-2 sm:gap-3";
29217
+ /** Row padding inside a divided list (e.g. InfoRow). */
29218
+ var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
29219
+ /** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
29220
+ var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
29221
+ /** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
29222
+ var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
29223
+ /** Side-by-side layout that stacks below md (canvas + side panel). */
29224
+ var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
29225
+ /** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
29226
+ 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";
29227
+ /**
29228
+ * The tier boundary at which a settings label/value row stops stacking.
29229
+ *
29230
+ * Below it (L1 — compact) the row is ONE column: label on its own line, value
29231
+ * on the next at full width. Above it the row is label-left / value-right as
29232
+ * before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
29233
+ * value beside it; a phone has neither, and the failure mode is not "cramped"
29234
+ * but "the value is the part that gets cut" — the one thing the operator
29235
+ * opened the page to read.
29236
+ */
29237
+ var SETTING_ROW_STACK_BREAKPOINT = "sm";
29238
+ /** Outer row: single column at L1, label/value columns from `sm` up. */
29239
+ var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
29240
+ /** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
29241
+ var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
29242
+ /**
29243
+ * Value lane — the value text plus its trailing affordances (copy / reveal).
29244
+ * `w-full` at L1 so the value owns the whole row once the label is above it;
29245
+ * `sm:flex-1` makes it share the row again from `sm` up.
29246
+ */
29247
+ var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
29248
+ /**
29249
+ * The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
29250
+ * at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
29251
+ * desktop form, where the row is wide enough for truncation to be a choice
29252
+ * rather than data loss.
29253
+ */
29254
+ var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
29255
+ /** Section header label (uppercase tracking-wider). */
29256
+ var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
29257
+ /** Field label inside a row. */
29258
+ var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
29259
+ /** Sub-label / hint text. */
29260
+ var TEXT_HINT = "text-[10px] text-foreground-subtle";
29261
+ /** Primary value (a metric, badge, …). */
29262
+ var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
29263
+ /** Big metric value (StatCard). */
29264
+ var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
29265
+ /** Default compact button. */
29266
+ 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";
29267
+ /** Primary compact button. */
29268
+ 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";
29269
+ /** Danger compact button. */
29270
+ 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";
29271
+ /** Warning compact button (e.g. cancel-drawing). */
29272
+ 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";
29273
+ /** Compact text/select/number input. */
29274
+ 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";
29275
+ /** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
29276
+ var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
29277
+ var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
29278
+ var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
29279
+ //#endregion
29053
29280
  //#region src/composites/model-bound-slider.tsx
29054
29281
  var SOURCE_LABEL = {
29055
29282
  override: "Override for this model",
@@ -29059,7 +29286,7 @@ var SOURCE_LABEL = {
29059
29286
  function formatNumber(value, scale) {
29060
29287
  return (scale !== void 0 ? value / scale : value).toFixed(2).replace(/\.?0+$/, "");
29061
29288
  }
29062
- function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings }) {
29289
+ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings, hideSourceRow = false }) {
29063
29290
  const modelDefault = field.modelApplicability?.defaultByModel?.[modelId];
29064
29291
  const state = (0, _camstack_types.resolveModelBoundSetting)(settings, field.key, modelId.length > 0 ? modelId : void 0, modelDefault, field.default);
29065
29292
  const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
@@ -29099,7 +29326,7 @@ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings
29099
29326
  onChange: (e) => handleChange(Number(e.target.value)),
29100
29327
  className: "w-full accent-primary h-1.5"
29101
29328
  }),
29102
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29329
+ !hideSourceRow && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29103
29330
  className: "mt-1 flex items-center justify-between gap-2 text-[10px]",
29104
29331
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29105
29332
  "data-testid": "model-bound-source",
@@ -29150,6 +29377,32 @@ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset
29150
29377
  });
29151
29378
  }
29152
29379
  //#endregion
29380
+ //#region src/primitives/collapsible-card.tsx
29381
+ function CollapsibleCard({ expanded, onExpandedChange, header, children, dimmed = false, hideChevron = false, className }) {
29382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29383
+ className: cn("rounded-lg border border-border bg-surface overflow-hidden", dimmed && "opacity-[0.45]", className),
29384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29385
+ role: "button",
29386
+ tabIndex: 0,
29387
+ className: "flex items-center gap-2.5 px-3 py-2.5 cursor-pointer select-none",
29388
+ onClick: () => onExpandedChange(!expanded),
29389
+ onKeyDown: (e) => {
29390
+ if (e.key === "Enter" || e.key === " ") {
29391
+ e.preventDefault();
29392
+ onExpandedChange(!expanded);
29393
+ }
29394
+ },
29395
+ children: [!hideChevron && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29396
+ className: "text-foreground-subtle shrink-0",
29397
+ 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" })
29398
+ }), header]
29399
+ }), expanded && children && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29400
+ className: "border-t border-border px-3 py-3",
29401
+ children
29402
+ })]
29403
+ });
29404
+ }
29405
+ //#endregion
29153
29406
  //#region src/composites/pipeline-step.tsx
29154
29407
  /**
29155
29408
  * PipelineStep — single step card in the pipeline builder.
@@ -29161,124 +29414,77 @@ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset
29161
29414
  function modelsForStep(schema) {
29162
29415
  return schema?.models ?? [];
29163
29416
  }
29164
- function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false, toggleMode = "simple", overrideState = null, onOverrideChange, inheritedEnabled, hideModelAndSettings = false, allowAutoModel = false }) {
29417
+ function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false }) {
29165
29418
  const [expanded, setExpanded] = (0, react$1.useState)(false);
29166
29419
  const models = modelsForStep(schema);
29167
- const isAutoSentinel = allowAutoModel && step.modelId === "";
29168
- if (models.length > 0 && !isAutoSentinel && !models.some((m) => m.id === step.modelId) && !readOnly) {
29420
+ if (models.length > 0 && !models.some((m) => m.id === step.modelId) && !readOnly) {
29169
29421
  const fallback = (schema?.defaultModelId ? models.find((m) => m.id === schema.defaultModelId) : null) ?? models[0];
29170
29422
  if (fallback && fallback.id !== step.modelId) queueMicrotask(() => onChange({
29171
29423
  ...step,
29172
29424
  modelId: fallback.id
29173
29425
  }));
29174
29426
  }
29175
- const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
29176
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29177
- className: "flex-1 min-w-0",
29178
- children: [
29179
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29180
- className: "text-[10px] uppercase tracking-wider font-medium text-foreground-subtle/60 block leading-none",
29181
- children: step.slot
29182
- }),
29183
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29184
- className: "text-sm font-semibold text-foreground truncate block leading-tight",
29185
- children: step.addonName
29186
- }),
29187
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29188
- className: "flex items-center gap-1 mt-0.5 flex-wrap",
29189
- children: [
29190
- step.inputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29191
- className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/12 text-blue-400",
29192
- children: c
29193
- }, c)),
29194
- step.inputClasses.length > 0 && step.outputClasses.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29195
- className: "text-foreground-subtle/40 text-[10px]",
29196
- children: "→"
29197
- }),
29198
- step.outputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29199
- className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/12 text-green-400",
29200
- children: c
29201
- }, c))
29202
- ]
29203
- })
29204
- ]
29205
- }),
29206
- !readOnly && toggleMode === "simple" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29207
- onClick: (e) => {
29208
- e.stopPropagation();
29209
- onChange({
29210
- ...step,
29211
- enabled: !step.enabled
29212
- });
29213
- },
29214
- 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"),
29215
- 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") })
29216
- }),
29217
- !readOnly && toggleMode === "threeState" && onOverrideChange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29218
- className: "flex items-center gap-0.5 rounded-md border border-border p-0.5",
29219
- onClick: (e) => e.stopPropagation(),
29220
- children: [
29221
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29222
- label: "Inherit",
29223
- active: overrideState === null,
29224
- variant: "inherit",
29225
- subtext: inheritedEnabled === void 0 ? void 0 : inheritedEnabled ? "on" : "off",
29226
- onClick: () => onOverrideChange(null)
29227
- }),
29228
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29229
- label: "On",
29230
- active: overrideState === true,
29231
- variant: "on",
29232
- onClick: () => onOverrideChange(true)
29233
- }),
29234
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29235
- label: "Off",
29236
- active: overrideState === false,
29237
- variant: "off",
29238
- onClick: () => onOverrideChange(false)
29239
- })
29240
- ]
29241
- })
29242
- ] });
29427
+ const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29428
+ className: "flex-1 min-w-0",
29429
+ children: [
29430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29431
+ className: "text-[10px] uppercase tracking-wider font-medium text-foreground-subtle/60 block leading-none",
29432
+ children: step.slot
29433
+ }),
29434
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29435
+ className: "text-sm font-semibold text-foreground truncate block leading-tight",
29436
+ children: step.addonName
29437
+ }),
29438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29439
+ className: "flex items-center gap-1 mt-0.5 flex-wrap",
29440
+ children: [
29441
+ step.inputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29442
+ className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/12 text-blue-400",
29443
+ children: c
29444
+ }, c)),
29445
+ step.inputClasses.length > 0 && step.outputClasses.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29446
+ className: "text-foreground-subtle/40 text-[10px]",
29447
+ children: "→"
29448
+ }),
29449
+ step.outputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29450
+ className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/12 text-green-400",
29451
+ children: c
29452
+ }, c))
29453
+ ]
29454
+ })
29455
+ ]
29456
+ }), !readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29457
+ onClick: (e) => {
29458
+ e.stopPropagation();
29459
+ onChange({
29460
+ ...step,
29461
+ enabled: !step.enabled
29462
+ });
29463
+ },
29464
+ 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"),
29465
+ 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") })
29466
+ })] });
29243
29467
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleCard, {
29244
29468
  expanded,
29245
29469
  onExpandedChange: setExpanded,
29246
29470
  dimmed: !step.enabled,
29247
29471
  header,
29248
- children: hideModelAndSettings ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29249
- className: "text-[11px] text-foreground-subtle",
29250
- children: "Model and detection settings are managed per agent — open the Pipeline page for this camera's agent to edit them."
29251
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29472
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29252
29473
  className: "space-y-3",
29253
29474
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29254
29475
  className: "space-y-2",
29255
- children: [
29256
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
29257
- className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
29258
- children: "Model"
29259
- }),
29260
- allowAutoModel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29261
- className: "flex items-center gap-2 text-[11px] text-foreground-subtle",
29262
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29263
- type: "checkbox",
29264
- checked: isAutoSentinel,
29265
- disabled: readOnly,
29266
- onChange: (e) => onChange({
29267
- ...step,
29268
- modelId: e.target.checked ? "" : schema?.defaultModelId ?? models[0]?.id ?? ""
29269
- })
29270
- }), "Auto (node default)"]
29271
- }),
29272
- !isAutoSentinel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29273
- models,
29274
- value: step.modelId,
29275
- disabled: readOnly,
29276
- onChange: (id) => onChange({
29277
- ...step,
29278
- modelId: id
29279
- })
29476
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
29477
+ className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
29478
+ children: "Model"
29479
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29480
+ models,
29481
+ value: step.modelId,
29482
+ disabled: readOnly,
29483
+ onChange: (id) => onChange({
29484
+ ...step,
29485
+ modelId: id
29280
29486
  })
29281
- ]
29487
+ })]
29282
29488
  }), schema?.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29283
29489
  field,
29284
29490
  allFields: schema.configSchema ?? [],
@@ -29345,7 +29551,7 @@ function resolveSettingValue(key, step, allFields) {
29345
29551
  const sibling = allFields.find((f) => "key" in f && f.key === key);
29346
29552
  return sibling && "default" in sibling ? sibling.default : void 0;
29347
29553
  }
29348
- function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29554
+ function ConfigSchemaField({ field, allFields, step, disabled, onChange, hideModelBoundSourceRow }) {
29349
29555
  const applicability = "modelApplicability" in field ? field.modelApplicability : void 0;
29350
29556
  const inapplicableReason = applicability?.inapplicable?.[step.modelId];
29351
29557
  const control = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaFieldControl, {
@@ -29354,7 +29560,8 @@ function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29354
29560
  step,
29355
29561
  disabled: disabled || inapplicableReason !== void 0,
29356
29562
  modelScopedOptions: applicability?.optionsByModel?.[step.modelId],
29357
- onChange
29563
+ onChange,
29564
+ hideModelBoundSourceRow: hideModelBoundSourceRow === true
29358
29565
  });
29359
29566
  if (inapplicableReason === void 0) return control;
29360
29567
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -29365,7 +29572,7 @@ function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29365
29572
  })]
29366
29573
  });
29367
29574
  }
29368
- function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScopedOptions, onChange }) {
29575
+ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScopedOptions, onChange, hideModelBoundSourceRow }) {
29369
29576
  const showWhen = field.type !== "separator" && field.type !== "info" ? field.showWhen : void 0;
29370
29577
  if (showWhen) {
29371
29578
  const { field: condField, equals, notEquals, in: inList, notIn } = showWhen;
@@ -29415,7 +29622,8 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29415
29622
  onChangeSettings: (settings) => onChange({
29416
29623
  ...step,
29417
29624
  settings
29418
- })
29625
+ }),
29626
+ hideSourceRow: hideModelBoundSourceRow
29419
29627
  });
29420
29628
  if (field.type === "slider" || field.type === "number") {
29421
29629
  const raw = step.settings?.[field.key];
@@ -29501,204 +29709,247 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29501
29709
  });
29502
29710
  return null;
29503
29711
  }
29504
- function ThreeStateButton({ label, active, variant, subtext, onClick }) {
29505
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
29506
- type: "button",
29507
- onClick,
29508
- 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"),
29509
- children: [label, subtext && active && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29510
- className: "ml-1 opacity-60",
29511
- children: [
29512
- "(",
29513
- subtext,
29514
- ")"
29515
- ]
29516
- })]
29517
- });
29518
- }
29519
29712
  //#endregion
29520
- //#region src/composites/resolve-step-default-model.ts
29521
- function resolveStepDefaultModel(catalog, format) {
29522
- const hasFormatBuild = (modelId) => catalog.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
29523
- const declared = catalog.defaultModelIdByFormat?.[format];
29524
- if (declared !== void 0 && hasFormatBuild(declared)) return declared;
29525
- if (hasFormatBuild(catalog.defaultModelId)) return catalog.defaultModelId;
29526
- const available = catalog.models.filter((m) => m.formats[format] !== void 0 && m.legacy !== true);
29527
- if (available.length === 0) return catalog.defaultModelId;
29528
- return [...available].toSorted((a, b) => (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity))[0].id;
29713
+ //#region src/composites/step-inspector/setting-values.ts
29714
+ function isValueField(field) {
29715
+ return field.type !== "separator" && field.type !== "info" && field.type !== "group";
29529
29716
  }
29530
- /**
29531
- * The model a matrix CELL shows as this step's effective default on `device`.
29532
- *
29533
- * The device's ELECTED model (`device.defaultModelId`, the per-device
29534
- * object-detection election) applies ONLY when this step's catalog actually
29535
- * contains it — `slot === 'detector'` alone is too wide, because
29536
- * package-detection shares the detector slot with the root step. Bit live
29537
- * 2026-08-08: rfdetr-package became the package step's default and every
29538
- * matrix cell still showed the device's yolo as the "selected" model, because
29539
- * the election was applied slot-wide. Membership is the principled gate: an
29540
- * elected model this step cannot run is never displayed for it.
29541
- */
29542
- function resolveEffectiveDefaultModel(catalog, device) {
29543
- const format = device?.format;
29544
- if (format === void 0) return void 0;
29545
- const elected = device?.defaultModelId;
29546
- return catalog.slot === "detector" && elected !== void 0 && catalog.models.some((m) => m.id === elected) ? elected : resolveStepDefaultModel(catalog, format);
29717
+ /** True for a D738 field whose value is stored with the model it was made for. */
29718
+ function bindsToModel(field) {
29719
+ return (field.type === "slider" || field.type === "number") && field.bindsToModel === true;
29547
29720
  }
29548
29721
  /**
29549
- * The models a step may run on an engine `format` — the ONE derivation of the
29550
- * option list. An unknown format yields NOTHING rather than everything: a
29551
- * picker that offered a model with no build for the device would write a pin
29552
- * the runner cannot honour.
29722
+ * What runs when no layer stores the field: the running model's default for a
29723
+ * model-bound field that the model declares one for (D738), else the field's
29724
+ * own default. `fromModel` says which.
29553
29725
  */
29554
- function stepModelOptions(models, format) {
29555
- if (format === void 0) return [];
29556
- return models.filter((m) => m.formats[format] !== void 0);
29726
+ function defaultOfField(field, modelId) {
29727
+ const byModel = "modelApplicability" in field ? field.modelApplicability?.defaultByModel?.[modelId] : void 0;
29728
+ if (byModel !== void 0) return {
29729
+ value: byModel,
29730
+ fromModel: true
29731
+ };
29732
+ return {
29733
+ value: "default" in field ? field.default : void 0,
29734
+ fromModel: false
29735
+ };
29557
29736
  }
29558
- /** Can this step run on `format` at all? (`false` ⇒ the caller renders "⚠ skip"/"—".) */
29559
- function stepHasModelForFormat(models, format) {
29560
- return stepModelOptions(models, format).length > 0;
29737
+ function trimNumber(n) {
29738
+ return n.toFixed(2).replace(/\.?0+$/, "");
29561
29739
  }
29562
- /**
29563
- * The model a step ACTUALLY runs for a (device, camera) — the whole effective
29564
- * chain in one place: per-camera override → per-(node,device) base → the
29565
- * resolved default (`resolveEffectiveDefaultModel`).
29566
- *
29567
- * Every surface calls this: the matrix cell, and the drawer's picker seed. They
29568
- * are the same number, and the only way to keep them the same number is to have
29569
- * one function produce it. A surface that owns fewer layers omits them (the node
29570
- * matrix passes no `override`) — it does NOT get its own resolver.
29571
- */
29572
- function resolveEffectiveStepModel(input) {
29573
- return input.override ?? input.base ?? resolveEffectiveDefaultModel(input.catalog, input.device);
29740
+ /** One stored value as the operator reads it (`0.65`, `on`, `Person, Vehicle`, `—`). */
29741
+ function formatSettingValue(field, value) {
29742
+ if (value === void 0 || value === null) return "—";
29743
+ if (typeof value === "boolean") return value ? "on" : "off";
29744
+ if (typeof value === "number") {
29745
+ const scale = field.type === "slider" ? field.displayScale : void 0;
29746
+ const unit = "unit" in field && typeof field.unit === "string" ? ` ${field.unit}` : "";
29747
+ return `${trimNumber(scale !== void 0 ? value / scale : value)}${unit}`;
29748
+ }
29749
+ const options = "options" in field && Array.isArray(field.options) ? field.options : [];
29750
+ const labelOf = (v) => {
29751
+ const hit = options.find((o) => o.value === v);
29752
+ return hit ? hit.label : String(v);
29753
+ };
29754
+ if (Array.isArray(value)) return value.length === 0 ? "none" : value.map(labelOf).join(", ");
29755
+ if (typeof value === "string") return labelOf(value);
29756
+ return JSON.stringify(value);
29574
29757
  }
29575
29758
  //#endregion
29576
- //#region src/composites/root-model-source.tsx
29577
- /**
29578
- * "Use the root model" — the step editor's control for a step the camera's
29579
- * ROOT detection model can supply (D730).
29580
- *
29581
- * The flag (`useRootModel`, in the step's own settings) is an operator INTENT
29582
- * kept beside the step's model: checking it never changes the model, and a
29583
- * root that cannot supply the step leaves the step running its own model while
29584
- * the flag waits, inactive, for a root that can. Whether it is in force is
29585
- * `resolveRootSupply` — the same function the runner decides with.
29586
- */
29587
- /** The editor's view of the flag for one step, or `null` for a step no root can supply. */
29588
- function rootSourceState(stepId, settings, rootModel) {
29589
- if (!(0, _camstack_types.isRootSuppliableStepId)(stepId)) return null;
29590
- return (0, _camstack_types.resolveRootSupply)(stepId, settings, rootModel?.supplies);
29759
+ //#region src/composites/step-inspector/layered-settings-form.tsx
29760
+ var SOURCE_TONE$1 = {
29761
+ accelerator: "border-amber-500/40 bg-amber-500/10 text-amber-500",
29762
+ camera: "border-primary/40 bg-primary/10 text-primary",
29763
+ node: "border-border bg-muted/40 text-foreground",
29764
+ default: "border-border/60 text-foreground-subtle"
29765
+ };
29766
+ /** DOM id of a field's box — the target of a search hit. */
29767
+ function stepFieldDomId(key) {
29768
+ return `step-field-${key}`;
29591
29769
  }
29592
- /** The settings bag with the flag set — every other key untouched. */
29593
- function withUseRootModel(settings, value) {
29770
+ function displayStep(addon, modelId, settings) {
29594
29771
  return {
29595
- ...settings,
29596
- [_camstack_types.USE_ROOT_MODEL_SETTING_KEY]: value
29772
+ addonId: addon.id,
29773
+ addonName: addon.name,
29774
+ slot: addon.slot,
29775
+ inputClasses: [...addon.inputClasses],
29776
+ outputClasses: [...addon.outputClasses],
29777
+ enabled: true,
29778
+ modelId,
29779
+ children: [],
29780
+ settings: { ...settings }
29597
29781
  };
29598
29782
  }
29599
- /** What the operator must know about a supply, beyond "it is on". */
29600
- function supplyNotes(supply) {
29601
- if (supply.kind === "label") {
29602
- const notes = supply.uncovered.length > 0 ? [`Not produced by this model: ${supply.uncovered.join(", ")}.`] : [];
29603
- 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."];
29604
- 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."];
29605
- }
29606
- 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."];
29607
- 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."];
29608
- 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."];
29609
- }
29610
- /** The reason an intended supply is not in force, in the operator's words. */
29611
- function inactiveText(state, rootModel) {
29612
- switch (state.inactiveReason) {
29613
- 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).`;
29614
- case "root-does-not-declare":
29615
- case void 0: return `the current root model (${rootModel.name}) cannot supply this.`;
29616
- }
29617
- }
29618
- function RootModelSourceControl({ stepId, settings, rootModel, disabled, onChange }) {
29619
- if (rootModel === void 0) return null;
29620
- const state = rootSourceState(stepId, settings, rootModel);
29621
- if (state === null) return null;
29622
- const inactiveNote = state.inactive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29623
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29624
- "data-testid": "root-model-inactive",
29783
+ var LAYER_IDS = [
29784
+ "accelerator",
29785
+ "camera",
29786
+ "node"
29787
+ ];
29788
+ /** "Camera · node 0.65 · model default 0.78 · [Reset to node]" under one field. */
29789
+ function SettingProvenance({ field, layers, target, labels, boundKeys, modelId, pinned, disabled, onReset, action }) {
29790
+ const effective = (0, _camstack_types.effectiveStepSettings)(layers);
29791
+ const fallback = defaultOfField(field, modelId);
29792
+ const defaultLabel = fallback.fromModel ? "model default" : "default";
29793
+ const stored = (0, _camstack_types.stepSettingSource)(layers, field.key, boundKeys);
29794
+ const bound = bindsToModel(field);
29795
+ const state = bound ? (0, _camstack_types.resolveModelBoundSetting)(effective, field.key, modelId, fallback.value, fallback.value) : null;
29796
+ const source = state === null || state.source === "override" ? stored : "default";
29797
+ const sourceText = source === "default" ? defaultLabel : labels[source] ?? source;
29798
+ const levels = LAYER_IDS.filter((id) => id !== source && labels[id] !== void 0).filter((id) => layers[id]?.[field.key] !== void 0).map((id) => {
29799
+ const binding = bound ? layers[id]?.[(0, _camstack_types.modelBindingKey)(field.key)] : void 0;
29800
+ const forOther = typeof binding === "string" && binding !== modelId ? ` (for ${binding})` : "";
29801
+ return `${labels[id]} ${formatSettingValue(field, layers[id]?.[field.key])}${forOther}`;
29802
+ });
29803
+ const withDefault = source === "default" ? levels : [...levels, `${defaultLabel} ${formatSettingValue(field, fallback.value)}`];
29804
+ const targetStores = (0, _camstack_types.stepSettingSource)({ [target]: layers[target] }, field.key, boundKeys) !== "default";
29805
+ const below = LAYER_IDS.slice(LAYER_IDS.indexOf(target) + 1).find((id) => labels[id] !== void 0 && layers[id]?.[field.key] !== void 0);
29806
+ const resetTo = below !== void 0 ? labels[below] ?? below : defaultLabel;
29807
+ const belowSource = stored === "default" ? "default" : (0, _camstack_types.stepSettingSource)({
29808
+ ...layers,
29809
+ [stored]: void 0
29810
+ }, field.key, boundKeys);
29811
+ const pinnedAt = belowSource === "default" ? defaultLabel : labels[belowSource] ?? belowSource;
29812
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29813
+ className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px]",
29625
29814
  children: [
29626
- "Use the root model: inactive — ",
29627
- inactiveText(state, rootModel),
29628
- " The step runs its own model."
29815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29816
+ "data-testid": "setting-source",
29817
+ className: cn("rounded border px-1.5 py-px font-medium", SOURCE_TONE$1[source]),
29818
+ children: sourceText
29819
+ }),
29820
+ withDefault.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29821
+ "data-testid": "setting-levels",
29822
+ className: "text-foreground-subtle",
29823
+ children: withDefault.join(" · ")
29824
+ }),
29825
+ pinned && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29826
+ className: "rounded border border-amber-500/40 px-1.5 py-px text-amber-500",
29827
+ 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.",
29828
+ children: [
29829
+ "pinned at the ",
29830
+ pinnedAt.toLowerCase(),
29831
+ " value"
29832
+ ]
29833
+ }),
29834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29835
+ className: "ml-auto flex items-center gap-2",
29836
+ children: [action, targetStores && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
29837
+ type: "button",
29838
+ disabled,
29839
+ onClick: onReset,
29840
+ className: "rounded px-1 text-foreground-subtle hover:bg-muted hover:text-foreground disabled:opacity-50",
29841
+ children: ["Reset to ", resetTo.toLowerCase()]
29842
+ })]
29843
+ })
29629
29844
  ]
29630
- }) : null;
29631
- if (state.supply === void 0) return inactiveNote;
29632
- const checked = state.intent;
29845
+ });
29846
+ }
29847
+ function LayeredSettingsForm({ addon, fields, layers, target, labels, modelId, onChangeLayer, rootModel, pinnedKeys = [], fieldActions, headerActions, highlightKey = null, disabled = false }) {
29848
+ const boundKeys = (0, _camstack_types.collectModelBoundKeys)(fields);
29849
+ const effective = (0, _camstack_types.effectiveStepSettings)(layers);
29850
+ const targetLayer = layers[target];
29851
+ const step = displayStep(addon, modelId, effective);
29852
+ const rootState = rootSourceState(addon.id, effective, rootModel);
29633
29853
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29634
- className: "space-y-1",
29635
- "data-testid": "root-model-source",
29854
+ className: "space-y-2 text-xs",
29636
29855
  children: [
29637
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29638
- className: "flex items-center gap-2 text-xs text-foreground",
29856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29857
+ className: "flex flex-wrap items-center gap-2",
29639
29858
  children: [
29640
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29641
- type: "checkbox",
29642
- checked,
29643
- disabled,
29644
- onChange: (e) => onChange(e.target.checked)
29859
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
29860
+ className: TEXT_SECTION_LABEL,
29861
+ children: "Settings"
29645
29862
  }),
29646
- "Use the root model (",
29647
- rootModel.name,
29648
- ")"
29863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29864
+ className: TEXT_HINT,
29865
+ children: ["editing: ", labels[target] ?? target]
29866
+ }),
29867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29868
+ className: "ml-auto flex flex-wrap items-center gap-3",
29869
+ children: [headerActions, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29870
+ fields,
29871
+ modelId,
29872
+ settings: targetLayer,
29873
+ disabled,
29874
+ onReset: (next) => onChangeLayer(next)
29875
+ })]
29876
+ })
29649
29877
  ]
29650
29878
  }),
29651
- inactiveNote,
29652
- state.effective && supplyNotes(state.supply).map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29653
- className: "text-[10px] leading-snug text-foreground-subtle",
29654
- children: note
29655
- }, note))
29879
+ rootState !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29880
+ stepId: addon.id,
29881
+ settings: effective,
29882
+ rootModel,
29883
+ disabled,
29884
+ onChange: (v) => onChangeLayer((0, _camstack_types.withFieldFrom)(targetLayer, _camstack_types.USE_ROOT_MODEL_SETTING_KEY, withUseRootModel(effective, v), []))
29885
+ }),
29886
+ fields.map((field) => {
29887
+ if (!isValueField(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29888
+ field,
29889
+ allFields: fields,
29890
+ step,
29891
+ disabled,
29892
+ onChange: () => {}
29893
+ }, field.key);
29894
+ const highlighted = highlightKey === field.key;
29895
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29896
+ id: stepFieldDomId(field.key),
29897
+ "data-field-key": field.key,
29898
+ className: cn("rounded-md border border-transparent px-2 py-1 -mx-2", highlighted && "border-primary/50 bg-primary/5"),
29899
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29900
+ field,
29901
+ allFields: fields,
29902
+ step,
29903
+ disabled,
29904
+ hideModelBoundSourceRow: true,
29905
+ onChange: (next) => onChangeLayer((0, _camstack_types.withFieldFrom)(targetLayer, field.key, next.settings ?? {}, boundKeys))
29906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingProvenance, {
29907
+ field,
29908
+ layers,
29909
+ target,
29910
+ labels,
29911
+ boundKeys,
29912
+ modelId,
29913
+ pinned: pinnedKeys.includes(field.key),
29914
+ disabled,
29915
+ onReset: () => onChangeLayer((0, _camstack_types.withoutField)(targetLayer, field.key, boundKeys)),
29916
+ action: fieldActions?.(field.key, (0, _camstack_types.stepSettingSource)(layers, field.key, boundKeys))
29917
+ })]
29918
+ }, field.key);
29919
+ })
29656
29920
  ]
29657
29921
  });
29658
29922
  }
29659
29923
  //#endregion
29660
29924
  //#region src/composites/agent-step-editor.tsx
29661
29925
  /**
29662
- * Per-(node,device) and per-camera editors must not surface knobs that the
29663
- * cluster already owns. Those fields still exist on the step's catalog
29664
- * schema so the runner can hydrate `child.settings`, but writing them here
29926
+ * A step's node-local fields: the catalog schema minus the knobs the CLUSTER
29927
+ * owns (`clusterStepSettingFieldsFor`). Those still exist on the schema so the
29928
+ * runner can hydrate `child.settings`, but writing them per node or per camera
29665
29929
  * would be a second store the overlay silently ignores.
29666
29930
  */
29667
- function nodeLocalConfigSchema(addon) {
29931
+ function stepLocalFields(addon) {
29668
29932
  const clusterKeys = new Set((0, _camstack_types.clusterStepSettingFieldsFor)(addon.id).map((field) => field.key));
29669
29933
  return (addon.configSchema ?? []).filter((field) => !clusterKeys.has(field.key));
29670
29934
  }
29671
- function ClusterSettingsHint({ addonId }) {
29672
- if ((0, _camstack_types.clusterStepSettingFieldsFor)(addonId).length === 0) return null;
29673
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29674
- className: "text-[10px] leading-snug text-foreground-subtle",
29675
- 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."
29676
- });
29677
- }
29678
29935
  /**
29679
- * The model the step's settings are read and written for — what a model-bound
29936
+ * The model a step's settings are read and written for — what a model-bound
29680
29937
  * field (D738) binds to. While the ROOT supplies the step's fine label (D741),
29681
29938
  * that is the root model: the step's threshold then gates the root's label, on
29682
29939
  * the root's score scale. Otherwise the step's own model: its pin, else the
29683
29940
  * model the row really runs (`effectiveModelId` — the device's hardware-aware
29684
29941
  * default when unpinned), else the step's flat catalog default.
29685
29942
  */
29686
- function settingsModelIdFor(addon, cfg, effectiveModelId, rootModel) {
29687
- if (rootModel !== void 0 && rootSourceState(addon.id, cfg.settings, rootModel)?.effective) return rootModel.modelId;
29688
- return cfg.modelId ?? effectiveModelId ?? addon.defaultModelId;
29943
+ function settingsModelIdFor(addon, modelId, settings, rootModel) {
29944
+ if (rootModel !== void 0 && rootSourceState(addon.id, settings, rootModel)?.effective) return rootModel.modelId;
29945
+ return modelId ?? addon.defaultModelId;
29689
29946
  }
29690
- function buildDisplayStep(addon, cfg, modelId) {
29691
- return {
29692
- addonId: addon.id,
29693
- addonName: addon.name,
29694
- slot: addon.slot,
29695
- inputClasses: [...addon.inputClasses],
29696
- outputClasses: [...addon.outputClasses],
29697
- enabled: cfg.enabled,
29698
- modelId,
29699
- children: [],
29700
- settings: { ...cfg.settings }
29701
- };
29947
+ function ClusterSettingsHint({ addonId }) {
29948
+ if ((0, _camstack_types.clusterStepSettingFieldsFor)(addonId).length === 0) return null;
29949
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29950
+ className: "text-[10px] leading-snug text-foreground-subtle",
29951
+ 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."
29952
+ });
29702
29953
  }
29703
29954
  /**
29704
29955
  * Shown in place of the model picker while the root model supplies the step:
@@ -29719,233 +29970,57 @@ function RootSuppliedModelNote({ rootModel, stepModelId }) {
29719
29970
  ]
29720
29971
  });
29721
29972
  }
29722
- function computeEffective(mode, agentDefault, patch) {
29723
- if (mode !== "device" || !patch) return agentDefault;
29724
- return {
29725
- enabled: agentDefault.enabled,
29726
- modelId: patch.modelId ?? agentDefault.modelId,
29727
- settings: patch.settings ?? agentDefault.settings
29728
- };
29729
- }
29730
- function AgentModeEditor({ addon, agentDefault, modelsForFormat, engineFormat, effectiveModelId, rootModel, onChangeAgentConfig }) {
29973
+ /**
29974
+ * AgentStepEditor — one step on ONE accelerator: its model and its settings,
29975
+ * as a single node-level layer. Model Studio's "settings for this installed
29976
+ * model" dialog uses it. The pipeline pages use the step inspector, which lays
29977
+ * the same parts out across accelerators and layers (D754/D756); the settings
29978
+ * form here is that inspector's `LayeredSettingsForm` with one layer.
29979
+ */
29980
+ function AgentStepEditor({ addon, agentDefault, onChangeAgentConfig, engineFormat, effectiveModelId, rootModel }) {
29981
+ const modelsForFormat = (0, react$1.useMemo)(() => stepModelOptions(addon.models, engineFormat), [addon.models, engineFormat]);
29731
29982
  const update = (patch) => {
29732
29983
  onChangeAgentConfig?.({
29733
29984
  ...agentDefault,
29734
29985
  ...patch
29735
29986
  });
29736
29987
  };
29737
- const localSchema = nodeLocalConfigSchema(addon);
29988
+ const fields = stepLocalFields(addon);
29738
29989
  const formatDefault = addon.defaultModelIdByFormat?.[engineFormat] ?? addon.defaultModelId;
29739
29990
  const rootSupplied = rootSourceState(addon.id, agentDefault.settings, rootModel)?.effective === true;
29991
+ const runningModel = agentDefault.modelId ?? effectiveModelId;
29740
29992
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29741
29993
  className: "space-y-4 text-xs",
29742
29994
  children: [
29743
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29744
- stepId: addon.id,
29745
- settings: agentDefault.settings,
29746
- rootModel,
29747
- onChange: (v) => update({ settings: withUseRootModel(agentDefault.settings, v) })
29748
- }),
29749
29995
  rootSupplied && rootModel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootSuppliedModelNote, {
29750
29996
  rootModel,
29751
- stepModelId: agentDefault.modelId ?? effectiveModelId
29997
+ stepModelId: runningModel
29752
29998
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29753
29999
  className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
29754
30000
  children: "Model"
29755
30001
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29756
30002
  models: modelsForFormat,
29757
- value: agentDefault.modelId ?? effectiveModelId ?? "",
30003
+ value: runningModel ?? "",
29758
30004
  defaultModelId: formatDefault,
29759
30005
  onChange: (modelId) => update({ modelId })
29760
30006
  })] }),
29761
- localSchema.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29762
- className: "pt-2 border-t border-border/50 space-y-3",
29763
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29764
- className: "flex items-center justify-between",
29765
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29766
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29767
- children: "Settings"
29768
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29769
- fields: localSchema,
29770
- modelId: settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel),
29771
- settings: agentDefault.settings,
29772
- onReset: (settings) => update({ settings })
29773
- })]
29774
- }), localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29775
- field,
29776
- allFields: localSchema,
29777
- step: buildDisplayStep(addon, agentDefault, settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel)),
29778
- disabled: false,
29779
- onChange: (next) => update({ settings: { ...next.settings } })
29780
- }, field.key))]
30007
+ (fields.length > 0 || rootSourceState(addon.id, agentDefault.settings, rootModel)) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30008
+ className: "pt-2 border-t border-border/50",
30009
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayeredSettingsForm, {
30010
+ addon,
30011
+ fields,
30012
+ layers: { node: agentDefault.settings },
30013
+ target: "node",
30014
+ labels: { node: "This node" },
30015
+ modelId: settingsModelIdFor(addon, runningModel, agentDefault.settings, rootModel),
30016
+ ...rootModel !== void 0 ? { rootModel } : {},
30017
+ onChangeLayer: (settings) => update({ settings })
30018
+ })
29781
30019
  }),
29782
30020
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterSettingsHint, { addonId: addon.id })
29783
30021
  ]
29784
30022
  });
29785
30023
  }
29786
- function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, modelsForFormat, engineFormat, effectiveModelId, rootModel, onChangePatch }) {
29787
- const effective = computeEffective("device", agentDefault, currentPatch);
29788
- const inheritedModelId = agentDefault.modelId ?? effectiveModelId;
29789
- const formatDefault = addon.defaultModelIdByFormat?.[engineFormat] ?? addon.defaultModelId;
29790
- const commitPatch = (next) => {
29791
- const hasAnyField = next.modelId !== void 0 || next.settings !== void 0;
29792
- onChangePatch?.(hasAnyField ? next : null);
29793
- };
29794
- const setModel = (modelId) => {
29795
- const { modelId: _prev, ...rest } = currentPatch ?? {};
29796
- commitPatch(modelId === void 0 ? rest : {
29797
- ...rest,
29798
- modelId
29799
- });
29800
- };
29801
- const setSettings = (settings) => {
29802
- const { settings: _prev, ...rest } = currentPatch ?? {};
29803
- commitPatch(settings === void 0 ? rest : {
29804
- ...rest,
29805
- settings
29806
- });
29807
- };
29808
- const modelOverridden = currentPatch?.modelId !== void 0;
29809
- const localSchema = nodeLocalConfigSchema(addon);
29810
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29811
- className: "space-y-4 text-xs",
29812
- children: [
29813
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29814
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29815
- children: [
29816
- "Provisioning for ",
29817
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29818
- className: "font-semibold text-foreground",
29819
- children: agentNodeId
29820
- }),
29821
- " — model + settings only. Whether this camera runs the step is set by its per-camera gate above."
29822
- ]
29823
- }),
29824
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29825
- stepId: addon.id,
29826
- settings: effective.settings,
29827
- rootModel,
29828
- onChange: (v) => setSettings(withUseRootModel(effective.settings, v))
29829
- }),
29830
- rootSourceState(addon.id, effective.settings, rootModel)?.effective === true && rootModel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootSuppliedModelNote, {
29831
- rootModel,
29832
- stepModelId: effective.modelId ?? inheritedModelId
29833
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
29834
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29835
- className: "flex items-center justify-between mb-1",
29836
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29837
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29838
- children: "Model"
29839
- }), modelOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29840
- type: "button",
29841
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29842
- onClick: () => setModel(void 0),
29843
- children: "Clear"
29844
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29845
- type: "button",
29846
- className: "text-[10px] text-primary hover:underline",
29847
- onClick: () => setModel(inheritedModelId),
29848
- children: "Override"
29849
- })]
29850
- }),
29851
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29852
- models: modelsForFormat,
29853
- disabled: !modelOverridden,
29854
- value: (modelOverridden ? currentPatch?.modelId ?? inheritedModelId : inheritedModelId) ?? "",
29855
- defaultModelId: formatDefault,
29856
- onChange: (modelId) => setModel(modelId)
29857
- }),
29858
- !modelOverridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29859
- className: "text-[10px] text-foreground-subtle mt-1",
29860
- children: ["inherits from ", agentNodeId]
29861
- })
29862
- ] }),
29863
- localSchema.length > 0 && (() => {
29864
- const settingsOverridden = currentPatch?.settings !== void 0;
29865
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29866
- className: "pt-2 border-t border-border/50 space-y-3",
29867
- children: [
29868
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29869
- className: "flex items-center justify-between",
29870
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29871
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29872
- children: "Settings"
29873
- }), settingsOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29874
- className: "flex items-center gap-3",
29875
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29876
- fields: localSchema,
29877
- modelId: settingsModelIdFor(addon, effective, inheritedModelId, rootModel),
29878
- settings: currentPatch?.settings,
29879
- onReset: (settings) => setSettings(settings)
29880
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29881
- type: "button",
29882
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29883
- onClick: () => setSettings(void 0),
29884
- children: "Clear"
29885
- })]
29886
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29887
- type: "button",
29888
- className: "text-[10px] text-primary hover:underline",
29889
- onClick: () => setSettings({ ...agentDefault.settings }),
29890
- children: "Override"
29891
- })]
29892
- }),
29893
- localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29894
- field,
29895
- allFields: localSchema,
29896
- step: buildDisplayStep(addon, effective, settingsModelIdFor(addon, effective, inheritedModelId, rootModel)),
29897
- disabled: !settingsOverridden,
29898
- onChange: (next) => setSettings({ ...next.settings })
29899
- }, field.key)),
29900
- !settingsOverridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29901
- className: "text-[10px] text-foreground-subtle",
29902
- children: ["inherits from ", agentNodeId]
29903
- })
29904
- ]
29905
- });
29906
- })(),
29907
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterSettingsHint, { addonId: addon.id }),
29908
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29909
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29910
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29911
- className: "font-semibold text-foreground mb-0.5",
29912
- children: "Effective provisioning preview"
29913
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29914
- className: "font-mono truncate",
29915
- children: [
29916
- addon.id,
29917
- " · model: ",
29918
- effective.modelId ?? inheritedModelId ?? "Auto"
29919
- ]
29920
- })]
29921
- })
29922
- ]
29923
- });
29924
- }
29925
- function AgentStepEditor(props) {
29926
- const { mode, addon, agentDefault, agentNodeId, currentPatch, onChangeAgentConfig, onChangePatch, engineFormat, effectiveModelId, rootModel } = props;
29927
- const modelsForFormat = (0, react$1.useMemo)(() => stepModelOptions(addon.models, engineFormat), [addon.models, engineFormat]);
29928
- if (mode === "agent") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentModeEditor, {
29929
- addon,
29930
- agentDefault,
29931
- modelsForFormat,
29932
- engineFormat,
29933
- effectiveModelId,
29934
- ...rootModel !== void 0 ? { rootModel } : {},
29935
- onChangeAgentConfig
29936
- });
29937
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceModeEditor, {
29938
- addon,
29939
- agentDefault,
29940
- agentNodeId,
29941
- currentPatch,
29942
- modelsForFormat,
29943
- engineFormat,
29944
- effectiveModelId,
29945
- ...rootModel !== void 0 ? { rootModel } : {},
29946
- onChangePatch
29947
- });
29948
- }
29949
30024
  //#endregion
29950
30025
  //#region src/primitives/icon-button.tsx
29951
30026
  var iconButtonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
@@ -33860,117 +33935,6 @@ async function waitForIceGathering(pc) {
33860
33935
  });
33861
33936
  }
33862
33937
  //#endregion
33863
- //#region src/lib/responsive.ts
33864
- /**
33865
- * Responsive design tokens — three-tier system.
33866
- *
33867
- * The admin-ui targets three width tiers; every component should pick
33868
- * the right token from this file instead of hand-coding breakpoints.
33869
- * Adopting the tokens uniformly is what makes the app feel consistent
33870
- * across operator devices (mobile / tablet / desktop) and what unlocks
33871
- * targeted tuning (e.g. shrink every label by 1px in one place).
33872
- *
33873
- * Tier definitions (Tailwind breakpoints):
33874
- *
33875
- * • **L1 — Compact** (`< 640px`, default = mobile portrait)
33876
- * Single column, tight padding, smallest text, icon-only buttons
33877
- * where labels can be inferred. The operator can still see every
33878
- * stat / action — no horizontal scroll, no clipped text. The user
33879
- * directive: "molta informazione in poco spazio quando la width è
33880
- * bassa." [a lot of information in little space when the width is low.]
33881
- *
33882
- * • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
33883
- * 2-column grids, side panels become visible, button labels return.
33884
- * Targets ~768px–1024px viewports — the most common operator sizes.
33885
- *
33886
- * • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
33887
- * 4-column grids, generous padding, full-width header text,
33888
- * side-by-side composite layouts. The operator has elbow room.
33889
- *
33890
- * Conventions:
33891
- * - **Always start from L1** — the bare class is always the compact
33892
- * variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
33893
- * desktop-first breakpoints; no max-width queries.
33894
- * - **Token strings are static** — kept as `as const` so callers can
33895
- * compose them via `cn()` / template literals without running a
33896
- * class generator at runtime.
33897
- * - **Pair the token with a comment** — the comment in the file
33898
- * names every breakpoint it crosses, so a reader can predict the
33899
- * layout without going to Tailwind docs.
33900
- */
33901
- /** Container padding for a card-style section (header + body). */
33902
- var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
33903
- /** Header strip inside a card (between border-b lines). */
33904
- var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
33905
- /** Body padding for a card-style section. */
33906
- var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
33907
- /** Gap between cards in a vertical stack. */
33908
- var STACK_GAP = "space-y-3 sm:space-y-4";
33909
- /** Gap between cards in a grid (4 cells / row at lg). */
33910
- var GRID_GAP = "gap-2 sm:gap-3";
33911
- /** Row padding inside a divided list (e.g. InfoRow). */
33912
- var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
33913
- /** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
33914
- var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
33915
- /** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
33916
- var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
33917
- /** Side-by-side layout that stacks below md (canvas + side panel). */
33918
- var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
33919
- /** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
33920
- 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";
33921
- /**
33922
- * The tier boundary at which a settings label/value row stops stacking.
33923
- *
33924
- * Below it (L1 — compact) the row is ONE column: label on its own line, value
33925
- * on the next at full width. Above it the row is label-left / value-right as
33926
- * before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
33927
- * value beside it; a phone has neither, and the failure mode is not "cramped"
33928
- * but "the value is the part that gets cut" — the one thing the operator
33929
- * opened the page to read.
33930
- */
33931
- var SETTING_ROW_STACK_BREAKPOINT = "sm";
33932
- /** Outer row: single column at L1, label/value columns from `sm` up. */
33933
- var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
33934
- /** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
33935
- var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
33936
- /**
33937
- * Value lane — the value text plus its trailing affordances (copy / reveal).
33938
- * `w-full` at L1 so the value owns the whole row once the label is above it;
33939
- * `sm:flex-1` makes it share the row again from `sm` up.
33940
- */
33941
- var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
33942
- /**
33943
- * The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
33944
- * at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
33945
- * desktop form, where the row is wide enough for truncation to be a choice
33946
- * rather than data loss.
33947
- */
33948
- var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
33949
- /** Section header label (uppercase tracking-wider). */
33950
- var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
33951
- /** Field label inside a row. */
33952
- var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
33953
- /** Sub-label / hint text. */
33954
- var TEXT_HINT = "text-[10px] text-foreground-subtle";
33955
- /** Primary value (a metric, badge, …). */
33956
- var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
33957
- /** Big metric value (StatCard). */
33958
- var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
33959
- /** Default compact button. */
33960
- 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";
33961
- /** Primary compact button. */
33962
- 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";
33963
- /** Danger compact button. */
33964
- 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";
33965
- /** Warning compact button (e.g. cancel-drawing). */
33966
- 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";
33967
- /** Compact text/select/number input. */
33968
- 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";
33969
- /** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
33970
- var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
33971
- var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
33972
- var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
33973
- //#endregion
33974
33938
  //#region src/composites/data-table-layout.ts
33975
33939
  /**
33976
33940
  * At this many columns and above, `auto` switches a narrow viewport to cards.
@@ -55563,16 +55527,16 @@ function cellButtonClass(state, isSelected, extra) {
55563
55527
  //#endregion
55564
55528
  //#region src/composites/device-step-matrix.tsx
55565
55529
  /**
55566
- * DeviceStepMatrix — the per-camera "Detection pipeline → Steps" surface (#33)
55567
- * rendered as ONE hierarchical steps×nodes matrix.
55530
+ * DeviceStepMatrix — the READ-ONLY "Compare" view of the step inspector
55531
+ * (D756): one hierarchical steps × accelerators matrix showing what every
55532
+ * accelerator runs for every step, side by side.
55568
55533
  *
55569
- * Column 1 (STICKY): the indented step tree. Each step carries its slot +
55570
- * name + class-flow chips PLUS a per-camera, node-independent GATE control
55571
- * with THREE states — On / Off / Default (inherit). `package-detection` is
55572
- * zone-driven, so it shows a "Zones" badge instead of a gate.
55573
- * Columns 2..N (one per online node): the provisioned model chip (click →
55574
- * the model/config picker), a clickable "⚠ skip" when the step is gated ON
55575
- * but the node has no compatible model, or "—" when N/A.
55534
+ * Column 1 (STICKY): the indented step tree (slot + name + class-flow chips).
55535
+ * Columns 2..N (one per accelerator): the model chip, "⚠ skip" when the step
55536
+ * runs on the camera but the accelerator has no compatible model, or "—".
55537
+ * A click selects that (step, accelerator) in the inspector — the matrix
55538
+ * itself edits nothing (it used to host a slide-over editor that scrolled
55539
+ * away with the page).
55576
55540
  *
55577
55541
  * Responsive, container-measured (never a global media query, so it also works
55578
55542
  * embedded):
@@ -55583,77 +55547,20 @@ function cellButtonClass(state, isSelected, extra) {
55583
55547
  * single-node view: a node selector picks ONE node and the matrix renders
55584
55548
  * `step-tree × that-one-node`.
55585
55549
  */
55586
- /** Wider than the cluster matrix's 18rem: col-1 also hosts the gate control. */
55587
- var STEP_COL_REM$1 = 21;
55588
- var STEP_COL_PX = STEP_COL_REM$1 * 16;
55550
+ /** The sticky step column. */
55551
+ var STEP_COL_REM = 18;
55552
+ var STEP_COL_PX = STEP_COL_REM * 16;
55589
55553
  /**
55590
- * Narrow (mobile) step-column width. On a phone the wide 21rem column would
55591
- * occlude nearly the whole viewport, so the single-node layout pins a slimmer
55592
- * sticky step column and lets the device columns scroll horizontally beside it
55593
- * (both fully readable) rather than cramming every column into the viewport.
55554
+ * Narrow (mobile) step-column width. On a phone the wide column would occlude
55555
+ * nearly the whole viewport, so the single-node layout pins a slimmer sticky
55556
+ * step column and lets the device columns scroll horizontally beside it.
55594
55557
  */
55595
55558
  var STEP_COL_SINGLE_REM = 11;
55596
- function activeChoice(gate) {
55597
- if (!gate.explicit) return "default";
55598
- return gate.effectiveOn ? "on" : "off";
55599
- }
55600
- /**
55601
- * Tri-state segmented control. `Default` inherits the catalog value (shown in
55602
- * the caption); `On`/`Off` pin an explicit per-camera override. Touch-friendly
55603
- * 28px segments.
55604
- */
55605
- function GateControl({ gate, busy, onSet }) {
55606
- const active = activeChoice(gate);
55607
- const seg = (choice, label, value, activeClass) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55608
- type: "button",
55609
- "aria-pressed": active === choice,
55610
- disabled: busy,
55611
- onClick: () => onSet(value),
55612
- 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"),
55613
- children: label
55614
- });
55615
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55616
- className: "flex flex-col gap-1",
55617
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55618
- className: "inline-flex w-fit max-w-full self-start overflow-hidden rounded-md border border-border",
55619
- children: [
55620
- seg("default", "Auto", null, "bg-primary/15 text-primary"),
55621
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
55622
- seg("on", "On", true, "bg-emerald-500 text-white"),
55623
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
55624
- seg("off", "Off", false, "bg-foreground-subtle/70 text-white")
55625
- ]
55626
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55627
- className: "text-[10px] leading-none text-foreground-subtle",
55628
- children: active === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
55629
- "= default (",
55630
- gate.defaultOn ? "on" : "off",
55631
- ")"
55632
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: ["overridden · default ", gate.defaultOn ? "on" : "off"] })
55633
- })]
55634
- });
55635
- }
55636
- function ZoneBadge() {
55637
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55638
- className: "flex flex-col gap-1",
55639
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55640
- 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",
55641
- children: "Zones"
55642
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55643
- className: "text-[10px] leading-none text-foreground-subtle",
55644
- children: "driven by package zones"
55645
- })]
55646
- });
55647
- }
55648
- function StepGateCell({ node, depth, gate, busy, onSet, hideGate }) {
55649
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55650
- 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",
55559
+ function StepCell({ node, depth }) {
55560
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55561
+ className: "sticky left-0 z-10 flex flex-col gap-2 border-b border-border bg-surface px-3 py-2 text-xs min-w-0",
55651
55562
  style: { paddingLeft: `${12 + depth * 14}px` },
55652
- 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, {
55653
- gate,
55654
- busy,
55655
- onSet
55656
- })]
55563
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node })
55657
55564
  });
55658
55565
  }
55659
55566
  /**
@@ -55692,18 +55599,18 @@ function NodeSelectorBar({ nodes, selected, onSelect }) {
55692
55599
  ]
55693
55600
  });
55694
55601
  }
55695
- function MatrixGrid$1({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single, hideGate }) {
55602
+ function MatrixGrid({ tree, agents, getCellState, onCellClick, selectedCell, single }) {
55696
55603
  const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
55697
55604
  const groups = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
55698
55605
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55699
55606
  className: "overflow-auto rounded border border-border",
55700
55607
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55701
55608
  className: "grid w-max",
55702
- style: { gridTemplateColumns: `${single ? STEP_COL_SINGLE_REM : STEP_COL_REM$1}rem repeat(${agents.length}, 13rem)` },
55609
+ style: { gridTemplateColumns: `${single ? STEP_COL_SINGLE_REM : STEP_COL_REM}rem repeat(${agents.length}, 13rem)` },
55703
55610
  children: [
55704
55611
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55705
55612
  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",
55706
- children: hideGate ? "Step" : "Step · gate"
55613
+ children: "Step"
55707
55614
  }),
55708
55615
  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", {
55709
55616
  className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2",
@@ -55719,26 +55626,18 @@ function MatrixGrid$1({ tree, agents, gateFor, getCellState, onCellClick, select
55719
55626
  node,
55720
55627
  depth,
55721
55628
  agents,
55722
- gate: gateFor(node.addonId),
55723
55629
  getCellState,
55724
55630
  onCellClick,
55725
- selectedCell,
55726
- onSetGate,
55727
- gateBusy,
55728
- hideGate
55631
+ selectedCell
55729
55632
  }, node.addonId))
55730
55633
  ]
55731
55634
  })
55732
55635
  });
55733
55636
  }
55734
- function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, hideGate }) {
55735
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepGateCell, {
55637
+ function MatrixRow({ node, depth, agents, getCellState, onCellClick, selectedCell }) {
55638
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepCell, {
55736
55639
  node,
55737
- depth,
55738
- gate,
55739
- busy: gateBusy,
55740
- onSet: (next) => onSetGate(node.addonId, next),
55741
- hideGate
55640
+ depth
55742
55641
  }), agents.map((a) => {
55743
55642
  const state = getCellState(node.addonId, a);
55744
55643
  const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
@@ -55752,7 +55651,7 @@ function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selec
55752
55651
  }, agentColumnKey(a));
55753
55652
  })] });
55754
55653
  }
55755
- function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange, hideGate }) {
55654
+ function DeviceStepMatrix({ tree, agents, getCellState, onCellClick, selectedCell, fallbackNodeId, onFallbackNodeChange }) {
55756
55655
  const containerRef = (0, react$1.useRef)(null);
55757
55656
  const [width, setWidth] = (0, react$1.useState)(0);
55758
55657
  (0, react$1.useLayoutEffect)(() => {
@@ -55778,17 +55677,13 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
55778
55677
  nodes,
55779
55678
  selected: selectedNode.agentNodeId,
55780
55679
  onSelect: onFallbackNodeChange
55781
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid$1, {
55680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
55782
55681
  tree,
55783
55682
  agents: shownAgents,
55784
- gateFor,
55785
55683
  getCellState,
55786
55684
  onCellClick,
55787
55685
  selectedCell,
55788
- onSetGate,
55789
- gateBusy,
55790
- single: single && selectedNode !== null,
55791
- hideGate
55686
+ single: single && selectedNode !== null
55792
55687
  })]
55793
55688
  });
55794
55689
  }
@@ -56818,17 +56713,7 @@ function PlaceholderStep({ addon, onClick }) {
56818
56713
  })
56819
56714
  });
56820
56715
  }
56821
- 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 }) {
56822
- const stepExtraProps = (addonId) => ({
56823
- toggleMode,
56824
- hideModelAndSettings,
56825
- allowAutoModel,
56826
- ...toggleMode === "threeState" ? {
56827
- overrideState: overrides[addonId] !== void 0 ? overrides[addonId] : null,
56828
- onOverrideChange: onOverrideChange ? (next) => onOverrideChange(addonId, next) : void 0,
56829
- inheritedEnabled: inheritedEnabledByAddon[addonId]
56830
- } : {}
56831
- });
56716
+ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateId, onSelectTemplate, onSaveTemplate, onUpdateTemplate, onDeleteTemplate, readOnly = false, excludeAddons = [], hideTemplates = false, showOnlyEnabled = false, showSlotLabels = false }) {
56832
56717
  const excluded = (0, react$1.useMemo)(() => new Set(excludeAddons), [excludeAddons]);
56833
56718
  const schemaMap = (0, react$1.useMemo)(() => buildSchemaMap(schema), [schema]);
56834
56719
  const [warnings, setWarnings] = (0, react$1.useState)([]);
@@ -56935,8 +56820,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56935
56820
  allSchemas: schemaMap,
56936
56821
  onChange: handleStepChange,
56937
56822
  onDelete: readOnly ? void 0 : (id) => onChange(steps.filter((s) => s.addonId !== id)),
56938
- readOnly,
56939
- ...stepExtraProps(step.addonId)
56823
+ readOnly
56940
56824
  }), (step.children.length > 0 || childPlaceholders.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56941
56825
  className: "ml-4 pl-3 border-l border-border/60 space-y-1.5",
56942
56826
  children: [
@@ -56965,8 +56849,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56965
56849
  children: step.children.filter((c) => c.addonId !== id)
56966
56850
  });
56967
56851
  },
56968
- readOnly,
56969
- ...stepExtraProps(child.addonId)
56852
+ readOnly
56970
56853
  }), (child.children.length > 0 || childChildPlaceholders.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56971
56854
  className: "ml-4 pl-3 border-l border-border/40 space-y-1.5",
56972
56855
  children: [
@@ -56997,8 +56880,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56997
56880
  } : c)
56998
56881
  });
56999
56882
  },
57000
- readOnly,
57001
- ...stepExtraProps(grandchild.addonId)
56883
+ readOnly
57002
56884
  }, grandchild.addonId)),
57003
56885
  !readOnly && childChildPlaceholders.map((addon) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlaceholderStep, {
57004
56886
  addon,
@@ -57158,179 +57040,6 @@ function PipelineRuntimeSelector({ options, value, onChange }) {
57158
57040
  });
57159
57041
  }
57160
57042
  //#endregion
57161
- //#region src/composites/pipeline-tree-matrix.tsx
57162
- var STEP_COL_REM = 18;
57163
- function QuickToggle({ enabled, onChange, disabled }) {
57164
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57165
- type: "button",
57166
- role: "switch",
57167
- "aria-checked": enabled,
57168
- disabled,
57169
- onClick: (e) => {
57170
- e.stopPropagation();
57171
- onChange(!enabled);
57172
- },
57173
- 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"),
57174
- title: enabled ? "Click to disable" : "Click to enable",
57175
- 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") })
57176
- });
57177
- }
57178
- function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell, toggleProps }) {
57179
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57180
- 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",
57181
- style: { paddingLeft: `${12 + depth * 14}px` },
57182
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), toggleProps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57183
- className: "pl-2 pt-0.5 shrink-0",
57184
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickToggle, {
57185
- enabled: toggleProps.enabled,
57186
- onChange: toggleProps.onChange,
57187
- disabled: toggleProps.disabled
57188
- })
57189
- })]
57190
- }), agents.map((a) => {
57191
- const state = getCellState(node.addonId, a);
57192
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57193
- type: "button",
57194
- onClick: () => onCellClick(node.addonId, a),
57195
- 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"),
57196
- children: renderCellContent(state)
57197
- }, agentColumnKey(a));
57198
- })] });
57199
- }
57200
- /**
57201
- * Wide layout: horizontally self-scrolling matrix. The scroll lives INSIDE
57202
- * this `overflow-auto` box (sticky first column + sticky header row) so the
57203
- * page body never scrolls sideways. Column tracks are fixed px widths so the
57204
- * grid is static regardless of content.
57205
- */
57206
- function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57207
- const gridTemplate = `${STEP_COL_REM}rem repeat(${agents.length}, 13rem)`;
57208
- const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
57209
- const onlyAgent = agents[0];
57210
- const groups = groupAgentColumns(agents);
57211
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57212
- className: "overflow-auto border border-border rounded",
57213
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57214
- className: "grid w-max",
57215
- style: { gridTemplateColumns: gridTemplate },
57216
- children: [
57217
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57218
- 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",
57219
- children: "Step"
57220
- }),
57221
- 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", {
57222
- className: "sticky top-0 z-10 min-w-0 bg-muted/60 px-3 py-2 border-b border-l border-border",
57223
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57224
- className: "text-xs font-semibold text-foreground truncate",
57225
- children: col.agentNodeId
57226
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57227
- className: "text-[10px] text-foreground-subtle truncate",
57228
- children: col.engineLabel
57229
- })]
57230
- }, agentColumnKey(col)))),
57231
- rows.map(({ node, depth }) => {
57232
- const cellState = showToggle && onlyAgent !== void 0 ? getCellState(node.addonId, onlyAgent) : null;
57233
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridRow, {
57234
- node,
57235
- depth,
57236
- agents,
57237
- getCellState,
57238
- onCellClick,
57239
- selectedCell,
57240
- toggleProps: showToggle && cellState !== null ? {
57241
- enabled: cellState.kind === "enabled",
57242
- onChange: (next) => onToggleEnabled?.(node.addonId, next),
57243
- disabled: cellState.kind === "na"
57244
- } : void 0
57245
- }, node.addonId);
57246
- })
57247
- ]
57248
- })
57249
- });
57250
- }
57251
- /**
57252
- * Narrow layout: one card per agent column, each listing every step stacked
57253
- * vertically. Feature-parity with the matrix — same `onCellClick`, same
57254
- * `renderCellContent`, same single-agent `onToggleEnabled`.
57255
- */
57256
- function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57257
- const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
57258
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57259
- className: "space-y-3",
57260
- children: agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57261
- className: "border border-border rounded overflow-hidden",
57262
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57263
- className: "bg-muted/60 px-3 py-2 border-b border-border",
57264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57265
- className: "text-xs font-semibold text-foreground truncate",
57266
- children: a.deviceLabel ? `${a.agentNodeId} · ${a.deviceLabel}` : a.agentNodeId
57267
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57268
- className: "text-[10px] text-foreground-subtle truncate",
57269
- children: a.engineLabel
57270
- })]
57271
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: rows.map(({ node, depth }) => {
57272
- const state = getCellState(node.addonId, a);
57273
- const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
57274
- const toggleProps = showToggle && state !== null ? {
57275
- enabled: state.kind === "enabled",
57276
- onChange: (next) => onToggleEnabled?.(node.addonId, next),
57277
- disabled: state.kind === "na"
57278
- } : void 0;
57279
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
57280
- className: "border-b border-border last:border-b-0",
57281
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57282
- className: "flex items-center gap-2",
57283
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57284
- type: "button",
57285
- onClick: () => onCellClick(node.addonId, a),
57286
- className: cellButtonClass(state, isSelected, "flex flex-1 min-w-0 items-start justify-between gap-3 px-3 py-2"),
57287
- style: { paddingLeft: `${12 + depth * 14}px` },
57288
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57289
- className: "shrink-0 max-w-[45%] pt-0.5",
57290
- children: renderCellContent(state)
57291
- })]
57292
- }), toggleProps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57293
- className: "pr-3 shrink-0",
57294
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickToggle, {
57295
- enabled: toggleProps.enabled,
57296
- onChange: toggleProps.onChange,
57297
- disabled: toggleProps.disabled
57298
- })
57299
- })]
57300
- })
57301
- }, node.addonId);
57302
- }) })]
57303
- }, agentColumnKey(a)))
57304
- });
57305
- }
57306
- function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57307
- const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
57308
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57309
- className: "@container/matrix w-full",
57310
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57311
- className: "@2xl/matrix:hidden",
57312
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StackedCards, {
57313
- rows,
57314
- agents,
57315
- getCellState,
57316
- onCellClick,
57317
- selectedCell,
57318
- onToggleEnabled
57319
- })
57320
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57321
- className: "hidden @2xl/matrix:block",
57322
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
57323
- rows,
57324
- agents,
57325
- getCellState,
57326
- onCellClick,
57327
- selectedCell,
57328
- onToggleEnabled
57329
- })
57330
- })]
57331
- });
57332
- }
57333
- //#endregion
57334
57043
  //#region src/composites/provider-badge.tsx
57335
57044
  var providerConfig = {
57336
57045
  onvif: {
@@ -57774,89 +57483,6 @@ function validateSelector(selector) {
57774
57483
  }
57775
57484
  }
57776
57485
  //#endregion
57777
- //#region src/composites/slide-over-panel.tsx
57778
- /**
57779
- * SlideOverPanel — a detail surface anchored to context on desktop and to the
57780
- * viewport on mobile.
57781
- *
57782
- * - **Desktop (`md+`):** an in-container, NON-modal slide-over pinned to the
57783
- * right edge of the nearest positioned ancestor. The host reserves space with
57784
- * a matching `md:pr-*` so the panel sits beside its content (e.g. the
57785
- * multi-node pipeline matrix), not over it.
57786
- * - **Mobile (`< md`):** a MODAL bottom sheet pinned to the viewport
57787
- * (`position: fixed`). It never derives its size from the surrounding
57788
- * container, so it can't stretch across a horizontally-laid-out node matrix
57789
- * and overflow the page — the previous behaviour when it was a fixed-width
57790
- * `absolute` panel anchored to the wide matrix wrapper.
57791
- *
57792
- * The whole desktop⇄mobile transform is pure Tailwind responsive classes on one
57793
- * element — no layout-shift on hydration, no portal. JS is used only to lock
57794
- * body scroll behind the mobile sheet and to wire Escape-to-close.
57795
- */
57796
- function SlideOverPanel({ open, onClose, title, subtitle, footer, children, widthClass = "md:w-[360px]" }) {
57797
- const isMobile = useIsMobile();
57798
- (0, react$1.useEffect)(() => {
57799
- if (!open) return;
57800
- const onEsc = (e) => {
57801
- if (e.key === "Escape") onClose();
57802
- };
57803
- window.addEventListener("keydown", onEsc);
57804
- return () => window.removeEventListener("keydown", onEsc);
57805
- }, [open, onClose]);
57806
- (0, react$1.useEffect)(() => {
57807
- if (!open || !isMobile) return;
57808
- const previous = document.body.style.overflow;
57809
- document.body.style.overflow = "hidden";
57810
- return () => {
57811
- document.body.style.overflow = previous;
57812
- };
57813
- }, [open, isMobile]);
57814
- if (!open) return null;
57815
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57816
- className: "fixed inset-0 z-40 bg-black/50 md:hidden",
57817
- onClick: onClose,
57818
- "aria-hidden": "true"
57819
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
57820
- 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),
57821
- role: "dialog",
57822
- "aria-modal": isMobile ? "true" : "false",
57823
- children: [
57824
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57825
- className: "flex shrink-0 justify-center pt-2 pb-1 md:hidden",
57826
- "aria-hidden": "true",
57827
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-1 w-9 rounded-full bg-foreground-subtle/30" })
57828
- }),
57829
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
57830
- className: "flex shrink-0 items-start justify-between gap-2 px-4 py-3 border-b border-border",
57831
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57832
- className: "min-w-0",
57833
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57834
- className: "text-sm font-semibold text-foreground truncate",
57835
- children: title
57836
- }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57837
- className: "text-[11px] text-foreground-subtle mt-0.5 truncate",
57838
- children: subtitle
57839
- })]
57840
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57841
- type: "button",
57842
- onClick: onClose,
57843
- className: "p-1 rounded hover:bg-muted text-foreground-subtle hover:text-foreground",
57844
- "aria-label": "Close panel",
57845
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "w-4 h-4" })
57846
- })]
57847
- }),
57848
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57849
- className: "flex-1 min-h-0 overflow-y-auto overscroll-contain px-4 py-3",
57850
- children
57851
- }),
57852
- footer !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
57853
- className: "flex shrink-0 items-center gap-2 justify-end px-4 py-3 border-t border-border",
57854
- children: footer
57855
- })
57856
- ]
57857
- })] });
57858
- }
57859
- //#endregion
57860
57486
  //#region src/composites/snapshot-button.tsx
57861
57487
  /**
57862
57488
  * SnapshotButton — operator-facing manual snapshot trigger.
@@ -58042,51 +57668,725 @@ function StepTimings({ timings, totalMs, className }) {
58042
57668
  });
58043
57669
  }
58044
57670
  //#endregion
58045
- //#region src/composites/step-tree-master.tsx
58046
- var DOT_COLOR = {
58047
- inherit: "bg-muted",
58048
- on: "bg-emerald-500",
58049
- off: "bg-orange-500",
58050
- na: "bg-gray-300 opacity-50"
58051
- };
58052
- function TreeRow({ node, depth, selectedAddonId, onSelect, agentDots }) {
58053
- const isSelected = node.addonId === selectedAddonId;
58054
- const dots = agentDots?.[node.addonId] ?? [];
58055
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57671
+ //#region src/composites/step-inspector/accelerator-tabs.tsx
57672
+ function AcceleratorTabs({ items, selectedId, onSelect }) {
57673
+ const onKeyDown = (e, index) => {
57674
+ const delta = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
57675
+ if (delta === 0) return;
57676
+ e.preventDefault();
57677
+ const next = items[(index + delta + items.length) % items.length];
57678
+ if (next !== void 0) onSelect(next.id);
57679
+ };
57680
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57681
+ role: "tablist",
57682
+ "aria-label": "Accelerator",
57683
+ className: "-mx-1 flex min-w-0 flex-nowrap gap-1 overflow-x-auto px-1 pb-0.5",
57684
+ children: items.map((item, index) => {
57685
+ const selected = item.id === selectedId;
57686
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57687
+ type: "button",
57688
+ role: "tab",
57689
+ "aria-selected": selected,
57690
+ "aria-label": item.marked ? `${item.label} (camera override)` : item.label,
57691
+ tabIndex: selected ? 0 : -1,
57692
+ onClick: () => onSelect(item.id),
57693
+ onKeyDown: (e) => onKeyDown(e, index),
57694
+ 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"),
57695
+ children: [item.label, item.marked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57696
+ className: "h-1.5 w-1.5 rounded-full bg-amber-500",
57697
+ "aria-hidden": true
57698
+ })]
57699
+ }, item.id);
57700
+ })
57701
+ });
57702
+ }
57703
+ //#endregion
57704
+ //#region src/composites/step-inspector/gate-control.tsx
57705
+ /**
57706
+ * GateControl — whether ONE camera runs a step: Auto (the catalog default) /
57707
+ * On / Off. A per-camera, node-independent switch, written at once (it is not
57708
+ * part of the inspector's draft). `package-detection` is driven by package
57709
+ * zones and shows a badge instead.
57710
+ *
57711
+ * Segments are 36 px tall on a phone and 32 px from `md` up — the matrix this
57712
+ * replaced drew 26 px ones.
57713
+ */
57714
+ function activeChoice(gate) {
57715
+ if (!gate.explicit) return "auto";
57716
+ return gate.effectiveOn ? "on" : "off";
57717
+ }
57718
+ function GateControl({ gate, busy = false, onSet }) {
57719
+ if (gate.kind === "zone-driven") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57720
+ 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",
57721
+ title: "Runs where this camera has package zones — set on the Zones tab",
57722
+ children: "Zones"
57723
+ });
57724
+ const active = activeChoice(gate);
57725
+ const segment = (choice, label, value, on) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58056
57726
  type: "button",
58057
- onClick: () => onSelect(node.addonId),
58058
- 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"),
58059
- style: { paddingLeft: `${12 + depth * 14}px` },
58060
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58061
- className: "truncate",
58062
- children: node.addonName
58063
- }), dots.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58064
- className: "flex items-center gap-1 shrink-0",
58065
- children: dots.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58066
- title: `${d.agentNodeId}: ${d.state}`,
58067
- className: cn("w-2 h-2 rounded-full", DOT_COLOR[d.state])
58068
- }, d.agentNodeId))
57727
+ role: "radio",
57728
+ "aria-checked": active === choice,
57729
+ disabled: busy,
57730
+ onClick: () => onSet(value),
57731
+ 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"),
57732
+ children: label
57733
+ });
57734
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57735
+ className: "flex items-center gap-2",
57736
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57737
+ role: "radiogroup",
57738
+ "aria-label": "Runs on this camera",
57739
+ title: `Catalog default: ${gate.defaultOn ? "on" : "off"}`,
57740
+ className: "inline-flex overflow-hidden rounded-md border border-border",
57741
+ children: [
57742
+ segment("auto", "Auto", null, "bg-primary/15 text-primary"),
57743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57744
+ className: "w-px self-stretch bg-border",
57745
+ "aria-hidden": true
57746
+ }),
57747
+ segment("on", "On", true, "bg-emerald-500 text-white"),
57748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57749
+ className: "w-px self-stretch bg-border",
57750
+ "aria-hidden": true
57751
+ }),
57752
+ segment("off", "Off", false, "bg-foreground-subtle/70 text-white")
57753
+ ]
57754
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57755
+ className: "hidden text-[10px] text-foreground-subtle @3xl/inspector:inline",
57756
+ children: ["default: ", gate.defaultOn ? "on" : "off"]
58069
57757
  })]
58070
- }), node.children.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
58071
- node: child,
58072
- depth: depth + 1,
58073
- selectedAddonId,
58074
- onSelect,
58075
- agentDots
58076
- }, child.addonId))] });
57758
+ });
57759
+ }
57760
+ /** The gate as a short list-row badge. */
57761
+ function GateBadge({ gate }) {
57762
+ if (gate.kind === "zone-driven") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57763
+ className: "shrink-0 text-[9px] font-semibold uppercase text-violet-400",
57764
+ children: "zones"
57765
+ });
57766
+ const on = gate.effectiveOn;
57767
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57768
+ 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"),
57769
+ title: gate.explicit ? "Set for this camera" : "Catalog default",
57770
+ children: on ? "on" : "off"
57771
+ });
57772
+ }
57773
+ //#endregion
57774
+ //#region src/composites/step-inspector/segmented-choice.tsx
57775
+ /**
57776
+ * SegmentedChoice — a small, labelled single-choice group (a radiogroup of
57777
+ * buttons): which layer the inspector's settings edit ("All accelerators" /
57778
+ * "Only hub · NPU"). 36 px tall on a phone, 28 px from `md` up.
57779
+ */
57780
+ function SegmentedChoice({ label, options, value, onChange }) {
57781
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57782
+ role: "radiogroup",
57783
+ "aria-label": label,
57784
+ className: "flex flex-wrap items-center gap-1",
57785
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57786
+ type: "button",
57787
+ role: "radio",
57788
+ "aria-checked": value === option.value,
57789
+ onClick: () => onChange(option.value),
57790
+ 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"),
57791
+ children: option.label
57792
+ }, option.value))
57793
+ });
57794
+ }
57795
+ //#endregion
57796
+ //#region src/composites/step-inspector/step-chips.tsx
57797
+ function chipText(state) {
57798
+ switch (state.kind) {
57799
+ case "enabled":
57800
+ if (state.suppliedByRoot !== void 0) return "root";
57801
+ return state.jumpTo !== void 0 ? `→ ${state.jumpTo}` : state.modelId;
57802
+ case "skip": return state.jumpTo !== void 0 ? `→ ${state.jumpTo}` : "skip";
57803
+ case "disabled": return "off";
57804
+ case "na": return "—";
57805
+ }
57806
+ }
57807
+ /** What a chip MEANS — the full model id, not its shortened text. */
57808
+ function chipKey(state) {
57809
+ return state.kind === "enabled" ? `${state.modelId}|${chipText(state)}` : chipText(state);
57810
+ }
57811
+ function chipTone(state, selected) {
57812
+ if (selected) return "border-primary/60 text-primary";
57813
+ if (state.kind === "skip" && state.jumpTo === void 0) return "border-amber-500/40 text-amber-500";
57814
+ if (state.kind === "na" || state.kind === "disabled") return "border-border/60 text-foreground-subtle/70";
57815
+ return "border-border text-foreground-subtle";
57816
+ }
57817
+ function StepChips({ chips, selectedId }) {
57818
+ const first = chips[0];
57819
+ if (first !== void 0 && chips.length > 1 && chips.every((c) => chipKey(c.state) === chipKey(first.state) && !(c.state.kind === "enabled" && c.state.overridden === true) && chipTone(c.state, c.id === selectedId) === chipTone(first.state, first.id === selectedId)) && first !== void 0) {
57820
+ const text = chipText(first.state);
57821
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57822
+ className: "flex flex-wrap gap-1",
57823
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57824
+ title: `${text} on ${chips.map((c) => c.label).join(", ")}`,
57825
+ className: cn("inline-flex max-w-[12rem] items-center gap-1 truncate rounded border px-1 font-mono text-[9px] leading-4", chipTone(first.state, first.id === selectedId)),
57826
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57827
+ className: "truncate",
57828
+ children: text
57829
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57830
+ className: "shrink-0 font-sans text-foreground-subtle/80",
57831
+ children: ["×", chips.length]
57832
+ })]
57833
+ })
57834
+ });
57835
+ }
57836
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57837
+ className: "flex flex-wrap gap-1",
57838
+ children: chips.map((chip) => {
57839
+ const overridden = chip.state.kind === "enabled" && chip.state.overridden === true;
57840
+ const text = chipText(chip.state);
57841
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
57842
+ title: `${chip.label}: ${chip.state.kind === "enabled" ? chip.state.modelId : text}${overridden ? " (camera override)" : ""}`,
57843
+ className: cn("inline-flex max-w-[9rem] items-center gap-0.5 truncate rounded border px-1 font-mono text-[9px] leading-4", chipTone(chip.state, chip.id === selectedId)),
57844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57845
+ className: "truncate",
57846
+ children: text
57847
+ }), overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57848
+ className: "text-amber-500",
57849
+ "aria-hidden": true,
57850
+ children: "•"
57851
+ })]
57852
+ }, chip.id);
57853
+ })
57854
+ });
57855
+ }
57856
+ //#endregion
57857
+ //#region src/composites/step-inspector/step-inspector-layout.tsx
57858
+ /**
57859
+ * StepInspectorLayout — the master–detail frame of the step inspector.
57860
+ *
57861
+ * Desktop: the step list and the inspector side by side, each scrolling on its
57862
+ * own, in a block whose height FILLS the page's scroll container below it. So
57863
+ * neither pane depends on the page scroll position: the inspector's header and
57864
+ * save bar are always on screen, and the selected step stays visible in the
57865
+ * list. (The slide-over this replaced was absolutely positioned inside the
57866
+ * matrix block — its title and its Save button scrolled away with the page.)
57867
+ * In a narrow container (the camera page beside its live view, a tablet) the
57868
+ * two panes stack: the list on top, a third of the height.
57869
+ *
57870
+ * Phone: the list flows with the page; the inspector opens as a full-screen
57871
+ * sheet with "‹ Steps" to go back.
57872
+ */
57873
+ /** The tallest the inspector block can be without making its scroll container scroll. */
57874
+ function fillHeightPx(input) {
57875
+ const room = input.scrollerHeight - input.paddingBottom - input.offsetInScroller;
57876
+ return Math.max(input.minPx, Math.floor(room));
57877
+ }
57878
+ function scrollParentOf(el) {
57879
+ for (let p = el.parentElement; p !== null; p = p.parentElement) {
57880
+ const { overflowY } = getComputedStyle(p);
57881
+ if (overflowY === "auto" || overflowY === "scroll") return p;
57882
+ }
57883
+ return null;
57884
+ }
57885
+ /** Height of the block so that it ends at the bottom of its scroll container. */
57886
+ function useFillHeight(enabled, minPx) {
57887
+ const ref = (0, react$1.useRef)(null);
57888
+ const [height, setHeight] = (0, react$1.useState)(null);
57889
+ (0, react$1.useLayoutEffect)(() => {
57890
+ const el = ref.current;
57891
+ if (!enabled || el === null) return;
57892
+ const scroller = scrollParentOf(el);
57893
+ const measure = () => {
57894
+ const box = scroller ?? document.documentElement;
57895
+ const style = getComputedStyle(box);
57896
+ const top = scroller ? scroller.getBoundingClientRect().top : 0;
57897
+ const scrollTop = scroller ? scroller.scrollTop : window.scrollY;
57898
+ setHeight(fillHeightPx({
57899
+ scrollerHeight: scroller ? scroller.clientHeight : window.innerHeight,
57900
+ paddingBottom: Number.parseFloat(style.paddingBottom) || 0,
57901
+ offsetInScroller: el.getBoundingClientRect().top - top + scrollTop,
57902
+ minPx
57903
+ }));
57904
+ };
57905
+ measure();
57906
+ const RO = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
57907
+ if (scroller) RO?.observe(scroller);
57908
+ if (el.parentElement) RO?.observe(el.parentElement);
57909
+ window.addEventListener("resize", measure);
57910
+ return () => {
57911
+ RO?.disconnect();
57912
+ window.removeEventListener("resize", measure);
57913
+ };
57914
+ }, [enabled, minPx]);
57915
+ return {
57916
+ ref,
57917
+ height
57918
+ };
58077
57919
  }
58078
- function StepTreeMaster({ tree, selectedAddonId, onSelect, agentDots, className }) {
57920
+ function StepInspectorLayout({ list, detail, mobileDetailOpen, onMobileClose, minHeightPx = 420, className }) {
57921
+ const isMobile = useIsMobile();
57922
+ const { ref, height } = useFillHeight(!isMobile, minHeightPx);
57923
+ if (isMobile) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57924
+ className: cn("space-y-3", className),
57925
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57926
+ className: SECTION_CARD,
57927
+ children: list
57928
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
57929
+ open: mobileDetailOpen,
57930
+ onOpenChange: (open) => !open && onMobileClose(),
57931
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogContent, {
57932
+ placement: "sheet-on-mobile",
57933
+ scroll: "body",
57934
+ 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)]",
57935
+ children: mobileDetailOpen && detail
57936
+ })
57937
+ })]
57938
+ });
58079
57939
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58080
- className: cn("flex flex-col py-2", className),
58081
- children: tree.map((node) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
58082
- node,
58083
- depth: 0,
58084
- selectedAddonId,
58085
- onSelect,
58086
- agentDots
58087
- }, node.addonId))
57940
+ ref,
57941
+ className: cn("@container/inspector", className),
57942
+ style: height !== null ? { height } : void 0,
57943
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57944
+ className: cn("grid h-full min-h-0 gap-3", "grid-cols-1 grid-rows-[minmax(9rem,32%)_minmax(0,1fr)]", "@xl/inspector:grid-cols-[minmax(14rem,19rem)_minmax(0,1fr)] @xl/inspector:grid-rows-1"),
57945
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57946
+ className: cn(SECTION_CARD, "flex min-h-0 flex-col"),
57947
+ children: list
57948
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57949
+ className: cn(SECTION_CARD, "flex min-h-0 flex-col"),
57950
+ children: detail
57951
+ })]
57952
+ })
57953
+ });
57954
+ }
57955
+ /** "‹ Steps" — the phone sheet's way back to the list. */
57956
+ function BackToStepsButton({ onClick }) {
57957
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57958
+ type: "button",
57959
+ onClick,
57960
+ 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",
57961
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronLeft, {
57962
+ className: "h-3.5 w-3.5",
57963
+ "aria-hidden": true
57964
+ }), "Steps"]
58088
57965
  });
58089
57966
  }
57967
+ //#endregion
57968
+ //#region src/composites/step-inspector/step-inspector-panel.tsx
57969
+ /**
57970
+ * StepInspectorPanel — the detail pane of the step inspector: a pinned header
57971
+ * (the step, its gate, the accelerator tabs), ONE scrolling body, and a pinned
57972
+ * save bar. Nothing in it depends on the page's scroll position — the slide-over
57973
+ * it replaced was anchored to the matrix block and scrolled away with it.
57974
+ */
57975
+ function SaveBar({ dirtyCount, saving, status, error, onSave, onDiscard }) {
57976
+ const dirty = dirtyCount > 0;
57977
+ const message = saving ? "Saving…" : dirty ? `${dirtyCount} unsaved change${dirtyCount === 1 ? "" : "s"}` : status === "saved" ? "Saved" : status === "error" ? error ?? "Save failed" : "No unsaved changes";
57978
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
57979
+ 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",
57980
+ children: [
57981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57982
+ role: "status",
57983
+ className: cn("min-w-0 flex-1 truncate text-[11px]", status === "error" && !dirty ? "text-danger" : dirty ? "text-amber-500" : "text-foreground-subtle"),
57984
+ children: message
57985
+ }),
57986
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
57987
+ variant: "outline",
57988
+ size: "md",
57989
+ disabled: !dirty || saving,
57990
+ onClick: onDiscard,
57991
+ children: "Discard"
57992
+ }),
57993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
57994
+ variant: "primary",
57995
+ size: "md",
57996
+ disabled: !dirty || saving,
57997
+ onClick: onSave,
57998
+ children: "Save"
57999
+ })
58000
+ ]
58001
+ });
58002
+ }
58003
+ var StepInspectorPanel = (0, react$1.forwardRef)(function StepInspectorPanel({ title, subtitle, headerActions, headerExtra, leading, saveBar, onEscape, children, className }, headingRef) {
58004
+ const headingId = (0, react$1.useId)();
58005
+ const onKeyDown = (e) => {
58006
+ if (e.key !== "Escape" || onEscape === void 0) return;
58007
+ e.stopPropagation();
58008
+ onEscape();
58009
+ };
58010
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
58011
+ "aria-labelledby": headingId,
58012
+ onKeyDown,
58013
+ className: cn("flex min-h-0 flex-1 flex-col overflow-hidden bg-surface", className),
58014
+ children: [
58015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
58016
+ className: "shrink-0 space-y-2 border-b border-border px-4 py-2.5",
58017
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58018
+ className: "flex flex-wrap items-start gap-x-3 gap-y-2",
58019
+ children: [
58020
+ leading,
58021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58022
+ className: "min-w-[9rem] flex-1",
58023
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
58024
+ id: headingId,
58025
+ ref: headingRef,
58026
+ tabIndex: -1,
58027
+ className: "truncate text-sm font-semibold text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary rounded",
58028
+ children: title
58029
+ }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58030
+ className: "mt-0.5 text-[11px] text-foreground-subtle",
58031
+ children: subtitle
58032
+ })]
58033
+ }),
58034
+ headerActions
58035
+ ]
58036
+ }), headerExtra]
58037
+ }),
58038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58039
+ "data-inspector-body": true,
58040
+ className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3",
58041
+ children
58042
+ }),
58043
+ saveBar !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveBar, { ...saveBar })
58044
+ ]
58045
+ });
58046
+ });
58047
+ //#endregion
58048
+ //#region src/composites/step-inspector/step-list.tsx
58049
+ /**
58050
+ * StepList — the master pane of the step inspector: every detection step of the
58051
+ * tree, one compact row each, as ONE tab stop.
58052
+ *
58053
+ * Keyboard (a listbox with a roving tabindex): ↑/↓/Home/End move the selection,
58054
+ * Enter or → hands focus to the inspector, `/` jumps to the search box. The
58055
+ * search matches a step's name or any of its field labels, so a question about
58056
+ * a knob ("face size", "person") finds the step that owns it; Enter in the box
58057
+ * selects the first hit and says which field it matched.
58058
+ */
58059
+ function matches(text, tokens) {
58060
+ const lower = text.toLowerCase();
58061
+ return tokens.every((t) => lower.includes(t));
58062
+ }
58063
+ /** Rows matching `query` (all tokens in the name, or all in one field label). */
58064
+ function searchSteps(rows, query) {
58065
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
58066
+ if (tokens.length === 0) return rows.map((row) => ({ row }));
58067
+ const hits = [];
58068
+ for (const row of rows) {
58069
+ if (matches(row.name, tokens)) {
58070
+ hits.push({ row });
58071
+ continue;
58072
+ }
58073
+ const field = row.fieldLabels.find((l) => matches(l, tokens));
58074
+ if (field !== void 0) hits.push({
58075
+ row,
58076
+ matchedField: field
58077
+ });
58078
+ }
58079
+ return hits;
58080
+ }
58081
+ var StepList = (0, react$1.forwardRef)(function StepList({ rows, selectedId, onSelect, onActivate, onPick, label, className }, ref) {
58082
+ const [query, setQuery] = (0, react$1.useState)("");
58083
+ const listRef = (0, react$1.useRef)(null);
58084
+ const searchRef = (0, react$1.useRef)(null);
58085
+ const hits = (0, react$1.useMemo)(() => searchSteps(rows, query), [rows, query]);
58086
+ const tabStopId = hits.some((h) => h.row.addonId === selectedId) ? selectedId : hits[0]?.row.addonId ?? null;
58087
+ const pendingFocus = (0, react$1.useRef)(null);
58088
+ const tryFocus = (addonId) => {
58089
+ const el = listRef.current?.querySelector(`[data-step-id="${addonId}"]`);
58090
+ if (!el) return false;
58091
+ el.focus();
58092
+ return true;
58093
+ };
58094
+ const focusRow = (addonId) => {
58095
+ if (addonId === null) return;
58096
+ pendingFocus.current = tryFocus(addonId) ? null : addonId;
58097
+ };
58098
+ (0, react$1.useLayoutEffect)(() => {
58099
+ if (pendingFocus.current !== null && tryFocus(pendingFocus.current)) pendingFocus.current = null;
58100
+ });
58101
+ (0, react$1.useImperativeHandle)(ref, () => ({ focusSelected: () => focusRow(selectedId) }), [selectedId]);
58102
+ const move = (index) => {
58103
+ const target = hits[Math.max(0, Math.min(hits.length - 1, index))];
58104
+ if (target === void 0) return;
58105
+ onSelect(target.row.addonId, target.matchedField);
58106
+ focusRow(target.row.addonId);
58107
+ };
58108
+ const onRowKeyDown = (e, index, hit) => {
58109
+ switch (e.key) {
58110
+ case "ArrowDown":
58111
+ e.preventDefault();
58112
+ move(index + 1);
58113
+ return;
58114
+ case "ArrowUp":
58115
+ e.preventDefault();
58116
+ move(index - 1);
58117
+ return;
58118
+ case "Home":
58119
+ e.preventDefault();
58120
+ move(0);
58121
+ return;
58122
+ case "End":
58123
+ e.preventDefault();
58124
+ move(hits.length - 1);
58125
+ return;
58126
+ case "Enter":
58127
+ case "ArrowRight":
58128
+ e.preventDefault();
58129
+ onActivate(hit.row.addonId);
58130
+ return;
58131
+ case "/":
58132
+ e.preventDefault();
58133
+ searchRef.current?.focus();
58134
+ return;
58135
+ default:
58136
+ }
58137
+ };
58138
+ const onSearchKeyDown = (e) => {
58139
+ if (e.key === "Enter") {
58140
+ e.preventDefault();
58141
+ const first = hits[0];
58142
+ if (first !== void 0) {
58143
+ onSelect(first.row.addonId, first.matchedField);
58144
+ onPick?.(first.row.addonId);
58145
+ focusRow(first.row.addonId);
58146
+ }
58147
+ } else if (e.key === "Escape") {
58148
+ e.preventDefault();
58149
+ setQuery("");
58150
+ focusRow(selectedId);
58151
+ } else if (e.key === "ArrowDown") {
58152
+ e.preventDefault();
58153
+ focusRow(tabStopId);
58154
+ }
58155
+ };
58156
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58157
+ className: cn("flex min-h-0 flex-col", className),
58158
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58159
+ className: "relative shrink-0 border-b border-border p-1.5",
58160
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, {
58161
+ className: "pointer-events-none absolute left-3.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-foreground-subtle",
58162
+ "aria-hidden": true
58163
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
58164
+ ref: searchRef,
58165
+ type: "search",
58166
+ value: query,
58167
+ onChange: (e) => setQuery(e.target.value),
58168
+ onKeyDown: onSearchKeyDown,
58169
+ placeholder: "Find a step or setting /",
58170
+ "aria-label": "Find a step or setting",
58171
+ className: "h-9 w-full rounded-md border border-border bg-background pl-8 pr-2 text-xs text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary/30 md:h-8"
58172
+ })]
58173
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
58174
+ ref: listRef,
58175
+ role: "listbox",
58176
+ "aria-label": label,
58177
+ className: "min-h-0 flex-1 overflow-y-auto py-1",
58178
+ children: [hits.map((hit, index) => {
58179
+ const { row } = hit;
58180
+ const selected = row.addonId === selectedId;
58181
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
58182
+ role: "option",
58183
+ "aria-selected": selected,
58184
+ tabIndex: row.addonId === tabStopId ? 0 : -1,
58185
+ "data-step-id": row.addonId,
58186
+ onClick: () => {
58187
+ onSelect(row.addonId, hit.matchedField);
58188
+ onPick?.(row.addonId);
58189
+ },
58190
+ onKeyDown: (e) => onRowKeyDown(e, index, hit),
58191
+ className: cn("cursor-pointer border-l-2 py-1 pl-2.5 pr-3 outline-none transition-colors", "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary", selected ? "border-primary bg-primary/10" : "border-transparent hover:bg-muted/40"),
58192
+ children: [
58193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58194
+ className: "flex min-w-0 items-baseline gap-2",
58195
+ children: [
58196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58197
+ className: "w-[4.5rem] shrink-0 truncate",
58198
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotHeading, { slot: row.slot })
58199
+ }),
58200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
58201
+ className: cn("min-w-0 flex-1 truncate text-xs font-medium", selected ? "text-foreground" : "text-foreground/90"),
58202
+ style: { paddingLeft: `${row.depth * 12}px` },
58203
+ children: [row.depth > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58204
+ className: "mr-1 text-foreground-subtle/60",
58205
+ "aria-hidden": true,
58206
+ children: "└"
58207
+ }), row.name]
58208
+ }),
58209
+ row.status
58210
+ ]
58211
+ }),
58212
+ hit.matchedField !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58213
+ className: "mt-0.5 truncate pl-[5rem] text-[10px] text-primary",
58214
+ children: ["↳ ", hit.matchedField]
58215
+ }),
58216
+ row.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58217
+ className: "mt-0.5 pl-[5rem]",
58218
+ children: row.detail
58219
+ })
58220
+ ]
58221
+ }, row.addonId);
58222
+ }), hits.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
58223
+ className: "px-3 py-4 text-center text-[11px] text-foreground-subtle",
58224
+ children: [
58225
+ "No step matches “",
58226
+ query,
58227
+ "”."
58228
+ ]
58229
+ })]
58230
+ })]
58231
+ });
58232
+ });
58233
+ //#endregion
58234
+ //#region src/composites/step-inspector/step-model-row.tsx
58235
+ var SOURCE_TONE = {
58236
+ camera: "border-primary/40 bg-primary/10 text-primary",
58237
+ node: "border-border bg-muted/40 text-foreground",
58238
+ default: "border-border/60 text-foreground-subtle",
58239
+ cluster: "border-primary/40 bg-primary/5 text-primary"
58240
+ };
58241
+ function StepModelRow({ label, format, modelId, source, sourceLabel, models, defaultModelId, expanded, onToggle, onChangeModel, onReset, resetLabel = "Reset", note, extra, changed = false, disabled = false }) {
58242
+ const pickable = models.length > 0 && note === void 0;
58243
+ const canExpand = pickable || extra !== void 0;
58244
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58245
+ className: cn("rounded-md border border-border", expanded && "bg-muted/10"),
58246
+ children: [
58247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58248
+ className: cn(LIST_ROW, "space-y-1"),
58249
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58250
+ className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs",
58251
+ children: [
58252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58253
+ className: "font-medium text-foreground",
58254
+ children: label
58255
+ }),
58256
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58257
+ className: "text-[10px] text-foreground-subtle",
58258
+ children: format
58259
+ }),
58260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58261
+ className: cn("ml-auto rounded border px-1.5 py-px text-[10px] font-medium", SOURCE_TONE[source]),
58262
+ children: sourceLabel
58263
+ })
58264
+ ]
58265
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58266
+ className: "flex items-center gap-2",
58267
+ children: [
58268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
58269
+ className: "min-w-0 flex-1 truncate font-mono text-xs text-foreground",
58270
+ children: [modelId ?? "—", changed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58271
+ className: "ml-1 text-amber-500",
58272
+ children: "•"
58273
+ })]
58274
+ }),
58275
+ onReset !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58276
+ type: "button",
58277
+ 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",
58278
+ disabled,
58279
+ onClick: onReset,
58280
+ children: resetLabel
58281
+ }),
58282
+ canExpand && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58283
+ type: "button",
58284
+ 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"),
58285
+ "aria-expanded": expanded,
58286
+ "aria-label": `Change model for ${label}`,
58287
+ disabled,
58288
+ onClick: onToggle,
58289
+ children: expanded ? "Done" : pickable ? "Change" : "Route"
58290
+ })
58291
+ ]
58292
+ })]
58293
+ }),
58294
+ note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58295
+ className: "border-t border-border/60 px-3 py-1.5 text-[10px] leading-snug text-foreground-subtle",
58296
+ children: note
58297
+ }),
58298
+ expanded && canExpand && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58299
+ className: "space-y-3 border-t border-border/60 px-3 py-3",
58300
+ children: [pickable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
58301
+ models,
58302
+ value: modelId ?? "",
58303
+ defaultModelId,
58304
+ disabled,
58305
+ onChange: onChangeModel
58306
+ }), extra]
58307
+ })
58308
+ ]
58309
+ });
58310
+ }
58311
+ //#endregion
58312
+ //#region src/composites/step-inspector/step-model-list.tsx
58313
+ /**
58314
+ * StepModelList — the model section of the step inspector.
58315
+ *
58316
+ * While nothing is pinned, edited or worth a note, the accelerators are
58317
+ * summarised by model — one line per distinct model, naming the accelerators
58318
+ * that run it ("yolov9m-320-int8 · hub iGPU, hub NPU, little-unraid iGPU") —
58319
+ * with "Per accelerator" to open one `StepModelRow` each. Four near-identical
58320
+ * rows pushed the settings, the thing most edits are about, below the fold.
58321
+ * A pin, a pending change or a note (no build here, root-supplied) shows every
58322
+ * row, so nothing that differs from the default is ever folded away.
58323
+ */
58324
+ /** True when the rows can be summarised: nothing pinned here, edited, or noted. */
58325
+ function modelRowsAreFoldable(rows) {
58326
+ return rows.length >= 2 && rows.every((r) => r.note === void 0 && r.onReset === void 0 && r.changed !== true && !r.expanded);
58327
+ }
58328
+ function groupByModel(rows) {
58329
+ const groups = [];
58330
+ for (const row of rows) {
58331
+ const modelId = row.modelId ?? "—";
58332
+ const index = groups.findIndex((g) => g.modelId === modelId && g.sourceLabel === row.sourceLabel);
58333
+ const existing = groups[index];
58334
+ if (existing !== void 0) groups[index] = {
58335
+ ...existing,
58336
+ labels: [...existing.labels, row.label]
58337
+ };
58338
+ else groups.push({
58339
+ modelId,
58340
+ labels: [row.label],
58341
+ sourceLabel: row.sourceLabel
58342
+ });
58343
+ }
58344
+ return groups;
58345
+ }
58346
+ function StepModelList({ rows }) {
58347
+ const [open, setOpen] = (0, react$1.useState)(false);
58348
+ if (!open && modelRowsAreFoldable(rows)) {
58349
+ const groups = groupByModel(rows);
58350
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58351
+ className: cn("rounded-md border border-border", LIST_ROW),
58352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
58353
+ className: "space-y-1",
58354
+ children: groups.map((g) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
58355
+ className: "flex flex-wrap items-baseline gap-x-2 text-xs",
58356
+ children: [
58357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58358
+ className: "font-mono text-foreground",
58359
+ children: g.modelId
58360
+ }),
58361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58362
+ className: "min-w-0 flex-1 text-[11px] text-foreground-subtle",
58363
+ children: g.labels.length === rows.length ? `all ${rows.length} accelerators` : g.labels.join(", ")
58364
+ }),
58365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58366
+ className: "rounded border border-border px-1.5 py-px text-[10px] font-medium text-foreground-subtle",
58367
+ children: g.sourceLabel
58368
+ })
58369
+ ]
58370
+ }, `${g.modelId}|${g.sourceLabel}`))
58371
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58372
+ className: "mt-1.5 flex justify-end",
58373
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58374
+ type: "button",
58375
+ className: cn(BTN_COMPACT, "min-h-9 md:min-h-0"),
58376
+ "aria-expanded": false,
58377
+ onClick: () => setOpen(true),
58378
+ children: "Per accelerator"
58379
+ })
58380
+ })]
58381
+ });
58382
+ }
58383
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58384
+ className: "space-y-2",
58385
+ children: rows.map(({ id, ...row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepModelRow, { ...row }, id))
58386
+ });
58387
+ }
58388
+ //#endregion
58389
+ //#region src/composites/step-tree.ts
58090
58390
  /**
58091
58391
  * Helper: build a StepTreeNode[] from a `PipelineSchema` by chaining on
58092
58392
  * `inputClasses` ⇔ `outputClasses` compatibility. Mirrors the orchestrator
@@ -61031,6 +61331,7 @@ var SceneMonitorEditor = (0, react$1.lazy)(() => Promise.resolve().then(() => re
61031
61331
  /** Lazy-wrapped `PrivacyMaskSettings` — code-split off the main bundle. */
61032
61332
  var PrivacyMaskSettings = (0, react$1.lazy)(() => Promise.resolve().then(() => require("./PrivacyMaskSettings-BqjXsXZR.cjs")).then((m) => ({ default: m.PrivacyMaskSettings })));
61033
61333
  //#endregion
61334
+ exports.AcceleratorTabs = AcceleratorTabs;
61034
61335
  exports.AddonGlobalSettingsForm = AddonGlobalSettingsForm;
61035
61336
  exports.AdminLinkQuickAction = AdminLinkQuickAction;
61036
61337
  exports.AgentStepEditor = AgentStepEditor;
@@ -61048,6 +61349,7 @@ exports.BTN_COMPACT = BTN_COMPACT;
61048
61349
  exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
61049
61350
  exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
61050
61351
  exports.BTN_COMPACT_WARNING = BTN_COMPACT_WARNING;
61352
+ exports.BackToStepsButton = BackToStepsButton;
61051
61353
  exports.Badge = Badge;
61052
61354
  exports.BatteryBadge = BatteryBadge;
61053
61355
  exports.BottomSheet = BottomSheet;
@@ -61169,6 +61471,8 @@ exports.FormField = FormField$1;
61169
61471
  exports.GRID_GAP = GRID_GAP;
61170
61472
  exports.GRID_PAIRED = GRID_PAIRED;
61171
61473
  exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
61474
+ exports.GateBadge = GateBadge;
61475
+ exports.GateControl = GateControl;
61172
61476
  exports.Grid2x2 = Grid2x2;
61173
61477
  exports.GripTrack = GripTrack;
61174
61478
  exports.GroupedModelSelector = GroupedModelSelector;
@@ -61196,6 +61500,7 @@ exports.LIST_ROW = LIST_ROW;
61196
61500
  exports.Label = Label;
61197
61501
  exports.LawnMowerHeroCard = LawnMowerHeroCard;
61198
61502
  exports.LawnMowerInlineControl = LawnMowerInlineControl;
61503
+ exports.LayeredSettingsForm = LayeredSettingsForm;
61199
61504
  exports.LightHeroCard = LightHeroCard;
61200
61505
  exports.LightInlineControl = LightInlineControl;
61201
61506
  exports.LockHeroCard = LockHeroCard;
@@ -61243,7 +61548,6 @@ exports.PhaseIcon = PhaseIcon;
61243
61548
  exports.PipelineBuilder = PipelineBuilder;
61244
61549
  exports.PipelineRuntimeSelector = PipelineRuntimeSelector;
61245
61550
  exports.PipelineStep = PipelineStep;
61246
- exports.PipelineTreeMatrix = PipelineTreeMatrix;
61247
61551
  exports.PlaceholderChipField = PlaceholderChipField;
61248
61552
  exports.PlayerOverlaysProvider = PlayerOverlaysProvider;
61249
61553
  exports.Popover = Popover;
@@ -61266,6 +61570,8 @@ exports.RecordingPanel = RecordingPanel;
61266
61570
  exports.ResponseLog = ResponseLog;
61267
61571
  exports.RoleDisplayDefaultsFromServer = RoleDisplayDefaultsFromServer;
61268
61572
  exports.RoleDisplayDefaultsProvider = RoleDisplayDefaultsProvider;
61573
+ exports.RootModelSourceControl = RootModelSourceControl;
61574
+ exports.RootSuppliedModelNote = RootSuppliedModelNote;
61269
61575
  exports.RotateCcw = RotateCcw;
61270
61576
  exports.RunReport = RunReport;
61271
61577
  exports.SCENE_CAPTURE_VARIANTS = SCENE_CAPTURE_VARIANTS;
@@ -61289,6 +61595,7 @@ exports.ScriptHeroCard = ScriptHeroCard;
61289
61595
  exports.ScriptInlineControl = ScriptInlineControl;
61290
61596
  exports.ScrollArea = ScrollArea;
61291
61597
  exports.SectionCard = SectionCard;
61598
+ exports.SegmentedChoice = SegmentedChoice;
61292
61599
  exports.Select = Select;
61293
61600
  exports.SelectedChip = SelectedChip;
61294
61601
  exports.SemanticBadge = SemanticBadge;
@@ -61300,7 +61607,6 @@ exports.SettingRow = SettingRow;
61300
61607
  exports.Sidebar = Sidebar;
61301
61608
  exports.SidebarItem = SidebarItem;
61302
61609
  exports.Skeleton = Skeleton;
61303
- exports.SlideOverPanel = SlideOverPanel;
61304
61610
  exports.SlideToggle = SlideToggle;
61305
61611
  exports.SnapshotButton = SnapshotButton;
61306
61612
  exports.SortHeaderButton = SortHeaderButton;
@@ -61308,8 +61614,13 @@ exports.Square = Square;
61308
61614
  exports.StatCard = StatCard;
61309
61615
  exports.StateValuesStream = StateValuesStream;
61310
61616
  exports.StatusBadge = StatusBadge;
61617
+ exports.StepChips = StepChips;
61618
+ exports.StepInspectorLayout = StepInspectorLayout;
61619
+ exports.StepInspectorPanel = StepInspectorPanel;
61620
+ exports.StepList = StepList;
61621
+ exports.StepModelList = StepModelList;
61622
+ exports.StepModelRow = StepModelRow;
61311
61623
  exports.StepTimings = StepTimings;
61312
- exports.StepTreeMaster = StepTreeMaster;
61313
61624
  exports.Stepper = Stepper;
61314
61625
  exports.StreamBrokerSelector = StreamBrokerSelector;
61315
61626
  exports.StreamDebugOverlay = StreamDebugOverlay;
@@ -61363,6 +61674,7 @@ exports.applyPickerFilters = applyPickerFilters;
61363
61674
  exports.ariaSortForColumn = ariaSortForColumn;
61364
61675
  exports.badgeFor = badgeFor;
61365
61676
  exports.badgeFor$1 = badgeFor;
61677
+ exports.bindsToModel = bindsToModel;
61366
61678
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
61367
61679
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
61368
61680
  exports.catalogHasDraft = catalogHasDraft;
@@ -61387,6 +61699,7 @@ exports.createTheme = require_theme_index.createTheme$1;
61387
61699
  exports.cursorFractionFor = cursorFractionFor;
61388
61700
  exports.darkColors = require_theme_index.darkColors$1;
61389
61701
  exports.dataTableFacets = dataTableFacets;
61702
+ exports.defaultOfField = defaultOfField;
61390
61703
  exports.defaultTheme = require_theme_index.defaultTheme$1;
61391
61704
  exports.deriveDeviceKind = deriveDeviceKind;
61392
61705
  exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
@@ -61406,6 +61719,7 @@ exports.emptyDevicesWidens = emptyDevicesWidens;
61406
61719
  exports.emptySessionRun = emptySessionRun;
61407
61720
  exports.ensureMfHostInit = ensureMfHostInit;
61408
61721
  exports.eventKindLabel = eventKindLabel;
61722
+ exports.fillHeightPx = fillHeightPx;
61409
61723
  exports.filterDataTableRows = filterDataTableRows;
61410
61724
  exports.filterDeviceOptions = filterDeviceOptions;
61411
61725
  exports.filterOptionSelectOptions = filterOptions;
@@ -61418,6 +61732,7 @@ exports.fleetViewOf = fleetViewOf;
61418
61732
  exports.formatControlDateTime = formatControlDateTime;
61419
61733
  exports.formatLastSeen = formatLastSeen;
61420
61734
  exports.formatNumeric = formatNumeric;
61735
+ exports.formatSettingValue = formatSettingValue;
61421
61736
  exports.freezeVerdict = freezeVerdict;
61422
61737
  exports.fuzzyMatch = fuzzyMatch;
61423
61738
  exports.getClassColor = getClassColor;
@@ -61441,6 +61756,7 @@ exports.isAbsentProvider$1 = isAbsentProvider;
61441
61756
  exports.isDataTableFilterActive = isDataTableFilterActive;
61442
61757
  exports.isFieldVisible = isFieldVisible;
61443
61758
  exports.isGroupExpanded = isGroupExpanded;
61759
+ exports.isValueField = isValueField;
61444
61760
  exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
61445
61761
  exports.lightColors = require_theme_index.lightColors$1;
61446
61762
  exports.loadRemoteBundle = loadRemoteBundle;
@@ -61448,6 +61764,7 @@ exports.makeScrubBridge = makeScrubBridge;
61448
61764
  exports.metadataEntries = metadataEntries;
61449
61765
  exports.metadataString = metadataString;
61450
61766
  exports.mirror = mirror;
61767
+ exports.modelRowsAreFoldable = modelRowsAreFoldable;
61451
61768
  exports.mountAddonPage = mountAddonPage;
61452
61769
  exports.mountPath = mountPath;
61453
61770
  exports.nextReconnectAction = nextReconnectAction;
@@ -61505,9 +61822,11 @@ exports.rootSourceState = rootSourceState;
61505
61822
  exports.routerBasename = routerBasename;
61506
61823
  exports.sameLocationSuggestions = sameLocationSuggestions;
61507
61824
  exports.scrubReducer = scrubReducer;
61825
+ exports.searchSteps = searchSteps;
61508
61826
  exports.selectPickerGroup = selectPickerGroup;
61509
61827
  exports.selectedDeviceOptions = selectedDeviceOptions;
61510
61828
  exports.serializeRecordedCommand = serializeRecordedCommand;
61829
+ exports.settingsModelIdFor = settingsModelIdFor;
61511
61830
  exports.shouldCommit = shouldCommit;
61512
61831
  exports.shouldEmit = shouldEmit;
61513
61832
  exports.shouldShowAddNewFooter = shouldShowAddNewFooter;
@@ -61519,7 +61838,9 @@ exports.sortRowsByColumn = sortRowsByColumn;
61519
61838
  exports.statusIcons = statusIcons;
61520
61839
  exports.statusLine = statusLine;
61521
61840
  exports.statusLine$1 = statusLine;
61841
+ exports.stepFieldDomId = stepFieldDomId;
61522
61842
  exports.stepHasModelForFormat = stepHasModelForFormat;
61843
+ exports.stepLocalFields = stepLocalFields;
61523
61844
  exports.stepModelOptions = stepModelOptions;
61524
61845
  exports.stripParentNamePrefix = stripParentNamePrefix;
61525
61846
  exports.summarizeHubCandidates = summarizeHubCandidates;
@@ -62349,6 +62670,7 @@ exports.usePipelineOrchestratorSetAgentReachableHost = usePipelineOrchestratorSe
62349
62670
  exports.usePipelineOrchestratorSetCameraPipelineForAgent = usePipelineOrchestratorSetCameraPipelineForAgent;
62350
62671
  exports.usePipelineOrchestratorSetCameraSensors = usePipelineOrchestratorSetCameraSensors;
62351
62672
  exports.usePipelineOrchestratorSetCameraStepOverride = usePipelineOrchestratorSetCameraStepOverride;
62673
+ exports.usePipelineOrchestratorSetCameraStepSettings = usePipelineOrchestratorSetCameraStepSettings;
62352
62674
  exports.usePipelineOrchestratorSetCameraStepToggle = usePipelineOrchestratorSetCameraStepToggle;
62353
62675
  exports.usePipelineOrchestratorSetCameraSwitch = usePipelineOrchestratorSetCameraSwitch;
62354
62676
  exports.usePipelineOrchestratorSetCapabilityBinding = usePipelineOrchestratorSetCapabilityBinding;
@@ -62734,4 +63056,5 @@ exports.waterHeaterTint = waterHeaterTint;
62734
63056
  exports.weatherConditionMeta = weatherConditionMeta;
62735
63057
  exports.weatherTint = weatherTint;
62736
63058
  exports.withResolvedParentDetail = withResolvedParentDetail;
63059
+ exports.withUseRootModel = withUseRootModel;
62737
63060
  exports.writeStoredGroupBy = writeStoredGroupBy;