@camstack/ui-library 1.2.244 → 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.
package/dist/index.cjs CHANGED
@@ -28746,32 +28746,6 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
28746
28746
  });
28747
28747
  }
28748
28748
  //#endregion
28749
- //#region src/primitives/collapsible-card.tsx
28750
- function CollapsibleCard({ expanded, onExpandedChange, header, children, dimmed = false, hideChevron = false, className }) {
28751
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28752
- className: cn("rounded-lg border border-border bg-surface overflow-hidden", dimmed && "opacity-[0.45]", className),
28753
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28754
- role: "button",
28755
- tabIndex: 0,
28756
- className: "flex items-center gap-2.5 px-3 py-2.5 cursor-pointer select-none",
28757
- onClick: () => onExpandedChange(!expanded),
28758
- onKeyDown: (e) => {
28759
- if (e.key === "Enter" || e.key === " ") {
28760
- e.preventDefault();
28761
- onExpandedChange(!expanded);
28762
- }
28763
- },
28764
- children: [!hideChevron && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28765
- className: "text-foreground-subtle shrink-0",
28766
- children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-4 w-4" })
28767
- }), header]
28768
- }), expanded && children && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28769
- className: "border-t border-border px-3 py-3",
28770
- children
28771
- })]
28772
- });
28773
- }
28774
- //#endregion
28775
28749
  //#region src/composites/grouped-model-selector.tsx
28776
28750
  /**
28777
28751
  * GroupedModelSelector — the shared Family → Tier → Variant model picker.
@@ -29052,6 +29026,257 @@ function ModelPicker({ models, value, onChange, disabled, defaultModelId }) {
29052
29026
  });
29053
29027
  }
29054
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
29055
29280
  //#region src/composites/model-bound-slider.tsx
29056
29281
  var SOURCE_LABEL = {
29057
29282
  override: "Override for this model",
@@ -29061,7 +29286,7 @@ var SOURCE_LABEL = {
29061
29286
  function formatNumber(value, scale) {
29062
29287
  return (scale !== void 0 ? value / scale : value).toFixed(2).replace(/\.?0+$/, "");
29063
29288
  }
29064
- function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings }) {
29289
+ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings, hideSourceRow = false }) {
29065
29290
  const modelDefault = field.modelApplicability?.defaultByModel?.[modelId];
29066
29291
  const state = (0, _camstack_types.resolveModelBoundSetting)(settings, field.key, modelId.length > 0 ? modelId : void 0, modelDefault, field.default);
29067
29292
  const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
@@ -29101,7 +29326,7 @@ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings
29101
29326
  onChange: (e) => handleChange(Number(e.target.value)),
29102
29327
  className: "w-full accent-primary h-1.5"
29103
29328
  }),
29104
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29329
+ !hideSourceRow && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29105
29330
  className: "mt-1 flex items-center justify-between gap-2 text-[10px]",
29106
29331
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29107
29332
  "data-testid": "model-bound-source",
@@ -29152,6 +29377,32 @@ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset
29152
29377
  });
29153
29378
  }
29154
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
29155
29406
  //#region src/composites/pipeline-step.tsx
29156
29407
  /**
29157
29408
  * PipelineStep — single step card in the pipeline builder.
@@ -29163,124 +29414,77 @@ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset
29163
29414
  function modelsForStep(schema) {
29164
29415
  return schema?.models ?? [];
29165
29416
  }
29166
- function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false, toggleMode = "simple", overrideState = null, onOverrideChange, inheritedEnabled, hideModelAndSettings = false, allowAutoModel = false }) {
29417
+ function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false }) {
29167
29418
  const [expanded, setExpanded] = (0, react$1.useState)(false);
29168
29419
  const models = modelsForStep(schema);
29169
- const isAutoSentinel = allowAutoModel && step.modelId === "";
29170
- 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) {
29171
29421
  const fallback = (schema?.defaultModelId ? models.find((m) => m.id === schema.defaultModelId) : null) ?? models[0];
29172
29422
  if (fallback && fallback.id !== step.modelId) queueMicrotask(() => onChange({
29173
29423
  ...step,
29174
29424
  modelId: fallback.id
29175
29425
  }));
29176
29426
  }
29177
- const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
29178
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29179
- className: "flex-1 min-w-0",
29180
- children: [
29181
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29182
- className: "text-[10px] uppercase tracking-wider font-medium text-foreground-subtle/60 block leading-none",
29183
- children: step.slot
29184
- }),
29185
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29186
- className: "text-sm font-semibold text-foreground truncate block leading-tight",
29187
- children: step.addonName
29188
- }),
29189
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29190
- className: "flex items-center gap-1 mt-0.5 flex-wrap",
29191
- children: [
29192
- step.inputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29193
- className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/12 text-blue-400",
29194
- children: c
29195
- }, c)),
29196
- step.inputClasses.length > 0 && step.outputClasses.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29197
- className: "text-foreground-subtle/40 text-[10px]",
29198
- children: "→"
29199
- }),
29200
- step.outputClasses.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29201
- className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/12 text-green-400",
29202
- children: c
29203
- }, c))
29204
- ]
29205
- })
29206
- ]
29207
- }),
29208
- !readOnly && toggleMode === "simple" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29209
- onClick: (e) => {
29210
- e.stopPropagation();
29211
- onChange({
29212
- ...step,
29213
- enabled: !step.enabled
29214
- });
29215
- },
29216
- className: cn("relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors", step.enabled ? "bg-success" : "bg-foreground-subtle/30"),
29217
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-4 w-4 rounded-full bg-white shadow transition-transform", step.enabled ? "translate-x-6" : "translate-x-1") })
29218
- }),
29219
- !readOnly && toggleMode === "threeState" && onOverrideChange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29220
- className: "flex items-center gap-0.5 rounded-md border border-border p-0.5",
29221
- onClick: (e) => e.stopPropagation(),
29222
- children: [
29223
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29224
- label: "Inherit",
29225
- active: overrideState === null,
29226
- variant: "inherit",
29227
- subtext: inheritedEnabled === void 0 ? void 0 : inheritedEnabled ? "on" : "off",
29228
- onClick: () => onOverrideChange(null)
29229
- }),
29230
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29231
- label: "On",
29232
- active: overrideState === true,
29233
- variant: "on",
29234
- onClick: () => onOverrideChange(true)
29235
- }),
29236
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreeStateButton, {
29237
- label: "Off",
29238
- active: overrideState === false,
29239
- variant: "off",
29240
- onClick: () => onOverrideChange(false)
29241
- })
29242
- ]
29243
- })
29244
- ] });
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
+ })] });
29245
29467
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleCard, {
29246
29468
  expanded,
29247
29469
  onExpandedChange: setExpanded,
29248
29470
  dimmed: !step.enabled,
29249
29471
  header,
29250
- children: hideModelAndSettings ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29251
- className: "text-[11px] text-foreground-subtle",
29252
- children: "Model and detection settings are managed per agent — open the Pipeline page for this camera's agent to edit them."
29253
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29472
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29254
29473
  className: "space-y-3",
29255
29474
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29256
29475
  className: "space-y-2",
29257
- children: [
29258
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
29259
- className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
29260
- children: "Model"
29261
- }),
29262
- allowAutoModel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29263
- className: "flex items-center gap-2 text-[11px] text-foreground-subtle",
29264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29265
- type: "checkbox",
29266
- checked: isAutoSentinel,
29267
- disabled: readOnly,
29268
- onChange: (e) => onChange({
29269
- ...step,
29270
- modelId: e.target.checked ? "" : schema?.defaultModelId ?? models[0]?.id ?? ""
29271
- })
29272
- }), "Auto (node default)"]
29273
- }),
29274
- !isAutoSentinel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29275
- models,
29276
- value: step.modelId,
29277
- disabled: readOnly,
29278
- onChange: (id) => onChange({
29279
- ...step,
29280
- modelId: id
29281
- })
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
29282
29486
  })
29283
- ]
29487
+ })]
29284
29488
  }), schema?.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29285
29489
  field,
29286
29490
  allFields: schema.configSchema ?? [],
@@ -29347,7 +29551,7 @@ function resolveSettingValue(key, step, allFields) {
29347
29551
  const sibling = allFields.find((f) => "key" in f && f.key === key);
29348
29552
  return sibling && "default" in sibling ? sibling.default : void 0;
29349
29553
  }
29350
- function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29554
+ function ConfigSchemaField({ field, allFields, step, disabled, onChange, hideModelBoundSourceRow }) {
29351
29555
  const applicability = "modelApplicability" in field ? field.modelApplicability : void 0;
29352
29556
  const inapplicableReason = applicability?.inapplicable?.[step.modelId];
29353
29557
  const control = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaFieldControl, {
@@ -29356,7 +29560,8 @@ function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29356
29560
  step,
29357
29561
  disabled: disabled || inapplicableReason !== void 0,
29358
29562
  modelScopedOptions: applicability?.optionsByModel?.[step.modelId],
29359
- onChange
29563
+ onChange,
29564
+ hideModelBoundSourceRow: hideModelBoundSourceRow === true
29360
29565
  });
29361
29566
  if (inapplicableReason === void 0) return control;
29362
29567
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -29367,7 +29572,7 @@ function ConfigSchemaField({ field, allFields, step, disabled, onChange }) {
29367
29572
  })]
29368
29573
  });
29369
29574
  }
29370
- function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScopedOptions, onChange }) {
29575
+ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScopedOptions, onChange, hideModelBoundSourceRow }) {
29371
29576
  const showWhen = field.type !== "separator" && field.type !== "info" ? field.showWhen : void 0;
29372
29577
  if (showWhen) {
29373
29578
  const { field: condField, equals, notEquals, in: inList, notIn } = showWhen;
@@ -29417,7 +29622,8 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29417
29622
  onChangeSettings: (settings) => onChange({
29418
29623
  ...step,
29419
29624
  settings
29420
- })
29625
+ }),
29626
+ hideSourceRow: hideModelBoundSourceRow
29421
29627
  });
29422
29628
  if (field.type === "slider" || field.type === "number") {
29423
29629
  const raw = step.settings?.[field.key];
@@ -29503,204 +29709,247 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29503
29709
  });
29504
29710
  return null;
29505
29711
  }
29506
- function ThreeStateButton({ label, active, variant, subtext, onClick }) {
29507
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
29508
- type: "button",
29509
- onClick,
29510
- className: cn("px-2 py-1 text-[10px] font-medium rounded transition-colors", active ? variant === "on" ? "bg-green-500/20 text-green-400" : variant === "off" ? "bg-red-500/20 text-red-400" : "bg-primary/20 text-primary" : "text-foreground-subtle hover:bg-surface-hover"),
29511
- children: [label, subtext && active && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29512
- className: "ml-1 opacity-60",
29513
- children: [
29514
- "(",
29515
- subtext,
29516
- ")"
29517
- ]
29518
- })]
29519
- });
29520
- }
29521
29712
  //#endregion
29522
- //#region src/composites/resolve-step-default-model.ts
29523
- function resolveStepDefaultModel(catalog, format) {
29524
- const hasFormatBuild = (modelId) => catalog.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
29525
- const declared = catalog.defaultModelIdByFormat?.[format];
29526
- if (declared !== void 0 && hasFormatBuild(declared)) return declared;
29527
- if (hasFormatBuild(catalog.defaultModelId)) return catalog.defaultModelId;
29528
- const available = catalog.models.filter((m) => m.formats[format] !== void 0 && m.legacy !== true);
29529
- if (available.length === 0) return catalog.defaultModelId;
29530
- return [...available].toSorted((a, b) => (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity))[0].id;
29713
+ //#region src/composites/step-inspector/setting-values.ts
29714
+ function isValueField(field) {
29715
+ return field.type !== "separator" && field.type !== "info" && field.type !== "group";
29531
29716
  }
29532
- /**
29533
- * The model a matrix CELL shows as this step's effective default on `device`.
29534
- *
29535
- * The device's ELECTED model (`device.defaultModelId`, the per-device
29536
- * object-detection election) applies ONLY when this step's catalog actually
29537
- * contains it — `slot === 'detector'` alone is too wide, because
29538
- * package-detection shares the detector slot with the root step. Bit live
29539
- * 2026-08-08: rfdetr-package became the package step's default and every
29540
- * matrix cell still showed the device's yolo as the "selected" model, because
29541
- * the election was applied slot-wide. Membership is the principled gate: an
29542
- * elected model this step cannot run is never displayed for it.
29543
- */
29544
- function resolveEffectiveDefaultModel(catalog, device) {
29545
- const format = device?.format;
29546
- if (format === void 0) return void 0;
29547
- const elected = device?.defaultModelId;
29548
- return catalog.slot === "detector" && elected !== void 0 && catalog.models.some((m) => m.id === elected) ? elected : resolveStepDefaultModel(catalog, format);
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;
29549
29720
  }
29550
29721
  /**
29551
- * The models a step may run on an engine `format` — the ONE derivation of the
29552
- * option list. An unknown format yields NOTHING rather than everything: a
29553
- * picker that offered a model with no build for the device would write a pin
29554
- * the runner cannot honour.
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.
29555
29725
  */
29556
- function stepModelOptions(models, format) {
29557
- if (format === void 0) return [];
29558
- 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
+ };
29559
29736
  }
29560
- /** Can this step run on `format` at all? (`false` ⇒ the caller renders "⚠ skip"/"—".) */
29561
- function stepHasModelForFormat(models, format) {
29562
- return stepModelOptions(models, format).length > 0;
29737
+ function trimNumber(n) {
29738
+ return n.toFixed(2).replace(/\.?0+$/, "");
29563
29739
  }
29564
- /**
29565
- * The model a step ACTUALLY runs for a (device, camera) — the whole effective
29566
- * chain in one place: per-camera override → per-(node,device) base → the
29567
- * resolved default (`resolveEffectiveDefaultModel`).
29568
- *
29569
- * Every surface calls this: the matrix cell, and the drawer's picker seed. They
29570
- * are the same number, and the only way to keep them the same number is to have
29571
- * one function produce it. A surface that owns fewer layers omits them (the node
29572
- * matrix passes no `override`) — it does NOT get its own resolver.
29573
- */
29574
- function resolveEffectiveStepModel(input) {
29575
- return input.override ?? input.base ?? resolveEffectiveDefaultModel(input.catalog, input.device);
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);
29576
29757
  }
29577
29758
  //#endregion
29578
- //#region src/composites/root-model-source.tsx
29579
- /**
29580
- * "Use the root model" — the step editor's control for a step the camera's
29581
- * ROOT detection model can supply (D730).
29582
- *
29583
- * The flag (`useRootModel`, in the step's own settings) is an operator INTENT
29584
- * kept beside the step's model: checking it never changes the model, and a
29585
- * root that cannot supply the step leaves the step running its own model while
29586
- * the flag waits, inactive, for a root that can. Whether it is in force is
29587
- * `resolveRootSupply` — the same function the runner decides with.
29588
- */
29589
- /** The editor's view of the flag for one step, or `null` for a step no root can supply. */
29590
- function rootSourceState(stepId, settings, rootModel) {
29591
- if (!(0, _camstack_types.isRootSuppliableStepId)(stepId)) return null;
29592
- return (0, _camstack_types.resolveRootSupply)(stepId, settings, rootModel?.supplies);
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}`;
29593
29769
  }
29594
- /** The settings bag with the flag set — every other key untouched. */
29595
- function withUseRootModel(settings, value) {
29770
+ function displayStep(addon, modelId, settings) {
29596
29771
  return {
29597
- ...settings,
29598
- [_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 }
29599
29781
  };
29600
29782
  }
29601
- /** What the operator must know about a supply, beyond "it is on". */
29602
- function supplyNotes(supply) {
29603
- if (supply.kind === "label") {
29604
- const notes = supply.uncovered.length > 0 ? [`Not produced by this model: ${supply.uncovered.join(", ")}.`] : [];
29605
- if (supply.stepId === "package-detection") return [...notes, "The root model's own package boxes become the package result at this step's threshold; this step's zone-crop view is not used, so small parcels the full-frame model misses stay missed."];
29606
- return [...notes, "This step's class list decides which of the root's labels become the fine class; a switched-off class stays at its macro."];
29607
- }
29608
- if (supply.kind === "mask") return ["Masks come from the root model: each detection carries the root's own instance mask, so this step's model does not run. A root mask follows the model's mask grid (a quarter of its input), coarser than this step's own per-crop mask."];
29609
- if (supply.stepId === "face-detection") return ["Face boxes come from the root model. Each is re-found by this step's face detector in a square around it before recognition, so only a face it finds is recognised; a box with no face in it is dropped."];
29610
- return ["Plate boxes come from the root model and the plate reader reads them. The crop margin around a root plate box has not been measured for this model yet."];
29611
- }
29612
- /** The reason an intended supply is not in force, in the operator's words. */
29613
- function inactiveText(state, rootModel) {
29614
- switch (state.inactiveReason) {
29615
- case "root-input-too-small": return `the current root model (${rootModel.name}) sees the frame too small to find faces reliably (it needs an input of 640 px or more).`;
29616
- case "root-does-not-declare":
29617
- case void 0: return `the current root model (${rootModel.name}) cannot supply this.`;
29618
- }
29619
- }
29620
- function RootModelSourceControl({ stepId, settings, rootModel, disabled, onChange }) {
29621
- if (rootModel === void 0) return null;
29622
- const state = rootSourceState(stepId, settings, rootModel);
29623
- if (state === null) return null;
29624
- const inactiveNote = state.inactive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29625
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29626
- "data-testid": "root-model-inactive",
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]",
29627
29814
  children: [
29628
- "Use the root model: inactive — ",
29629
- inactiveText(state, rootModel),
29630
- " 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
+ })
29631
29844
  ]
29632
- }) : null;
29633
- if (state.supply === void 0) return inactiveNote;
29634
- 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);
29635
29853
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29636
- className: "space-y-1",
29637
- "data-testid": "root-model-source",
29854
+ className: "space-y-2 text-xs",
29638
29855
  children: [
29639
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
29640
- 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",
29641
29858
  children: [
29642
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29643
- type: "checkbox",
29644
- checked,
29645
- disabled,
29646
- onChange: (e) => onChange(e.target.checked)
29859
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
29860
+ className: TEXT_SECTION_LABEL,
29861
+ children: "Settings"
29647
29862
  }),
29648
- "Use the root model (",
29649
- rootModel.name,
29650
- ")"
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
+ })
29651
29877
  ]
29652
29878
  }),
29653
- inactiveNote,
29654
- state.effective && supplyNotes(state.supply).map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29655
- className: "text-[10px] leading-snug text-foreground-subtle",
29656
- children: note
29657
- }, note))
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
+ })
29658
29920
  ]
29659
29921
  });
29660
29922
  }
29661
29923
  //#endregion
29662
29924
  //#region src/composites/agent-step-editor.tsx
29663
29925
  /**
29664
- * Per-(node,device) and per-camera editors must not surface knobs that the
29665
- * cluster already owns. Those fields still exist on the step's catalog
29666
- * schema so the runner can hydrate `child.settings`, but writing them here
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
29667
29929
  * would be a second store the overlay silently ignores.
29668
29930
  */
29669
- function nodeLocalConfigSchema(addon) {
29931
+ function stepLocalFields(addon) {
29670
29932
  const clusterKeys = new Set((0, _camstack_types.clusterStepSettingFieldsFor)(addon.id).map((field) => field.key));
29671
29933
  return (addon.configSchema ?? []).filter((field) => !clusterKeys.has(field.key));
29672
29934
  }
29673
- function ClusterSettingsHint({ addonId }) {
29674
- if ((0, _camstack_types.clusterStepSettingFieldsFor)(addonId).length === 0) return null;
29675
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29676
- className: "text-[10px] leading-snug text-foreground-subtle",
29677
- children: "Admission knobs for this step live in Pipeline → Cluster → Settings → Enrichment models. They apply to every camera — a per-device copy would fill the shared index from two policies."
29678
- });
29679
- }
29680
29935
  /**
29681
- * 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
29682
29937
  * field (D738) binds to. While the ROOT supplies the step's fine label (D741),
29683
29938
  * that is the root model: the step's threshold then gates the root's label, on
29684
29939
  * the root's score scale. Otherwise the step's own model: its pin, else the
29685
29940
  * model the row really runs (`effectiveModelId` — the device's hardware-aware
29686
29941
  * default when unpinned), else the step's flat catalog default.
29687
29942
  */
29688
- function settingsModelIdFor(addon, cfg, effectiveModelId, rootModel) {
29689
- if (rootModel !== void 0 && rootSourceState(addon.id, cfg.settings, rootModel)?.effective) return rootModel.modelId;
29690
- return cfg.modelId ?? effectiveModelId ?? addon.defaultModelId;
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;
29691
29946
  }
29692
- function buildDisplayStep(addon, cfg, modelId) {
29693
- return {
29694
- addonId: addon.id,
29695
- addonName: addon.name,
29696
- slot: addon.slot,
29697
- inputClasses: [...addon.inputClasses],
29698
- outputClasses: [...addon.outputClasses],
29699
- enabled: cfg.enabled,
29700
- modelId,
29701
- children: [],
29702
- settings: { ...cfg.settings }
29703
- };
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
+ });
29704
29953
  }
29705
29954
  /**
29706
29955
  * Shown in place of the model picker while the root model supplies the step:
@@ -29721,233 +29970,57 @@ function RootSuppliedModelNote({ rootModel, stepModelId }) {
29721
29970
  ]
29722
29971
  });
29723
29972
  }
29724
- function computeEffective(mode, agentDefault, patch) {
29725
- if (mode !== "device" || !patch) return agentDefault;
29726
- return {
29727
- enabled: agentDefault.enabled,
29728
- modelId: patch.modelId ?? agentDefault.modelId,
29729
- settings: patch.settings ?? agentDefault.settings
29730
- };
29731
- }
29732
- function AgentModeEditor({ addon, agentDefault, modelsForFormat, engineFormat, effectiveModelId, rootModel, onChangeAgentConfig }) {
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]);
29733
29982
  const update = (patch) => {
29734
29983
  onChangeAgentConfig?.({
29735
29984
  ...agentDefault,
29736
29985
  ...patch
29737
29986
  });
29738
29987
  };
29739
- const localSchema = nodeLocalConfigSchema(addon);
29988
+ const fields = stepLocalFields(addon);
29740
29989
  const formatDefault = addon.defaultModelIdByFormat?.[engineFormat] ?? addon.defaultModelId;
29741
29990
  const rootSupplied = rootSourceState(addon.id, agentDefault.settings, rootModel)?.effective === true;
29991
+ const runningModel = agentDefault.modelId ?? effectiveModelId;
29742
29992
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29743
29993
  className: "space-y-4 text-xs",
29744
29994
  children: [
29745
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29746
- stepId: addon.id,
29747
- settings: agentDefault.settings,
29748
- rootModel,
29749
- onChange: (v) => update({ settings: withUseRootModel(agentDefault.settings, v) })
29750
- }),
29751
29995
  rootSupplied && rootModel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootSuppliedModelNote, {
29752
29996
  rootModel,
29753
- stepModelId: agentDefault.modelId ?? effectiveModelId
29997
+ stepModelId: runningModel
29754
29998
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29755
29999
  className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
29756
30000
  children: "Model"
29757
30001
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29758
30002
  models: modelsForFormat,
29759
- value: agentDefault.modelId ?? effectiveModelId ?? "",
30003
+ value: runningModel ?? "",
29760
30004
  defaultModelId: formatDefault,
29761
30005
  onChange: (modelId) => update({ modelId })
29762
30006
  })] }),
29763
- localSchema.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29764
- className: "pt-2 border-t border-border/50 space-y-3",
29765
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29766
- className: "flex items-center justify-between",
29767
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29768
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29769
- children: "Settings"
29770
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29771
- fields: localSchema,
29772
- modelId: settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel),
29773
- settings: agentDefault.settings,
29774
- onReset: (settings) => update({ settings })
29775
- })]
29776
- }), localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29777
- field,
29778
- allFields: localSchema,
29779
- step: buildDisplayStep(addon, agentDefault, settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel)),
29780
- disabled: false,
29781
- onChange: (next) => update({ settings: { ...next.settings } })
29782
- }, field.key))]
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
+ })
29783
30019
  }),
29784
30020
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterSettingsHint, { addonId: addon.id })
29785
30021
  ]
29786
30022
  });
29787
30023
  }
29788
- function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, modelsForFormat, engineFormat, effectiveModelId, rootModel, onChangePatch }) {
29789
- const effective = computeEffective("device", agentDefault, currentPatch);
29790
- const inheritedModelId = agentDefault.modelId ?? effectiveModelId;
29791
- const formatDefault = addon.defaultModelIdByFormat?.[engineFormat] ?? addon.defaultModelId;
29792
- const commitPatch = (next) => {
29793
- const hasAnyField = next.modelId !== void 0 || next.settings !== void 0;
29794
- onChangePatch?.(hasAnyField ? next : null);
29795
- };
29796
- const setModel = (modelId) => {
29797
- const { modelId: _prev, ...rest } = currentPatch ?? {};
29798
- commitPatch(modelId === void 0 ? rest : {
29799
- ...rest,
29800
- modelId
29801
- });
29802
- };
29803
- const setSettings = (settings) => {
29804
- const { settings: _prev, ...rest } = currentPatch ?? {};
29805
- commitPatch(settings === void 0 ? rest : {
29806
- ...rest,
29807
- settings
29808
- });
29809
- };
29810
- const modelOverridden = currentPatch?.modelId !== void 0;
29811
- const localSchema = nodeLocalConfigSchema(addon);
29812
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29813
- className: "space-y-4 text-xs",
29814
- children: [
29815
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29816
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29817
- children: [
29818
- "Provisioning for ",
29819
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29820
- className: "font-semibold text-foreground",
29821
- children: agentNodeId
29822
- }),
29823
- " — model + settings only. Whether this camera runs the step is set by its per-camera gate above."
29824
- ]
29825
- }),
29826
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootModelSourceControl, {
29827
- stepId: addon.id,
29828
- settings: effective.settings,
29829
- rootModel,
29830
- onChange: (v) => setSettings(withUseRootModel(effective.settings, v))
29831
- }),
29832
- rootSourceState(addon.id, effective.settings, rootModel)?.effective === true && rootModel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootSuppliedModelNote, {
29833
- rootModel,
29834
- stepModelId: effective.modelId ?? inheritedModelId
29835
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
29836
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29837
- className: "flex items-center justify-between mb-1",
29838
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29839
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29840
- children: "Model"
29841
- }), modelOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29842
- type: "button",
29843
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29844
- onClick: () => setModel(void 0),
29845
- children: "Clear"
29846
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29847
- type: "button",
29848
- className: "text-[10px] text-primary hover:underline",
29849
- onClick: () => setModel(inheritedModelId),
29850
- children: "Override"
29851
- })]
29852
- }),
29853
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
29854
- models: modelsForFormat,
29855
- disabled: !modelOverridden,
29856
- value: (modelOverridden ? currentPatch?.modelId ?? inheritedModelId : inheritedModelId) ?? "",
29857
- defaultModelId: formatDefault,
29858
- onChange: (modelId) => setModel(modelId)
29859
- }),
29860
- !modelOverridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29861
- className: "text-[10px] text-foreground-subtle mt-1",
29862
- children: ["inherits from ", agentNodeId]
29863
- })
29864
- ] }),
29865
- localSchema.length > 0 && (() => {
29866
- const settingsOverridden = currentPatch?.settings !== void 0;
29867
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29868
- className: "pt-2 border-t border-border/50 space-y-3",
29869
- children: [
29870
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29871
- className: "flex items-center justify-between",
29872
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29873
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29874
- children: "Settings"
29875
- }), settingsOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29876
- className: "flex items-center gap-3",
29877
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29878
- fields: localSchema,
29879
- modelId: settingsModelIdFor(addon, effective, inheritedModelId, rootModel),
29880
- settings: currentPatch?.settings,
29881
- onReset: (settings) => setSettings(settings)
29882
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29883
- type: "button",
29884
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29885
- onClick: () => setSettings(void 0),
29886
- children: "Clear"
29887
- })]
29888
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29889
- type: "button",
29890
- className: "text-[10px] text-primary hover:underline",
29891
- onClick: () => setSettings({ ...agentDefault.settings }),
29892
- children: "Override"
29893
- })]
29894
- }),
29895
- localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29896
- field,
29897
- allFields: localSchema,
29898
- step: buildDisplayStep(addon, effective, settingsModelIdFor(addon, effective, inheritedModelId, rootModel)),
29899
- disabled: !settingsOverridden,
29900
- onChange: (next) => setSettings({ ...next.settings })
29901
- }, field.key)),
29902
- !settingsOverridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29903
- className: "text-[10px] text-foreground-subtle",
29904
- children: ["inherits from ", agentNodeId]
29905
- })
29906
- ]
29907
- });
29908
- })(),
29909
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterSettingsHint, { addonId: addon.id }),
29910
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29911
- className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
29912
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29913
- className: "font-semibold text-foreground mb-0.5",
29914
- children: "Effective provisioning preview"
29915
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29916
- className: "font-mono truncate",
29917
- children: [
29918
- addon.id,
29919
- " · model: ",
29920
- effective.modelId ?? inheritedModelId ?? "Auto"
29921
- ]
29922
- })]
29923
- })
29924
- ]
29925
- });
29926
- }
29927
- function AgentStepEditor(props) {
29928
- const { mode, addon, agentDefault, agentNodeId, currentPatch, onChangeAgentConfig, onChangePatch, engineFormat, effectiveModelId, rootModel } = props;
29929
- const modelsForFormat = (0, react$1.useMemo)(() => stepModelOptions(addon.models, engineFormat), [addon.models, engineFormat]);
29930
- if (mode === "agent") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentModeEditor, {
29931
- addon,
29932
- agentDefault,
29933
- modelsForFormat,
29934
- engineFormat,
29935
- effectiveModelId,
29936
- ...rootModel !== void 0 ? { rootModel } : {},
29937
- onChangeAgentConfig
29938
- });
29939
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceModeEditor, {
29940
- addon,
29941
- agentDefault,
29942
- agentNodeId,
29943
- currentPatch,
29944
- modelsForFormat,
29945
- engineFormat,
29946
- effectiveModelId,
29947
- ...rootModel !== void 0 ? { rootModel } : {},
29948
- onChangePatch
29949
- });
29950
- }
29951
30024
  //#endregion
29952
30025
  //#region src/primitives/icon-button.tsx
29953
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", {
@@ -33862,117 +33935,6 @@ async function waitForIceGathering(pc) {
33862
33935
  });
33863
33936
  }
33864
33937
  //#endregion
33865
- //#region src/lib/responsive.ts
33866
- /**
33867
- * Responsive design tokens — three-tier system.
33868
- *
33869
- * The admin-ui targets three width tiers; every component should pick
33870
- * the right token from this file instead of hand-coding breakpoints.
33871
- * Adopting the tokens uniformly is what makes the app feel consistent
33872
- * across operator devices (mobile / tablet / desktop) and what unlocks
33873
- * targeted tuning (e.g. shrink every label by 1px in one place).
33874
- *
33875
- * Tier definitions (Tailwind breakpoints):
33876
- *
33877
- * • **L1 — Compact** (`< 640px`, default = mobile portrait)
33878
- * Single column, tight padding, smallest text, icon-only buttons
33879
- * where labels can be inferred. The operator can still see every
33880
- * stat / action — no horizontal scroll, no clipped text. The user
33881
- * directive: "molta informazione in poco spazio quando la width è
33882
- * bassa." [a lot of information in little space when the width is low.]
33883
- *
33884
- * • **L2 — Standard** (`sm` — `640–1023px`, tablet + small laptop)
33885
- * 2-column grids, side panels become visible, button labels return.
33886
- * Targets ~768px–1024px viewports — the most common operator sizes.
33887
- *
33888
- * • **L3 — Spacious** (`lg+` — `≥ 1024px`, desktop)
33889
- * 4-column grids, generous padding, full-width header text,
33890
- * side-by-side composite layouts. The operator has elbow room.
33891
- *
33892
- * Conventions:
33893
- * - **Always start from L1** — the bare class is always the compact
33894
- * variant, then `sm:` adds room at 640px, `lg:` at 1024px. No
33895
- * desktop-first breakpoints; no max-width queries.
33896
- * - **Token strings are static** — kept as `as const` so callers can
33897
- * compose them via `cn()` / template literals without running a
33898
- * class generator at runtime.
33899
- * - **Pair the token with a comment** — the comment in the file
33900
- * names every breakpoint it crosses, so a reader can predict the
33901
- * layout without going to Tailwind docs.
33902
- */
33903
- /** Container padding for a card-style section (header + body). */
33904
- var SECTION_CARD = "rounded-lg border border-border bg-surface overflow-hidden";
33905
- /** Header strip inside a card (between border-b lines). */
33906
- var SECTION_HEADER = "border-b border-border px-3 sm:px-4 py-1.5 sm:py-2";
33907
- /** Body padding for a card-style section. */
33908
- var SECTION_BODY = "p-2 sm:p-3 lg:p-4";
33909
- /** Gap between cards in a vertical stack. */
33910
- var STACK_GAP = "space-y-3 sm:space-y-4";
33911
- /** Gap between cards in a grid (4 cells / row at lg). */
33912
- var GRID_GAP = "gap-2 sm:gap-3";
33913
- /** Row padding inside a divided list (e.g. InfoRow). */
33914
- var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
33915
- /** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
33916
- var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
33917
- /** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
33918
- var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
33919
- /** Side-by-side layout that stacks below md (canvas + side panel). */
33920
- var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
33921
- /** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
33922
- var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
33923
- /**
33924
- * The tier boundary at which a settings label/value row stops stacking.
33925
- *
33926
- * Below it (L1 — compact) the row is ONE column: label on its own line, value
33927
- * on the next at full width. Above it the row is label-left / value-right as
33928
- * before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
33929
- * value beside it; a phone has neither, and the failure mode is not "cramped"
33930
- * but "the value is the part that gets cut" — the one thing the operator
33931
- * opened the page to read.
33932
- */
33933
- var SETTING_ROW_STACK_BREAKPOINT = "sm";
33934
- /** Outer row: single column at L1, label/value columns from `sm` up. */
33935
- var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
33936
- /** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
33937
- var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
33938
- /**
33939
- * Value lane — the value text plus its trailing affordances (copy / reveal).
33940
- * `w-full` at L1 so the value owns the whole row once the label is above it;
33941
- * `sm:flex-1` makes it share the row again from `sm` up.
33942
- */
33943
- var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
33944
- /**
33945
- * The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
33946
- * at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
33947
- * desktop form, where the row is wide enough for truncation to be a choice
33948
- * rather than data loss.
33949
- */
33950
- var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
33951
- /** Section header label (uppercase tracking-wider). */
33952
- var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
33953
- /** Field label inside a row. */
33954
- var TEXT_FIELD_LABEL = "text-[11px] sm:text-xs text-foreground";
33955
- /** Sub-label / hint text. */
33956
- var TEXT_HINT = "text-[10px] text-foreground-subtle";
33957
- /** Primary value (a metric, badge, …). */
33958
- var TEXT_VALUE = "text-[11px] sm:text-xs font-medium text-foreground";
33959
- /** Big metric value (StatCard). */
33960
- var TEXT_METRIC = "text-base sm:text-lg font-semibold leading-tight text-foreground";
33961
- /** Default compact button. */
33962
- var BTN_COMPACT = "inline-flex items-center justify-center gap-1 rounded border border-border bg-surface px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-foreground hover:bg-surface-hover transition-colors disabled:opacity-40";
33963
- /** Primary compact button. */
33964
- var BTN_COMPACT_PRIMARY = "inline-flex items-center justify-center gap-1 rounded border border-primary/40 bg-primary/15 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-primary hover:bg-primary/25 transition-colors disabled:opacity-40";
33965
- /** Danger compact button. */
33966
- var BTN_COMPACT_DANGER = "inline-flex items-center justify-center gap-1 rounded border border-danger/40 bg-danger/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-danger hover:bg-danger/20 transition-colors disabled:opacity-40";
33967
- /** Warning compact button (e.g. cancel-drawing). */
33968
- var BTN_COMPACT_WARNING = "inline-flex items-center justify-center gap-1 rounded border border-warning/40 bg-warning/10 px-1.5 py-0.5 sm:px-2 sm:py-1 text-[10px] sm:text-[11px] font-medium text-warning hover:bg-warning/20 transition-colors disabled:opacity-40";
33969
- /** Compact text/select/number input. */
33970
- var INPUT_COMPACT = "w-full rounded border border-border bg-background px-1.5 py-0.5 sm:px-2 sm:py-1 text-[11px] sm:text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none";
33971
- /** Chip-style toggle (zone / class selectors in ZoneRulesEditor). */
33972
- var CHIP_BASE = "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors";
33973
- var CHIP_ACTIVE = "border-primary/50 bg-primary/15 text-primary";
33974
- var CHIP_INACTIVE = "border-border bg-surface text-foreground-subtle hover:bg-surface-hover";
33975
- //#endregion
33976
33938
  //#region src/composites/data-table-layout.ts
33977
33939
  /**
33978
33940
  * At this many columns and above, `auto` switches a narrow viewport to cards.
@@ -55565,16 +55527,16 @@ function cellButtonClass(state, isSelected, extra) {
55565
55527
  //#endregion
55566
55528
  //#region src/composites/device-step-matrix.tsx
55567
55529
  /**
55568
- * DeviceStepMatrix — the per-camera "Detection pipeline → Steps" surface (#33)
55569
- * 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.
55570
55533
  *
55571
- * Column 1 (STICKY): the indented step tree. Each step carries its slot +
55572
- * name + class-flow chips PLUS a per-camera, node-independent GATE control
55573
- * with THREE states — On / Off / Default (inherit). `package-detection` is
55574
- * zone-driven, so it shows a "Zones" badge instead of a gate.
55575
- * Columns 2..N (one per online node): the provisioned model chip (click →
55576
- * the model/config picker), a clickable "⚠ skip" when the step is gated ON
55577
- * but the node has no compatible model, or "—" when N/A.
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).
55578
55540
  *
55579
55541
  * Responsive, container-measured (never a global media query, so it also works
55580
55542
  * embedded):
@@ -55585,77 +55547,20 @@ function cellButtonClass(state, isSelected, extra) {
55585
55547
  * single-node view: a node selector picks ONE node and the matrix renders
55586
55548
  * `step-tree × that-one-node`.
55587
55549
  */
55588
- /** Wider than the cluster matrix's 18rem: col-1 also hosts the gate control. */
55589
- var STEP_COL_REM$1 = 21;
55590
- var STEP_COL_PX = STEP_COL_REM$1 * 16;
55550
+ /** The sticky step column. */
55551
+ var STEP_COL_REM = 18;
55552
+ var STEP_COL_PX = STEP_COL_REM * 16;
55591
55553
  /**
55592
- * Narrow (mobile) step-column width. On a phone the wide 21rem column would
55593
- * occlude nearly the whole viewport, so the single-node layout pins a slimmer
55594
- * sticky step column and lets the device columns scroll horizontally beside it
55595
- * (both fully readable) rather than cramming every column into the viewport.
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.
55596
55557
  */
55597
55558
  var STEP_COL_SINGLE_REM = 11;
55598
- function activeChoice(gate) {
55599
- if (!gate.explicit) return "default";
55600
- return gate.effectiveOn ? "on" : "off";
55601
- }
55602
- /**
55603
- * Tri-state segmented control. `Default` inherits the catalog value (shown in
55604
- * the caption); `On`/`Off` pin an explicit per-camera override. Touch-friendly
55605
- * 28px segments.
55606
- */
55607
- function GateControl({ gate, busy, onSet }) {
55608
- const active = activeChoice(gate);
55609
- const seg = (choice, label, value, activeClass) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55610
- type: "button",
55611
- "aria-pressed": active === choice,
55612
- disabled: busy,
55613
- onClick: () => onSet(value),
55614
- className: cn("flex-1 px-2 py-1 text-[11px] font-medium leading-none transition-colors", "min-h-[26px]", active === choice ? activeClass : "text-foreground-subtle hover:bg-muted/60", busy && "opacity-50 cursor-not-allowed"),
55615
- children: label
55616
- });
55617
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55618
- className: "flex flex-col gap-1",
55619
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55620
- className: "inline-flex w-fit max-w-full self-start overflow-hidden rounded-md border border-border",
55621
- children: [
55622
- seg("default", "Auto", null, "bg-primary/15 text-primary"),
55623
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
55624
- seg("on", "On", true, "bg-emerald-500 text-white"),
55625
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
55626
- seg("off", "Off", false, "bg-foreground-subtle/70 text-white")
55627
- ]
55628
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55629
- className: "text-[10px] leading-none text-foreground-subtle",
55630
- children: active === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
55631
- "= default (",
55632
- gate.defaultOn ? "on" : "off",
55633
- ")"
55634
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: ["overridden · default ", gate.defaultOn ? "on" : "off"] })
55635
- })]
55636
- });
55637
- }
55638
- function ZoneBadge() {
55639
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55640
- className: "flex flex-col gap-1",
55641
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55642
- className: "inline-flex w-fit items-center gap-1 rounded-md border border-violet-500/40 bg-violet-500/10 px-2 py-1 text-[11px] font-medium text-violet-400",
55643
- children: "Zones"
55644
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55645
- className: "text-[10px] leading-none text-foreground-subtle",
55646
- children: "driven by package zones"
55647
- })]
55648
- });
55649
- }
55650
- function StepGateCell({ node, depth, gate, busy, onSet, hideGate }) {
55651
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55652
- className: "sticky left-0 z-10 flex flex-col gap-2 border-b border-border bg-surface px-3 py-2.5 text-xs min-w-0",
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",
55653
55562
  style: { paddingLeft: `${12 + depth * 14}px` },
55654
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), hideGate ? null : gate.kind === "zone-driven" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoneBadge, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GateControl, {
55655
- gate,
55656
- busy,
55657
- onSet
55658
- })]
55563
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node })
55659
55564
  });
55660
55565
  }
55661
55566
  /**
@@ -55694,18 +55599,18 @@ function NodeSelectorBar({ nodes, selected, onSelect }) {
55694
55599
  ]
55695
55600
  });
55696
55601
  }
55697
- function MatrixGrid$1({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single, hideGate }) {
55602
+ function MatrixGrid({ tree, agents, getCellState, onCellClick, selectedCell, single }) {
55698
55603
  const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
55699
55604
  const groups = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
55700
55605
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55701
55606
  className: "overflow-auto rounded border border-border",
55702
55607
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55703
55608
  className: "grid w-max",
55704
- 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)` },
55705
55610
  children: [
55706
55611
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55707
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",
55708
- children: hideGate ? "Step" : "Step · gate"
55613
+ children: "Step"
55709
55614
  }),
55710
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", {
55711
55616
  className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2",
@@ -55721,26 +55626,18 @@ function MatrixGrid$1({ tree, agents, gateFor, getCellState, onCellClick, select
55721
55626
  node,
55722
55627
  depth,
55723
55628
  agents,
55724
- gate: gateFor(node.addonId),
55725
55629
  getCellState,
55726
55630
  onCellClick,
55727
- selectedCell,
55728
- onSetGate,
55729
- gateBusy,
55730
- hideGate
55631
+ selectedCell
55731
55632
  }, node.addonId))
55732
55633
  ]
55733
55634
  })
55734
55635
  });
55735
55636
  }
55736
- function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, hideGate }) {
55737
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepGateCell, {
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, {
55738
55639
  node,
55739
- depth,
55740
- gate,
55741
- busy: gateBusy,
55742
- onSet: (next) => onSetGate(node.addonId, next),
55743
- hideGate
55640
+ depth
55744
55641
  }), agents.map((a) => {
55745
55642
  const state = getCellState(node.addonId, a);
55746
55643
  const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
@@ -55754,7 +55651,7 @@ function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selec
55754
55651
  }, agentColumnKey(a));
55755
55652
  })] });
55756
55653
  }
55757
- function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange, hideGate }) {
55654
+ function DeviceStepMatrix({ tree, agents, getCellState, onCellClick, selectedCell, fallbackNodeId, onFallbackNodeChange }) {
55758
55655
  const containerRef = (0, react$1.useRef)(null);
55759
55656
  const [width, setWidth] = (0, react$1.useState)(0);
55760
55657
  (0, react$1.useLayoutEffect)(() => {
@@ -55780,17 +55677,13 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
55780
55677
  nodes,
55781
55678
  selected: selectedNode.agentNodeId,
55782
55679
  onSelect: onFallbackNodeChange
55783
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid$1, {
55680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
55784
55681
  tree,
55785
55682
  agents: shownAgents,
55786
- gateFor,
55787
55683
  getCellState,
55788
55684
  onCellClick,
55789
55685
  selectedCell,
55790
- onSetGate,
55791
- gateBusy,
55792
- single: single && selectedNode !== null,
55793
- hideGate
55686
+ single: single && selectedNode !== null
55794
55687
  })]
55795
55688
  });
55796
55689
  }
@@ -56820,17 +56713,7 @@ function PlaceholderStep({ addon, onClick }) {
56820
56713
  })
56821
56714
  });
56822
56715
  }
56823
- function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateId, onSelectTemplate, onSaveTemplate, onUpdateTemplate, onDeleteTemplate, readOnly = false, excludeAddons = [], hideTemplates = false, showOnlyEnabled = false, showSlotLabels = false, toggleMode = "simple", overrides = {}, onOverrideChange, inheritedEnabledByAddon = {}, hideModelAndSettings = false, allowAutoModel = false }) {
56824
- const stepExtraProps = (addonId) => ({
56825
- toggleMode,
56826
- hideModelAndSettings,
56827
- allowAutoModel,
56828
- ...toggleMode === "threeState" ? {
56829
- overrideState: overrides[addonId] !== void 0 ? overrides[addonId] : null,
56830
- onOverrideChange: onOverrideChange ? (next) => onOverrideChange(addonId, next) : void 0,
56831
- inheritedEnabled: inheritedEnabledByAddon[addonId]
56832
- } : {}
56833
- });
56716
+ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateId, onSelectTemplate, onSaveTemplate, onUpdateTemplate, onDeleteTemplate, readOnly = false, excludeAddons = [], hideTemplates = false, showOnlyEnabled = false, showSlotLabels = false }) {
56834
56717
  const excluded = (0, react$1.useMemo)(() => new Set(excludeAddons), [excludeAddons]);
56835
56718
  const schemaMap = (0, react$1.useMemo)(() => buildSchemaMap(schema), [schema]);
56836
56719
  const [warnings, setWarnings] = (0, react$1.useState)([]);
@@ -56937,8 +56820,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56937
56820
  allSchemas: schemaMap,
56938
56821
  onChange: handleStepChange,
56939
56822
  onDelete: readOnly ? void 0 : (id) => onChange(steps.filter((s) => s.addonId !== id)),
56940
- readOnly,
56941
- ...stepExtraProps(step.addonId)
56823
+ readOnly
56942
56824
  }), (step.children.length > 0 || childPlaceholders.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56943
56825
  className: "ml-4 pl-3 border-l border-border/60 space-y-1.5",
56944
56826
  children: [
@@ -56967,8 +56849,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56967
56849
  children: step.children.filter((c) => c.addonId !== id)
56968
56850
  });
56969
56851
  },
56970
- readOnly,
56971
- ...stepExtraProps(child.addonId)
56852
+ readOnly
56972
56853
  }), (child.children.length > 0 || childChildPlaceholders.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56973
56854
  className: "ml-4 pl-3 border-l border-border/40 space-y-1.5",
56974
56855
  children: [
@@ -56999,8 +56880,7 @@ function PipelineBuilder({ schema, steps, onChange, templates, selectedTemplateI
56999
56880
  } : c)
57000
56881
  });
57001
56882
  },
57002
- readOnly,
57003
- ...stepExtraProps(grandchild.addonId)
56883
+ readOnly
57004
56884
  }, grandchild.addonId)),
57005
56885
  !readOnly && childChildPlaceholders.map((addon) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlaceholderStep, {
57006
56886
  addon,
@@ -57160,179 +57040,6 @@ function PipelineRuntimeSelector({ options, value, onChange }) {
57160
57040
  });
57161
57041
  }
57162
57042
  //#endregion
57163
- //#region src/composites/pipeline-tree-matrix.tsx
57164
- var STEP_COL_REM = 18;
57165
- function QuickToggle({ enabled, onChange, disabled }) {
57166
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57167
- type: "button",
57168
- role: "switch",
57169
- "aria-checked": enabled,
57170
- disabled,
57171
- onClick: (e) => {
57172
- e.stopPropagation();
57173
- onChange(!enabled);
57174
- },
57175
- className: cn("relative inline-flex h-3.5 w-7 shrink-0 items-center rounded-full transition-colors", enabled ? "bg-emerald-500" : "bg-foreground-subtle/30", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
57176
- title: enabled ? "Click to disable" : "Click to enable",
57177
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-3 w-3 rounded-full bg-white shadow transition-transform", enabled ? "translate-x-3.5" : "translate-x-0.5") })
57178
- });
57179
- }
57180
- function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell, toggleProps }) {
57181
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57182
- className: "sticky left-0 z-10 bg-surface px-3 py-2 border-b border-border text-xs flex items-start gap-2 min-w-0",
57183
- style: { paddingLeft: `${12 + depth * 14}px` },
57184
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), toggleProps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57185
- className: "pl-2 pt-0.5 shrink-0",
57186
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickToggle, {
57187
- enabled: toggleProps.enabled,
57188
- onChange: toggleProps.onChange,
57189
- disabled: toggleProps.disabled
57190
- })
57191
- })]
57192
- }), agents.map((a) => {
57193
- const state = getCellState(node.addonId, a);
57194
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57195
- type: "button",
57196
- onClick: () => onCellClick(node.addonId, a),
57197
- className: cellButtonClass(state, selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a), "min-w-0 overflow-hidden px-3 py-1.5 border-b border-l border-border"),
57198
- children: renderCellContent(state)
57199
- }, agentColumnKey(a));
57200
- })] });
57201
- }
57202
- /**
57203
- * Wide layout: horizontally self-scrolling matrix. The scroll lives INSIDE
57204
- * this `overflow-auto` box (sticky first column + sticky header row) so the
57205
- * page body never scrolls sideways. Column tracks are fixed px widths so the
57206
- * grid is static regardless of content.
57207
- */
57208
- function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57209
- const gridTemplate = `${STEP_COL_REM}rem repeat(${agents.length}, 13rem)`;
57210
- const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
57211
- const onlyAgent = agents[0];
57212
- const groups = groupAgentColumns(agents);
57213
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57214
- className: "overflow-auto border border-border rounded",
57215
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57216
- className: "grid w-max",
57217
- style: { gridTemplateColumns: gridTemplate },
57218
- children: [
57219
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57220
- className: "sticky top-0 left-0 z-20 bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle border-b border-border",
57221
- children: "Step"
57222
- }),
57223
- groups.map((group) => group.grouped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedAgentHeader, { group }, group.agentNodeId) : group.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57224
- className: "sticky top-0 z-10 min-w-0 bg-muted/60 px-3 py-2 border-b border-l border-border",
57225
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57226
- className: "text-xs font-semibold text-foreground truncate",
57227
- children: col.agentNodeId
57228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57229
- className: "text-[10px] text-foreground-subtle truncate",
57230
- children: col.engineLabel
57231
- })]
57232
- }, agentColumnKey(col)))),
57233
- rows.map(({ node, depth }) => {
57234
- const cellState = showToggle && onlyAgent !== void 0 ? getCellState(node.addonId, onlyAgent) : null;
57235
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridRow, {
57236
- node,
57237
- depth,
57238
- agents,
57239
- getCellState,
57240
- onCellClick,
57241
- selectedCell,
57242
- toggleProps: showToggle && cellState !== null ? {
57243
- enabled: cellState.kind === "enabled",
57244
- onChange: (next) => onToggleEnabled?.(node.addonId, next),
57245
- disabled: cellState.kind === "na"
57246
- } : void 0
57247
- }, node.addonId);
57248
- })
57249
- ]
57250
- })
57251
- });
57252
- }
57253
- /**
57254
- * Narrow layout: one card per agent column, each listing every step stacked
57255
- * vertically. Feature-parity with the matrix — same `onCellClick`, same
57256
- * `renderCellContent`, same single-agent `onToggleEnabled`.
57257
- */
57258
- function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57259
- const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
57260
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57261
- className: "space-y-3",
57262
- children: agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57263
- className: "border border-border rounded overflow-hidden",
57264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57265
- className: "bg-muted/60 px-3 py-2 border-b border-border",
57266
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57267
- className: "text-xs font-semibold text-foreground truncate",
57268
- children: a.deviceLabel ? `${a.agentNodeId} · ${a.deviceLabel}` : a.agentNodeId
57269
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57270
- className: "text-[10px] text-foreground-subtle truncate",
57271
- children: a.engineLabel
57272
- })]
57273
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: rows.map(({ node, depth }) => {
57274
- const state = getCellState(node.addonId, a);
57275
- const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
57276
- const toggleProps = showToggle && state !== null ? {
57277
- enabled: state.kind === "enabled",
57278
- onChange: (next) => onToggleEnabled?.(node.addonId, next),
57279
- disabled: state.kind === "na"
57280
- } : void 0;
57281
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
57282
- className: "border-b border-border last:border-b-0",
57283
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57284
- className: "flex items-center gap-2",
57285
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
57286
- type: "button",
57287
- onClick: () => onCellClick(node.addonId, a),
57288
- className: cellButtonClass(state, isSelected, "flex flex-1 min-w-0 items-start justify-between gap-3 px-3 py-2"),
57289
- style: { paddingLeft: `${12 + depth * 14}px` },
57290
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57291
- className: "shrink-0 max-w-[45%] pt-0.5",
57292
- children: renderCellContent(state)
57293
- })]
57294
- }), toggleProps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57295
- className: "pr-3 shrink-0",
57296
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickToggle, {
57297
- enabled: toggleProps.enabled,
57298
- onChange: toggleProps.onChange,
57299
- disabled: toggleProps.disabled
57300
- })
57301
- })]
57302
- })
57303
- }, node.addonId);
57304
- }) })]
57305
- }, agentColumnKey(a)))
57306
- });
57307
- }
57308
- function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
57309
- const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
57310
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57311
- className: "@container/matrix w-full",
57312
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57313
- className: "@2xl/matrix:hidden",
57314
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StackedCards, {
57315
- rows,
57316
- agents,
57317
- getCellState,
57318
- onCellClick,
57319
- selectedCell,
57320
- onToggleEnabled
57321
- })
57322
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57323
- className: "hidden @2xl/matrix:block",
57324
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
57325
- rows,
57326
- agents,
57327
- getCellState,
57328
- onCellClick,
57329
- selectedCell,
57330
- onToggleEnabled
57331
- })
57332
- })]
57333
- });
57334
- }
57335
- //#endregion
57336
57043
  //#region src/composites/provider-badge.tsx
57337
57044
  var providerConfig = {
57338
57045
  onvif: {
@@ -57776,89 +57483,6 @@ function validateSelector(selector) {
57776
57483
  }
57777
57484
  }
57778
57485
  //#endregion
57779
- //#region src/composites/slide-over-panel.tsx
57780
- /**
57781
- * SlideOverPanel — a detail surface anchored to context on desktop and to the
57782
- * viewport on mobile.
57783
- *
57784
- * - **Desktop (`md+`):** an in-container, NON-modal slide-over pinned to the
57785
- * right edge of the nearest positioned ancestor. The host reserves space with
57786
- * a matching `md:pr-*` so the panel sits beside its content (e.g. the
57787
- * multi-node pipeline matrix), not over it.
57788
- * - **Mobile (`< md`):** a MODAL bottom sheet pinned to the viewport
57789
- * (`position: fixed`). It never derives its size from the surrounding
57790
- * container, so it can't stretch across a horizontally-laid-out node matrix
57791
- * and overflow the page — the previous behaviour when it was a fixed-width
57792
- * `absolute` panel anchored to the wide matrix wrapper.
57793
- *
57794
- * The whole desktop⇄mobile transform is pure Tailwind responsive classes on one
57795
- * element — no layout-shift on hydration, no portal. JS is used only to lock
57796
- * body scroll behind the mobile sheet and to wire Escape-to-close.
57797
- */
57798
- function SlideOverPanel({ open, onClose, title, subtitle, footer, children, widthClass = "md:w-[360px]" }) {
57799
- const isMobile = useIsMobile();
57800
- (0, react$1.useEffect)(() => {
57801
- if (!open) return;
57802
- const onEsc = (e) => {
57803
- if (e.key === "Escape") onClose();
57804
- };
57805
- window.addEventListener("keydown", onEsc);
57806
- return () => window.removeEventListener("keydown", onEsc);
57807
- }, [open, onClose]);
57808
- (0, react$1.useEffect)(() => {
57809
- if (!open || !isMobile) return;
57810
- const previous = document.body.style.overflow;
57811
- document.body.style.overflow = "hidden";
57812
- return () => {
57813
- document.body.style.overflow = previous;
57814
- };
57815
- }, [open, isMobile]);
57816
- if (!open) return null;
57817
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57818
- className: "fixed inset-0 z-40 bg-black/50 md:hidden",
57819
- onClick: onClose,
57820
- "aria-hidden": "true"
57821
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
57822
- className: cn("fixed inset-x-0 bottom-0 z-50 flex flex-col", "max-h-[85dvh] rounded-t-2xl border-t border-border", "bg-surface shadow-2xl pb-[env(safe-area-inset-bottom)]", "md:absolute md:inset-x-auto md:top-0 md:right-0 md:bottom-0 md:z-20", "md:max-h-none md:rounded-none md:border-t-0 md:border-l md:pb-0", widthClass),
57823
- role: "dialog",
57824
- "aria-modal": isMobile ? "true" : "false",
57825
- children: [
57826
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57827
- className: "flex shrink-0 justify-center pt-2 pb-1 md:hidden",
57828
- "aria-hidden": "true",
57829
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-1 w-9 rounded-full bg-foreground-subtle/30" })
57830
- }),
57831
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
57832
- className: "flex shrink-0 items-start justify-between gap-2 px-4 py-3 border-b border-border",
57833
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57834
- className: "min-w-0",
57835
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57836
- className: "text-sm font-semibold text-foreground truncate",
57837
- children: title
57838
- }), subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57839
- className: "text-[11px] text-foreground-subtle mt-0.5 truncate",
57840
- children: subtitle
57841
- })]
57842
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57843
- type: "button",
57844
- onClick: onClose,
57845
- className: "p-1 rounded hover:bg-muted text-foreground-subtle hover:text-foreground",
57846
- "aria-label": "Close panel",
57847
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "w-4 h-4" })
57848
- })]
57849
- }),
57850
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57851
- className: "flex-1 min-h-0 overflow-y-auto overscroll-contain px-4 py-3",
57852
- children
57853
- }),
57854
- footer !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
57855
- className: "flex shrink-0 items-center gap-2 justify-end px-4 py-3 border-t border-border",
57856
- children: footer
57857
- })
57858
- ]
57859
- })] });
57860
- }
57861
- //#endregion
57862
57486
  //#region src/composites/snapshot-button.tsx
57863
57487
  /**
57864
57488
  * SnapshotButton — operator-facing manual snapshot trigger.
@@ -58044,51 +57668,725 @@ function StepTimings({ timings, totalMs, className }) {
58044
57668
  });
58045
57669
  }
58046
57670
  //#endregion
58047
- //#region src/composites/step-tree-master.tsx
58048
- var DOT_COLOR = {
58049
- inherit: "bg-muted",
58050
- on: "bg-emerald-500",
58051
- off: "bg-orange-500",
58052
- na: "bg-gray-300 opacity-50"
58053
- };
58054
- function TreeRow({ node, depth, selectedAddonId, onSelect, agentDots }) {
58055
- const isSelected = node.addonId === selectedAddonId;
58056
- const dots = agentDots?.[node.addonId] ?? [];
58057
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
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", {
58058
57726
  type: "button",
58059
- onClick: () => onSelect(node.addonId),
58060
- className: cn("w-full flex items-center justify-between gap-2 text-left px-3 py-1.5 text-xs", "border-l-2", isSelected ? "bg-accent border-primary text-foreground font-medium" : "border-transparent hover:bg-muted/40 text-foreground"),
58061
- style: { paddingLeft: `${12 + depth * 14}px` },
58062
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58063
- className: "truncate",
58064
- children: node.addonName
58065
- }), dots.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58066
- className: "flex items-center gap-1 shrink-0",
58067
- children: dots.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58068
- title: `${d.agentNodeId}: ${d.state}`,
58069
- className: cn("w-2 h-2 rounded-full", DOT_COLOR[d.state])
58070
- }, d.agentNodeId))
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"]
58071
57757
  })]
58072
- }), node.children.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
58073
- node: child,
58074
- depth: depth + 1,
58075
- selectedAddonId,
58076
- onSelect,
58077
- agentDots
58078
- }, child.addonId))] });
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
+ };
58079
57919
  }
58080
- 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
+ });
58081
57939
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58082
- className: cn("flex flex-col py-2", className),
58083
- children: tree.map((node) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
58084
- node,
58085
- depth: 0,
58086
- selectedAddonId,
58087
- onSelect,
58088
- agentDots
58089
- }, node.addonId))
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"]
58090
57965
  });
58091
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
58092
58390
  /**
58093
58391
  * Helper: build a StepTreeNode[] from a `PipelineSchema` by chaining on
58094
58392
  * `inputClasses` ⇔ `outputClasses` compatibility. Mirrors the orchestrator
@@ -61033,6 +61331,7 @@ var SceneMonitorEditor = (0, react$1.lazy)(() => Promise.resolve().then(() => re
61033
61331
  /** Lazy-wrapped `PrivacyMaskSettings` — code-split off the main bundle. */
61034
61332
  var PrivacyMaskSettings = (0, react$1.lazy)(() => Promise.resolve().then(() => require("./PrivacyMaskSettings-BqjXsXZR.cjs")).then((m) => ({ default: m.PrivacyMaskSettings })));
61035
61333
  //#endregion
61334
+ exports.AcceleratorTabs = AcceleratorTabs;
61036
61335
  exports.AddonGlobalSettingsForm = AddonGlobalSettingsForm;
61037
61336
  exports.AdminLinkQuickAction = AdminLinkQuickAction;
61038
61337
  exports.AgentStepEditor = AgentStepEditor;
@@ -61050,6 +61349,7 @@ exports.BTN_COMPACT = BTN_COMPACT;
61050
61349
  exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
61051
61350
  exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
61052
61351
  exports.BTN_COMPACT_WARNING = BTN_COMPACT_WARNING;
61352
+ exports.BackToStepsButton = BackToStepsButton;
61053
61353
  exports.Badge = Badge;
61054
61354
  exports.BatteryBadge = BatteryBadge;
61055
61355
  exports.BottomSheet = BottomSheet;
@@ -61171,6 +61471,8 @@ exports.FormField = FormField$1;
61171
61471
  exports.GRID_GAP = GRID_GAP;
61172
61472
  exports.GRID_PAIRED = GRID_PAIRED;
61173
61473
  exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
61474
+ exports.GateBadge = GateBadge;
61475
+ exports.GateControl = GateControl;
61174
61476
  exports.Grid2x2 = Grid2x2;
61175
61477
  exports.GripTrack = GripTrack;
61176
61478
  exports.GroupedModelSelector = GroupedModelSelector;
@@ -61198,6 +61500,7 @@ exports.LIST_ROW = LIST_ROW;
61198
61500
  exports.Label = Label;
61199
61501
  exports.LawnMowerHeroCard = LawnMowerHeroCard;
61200
61502
  exports.LawnMowerInlineControl = LawnMowerInlineControl;
61503
+ exports.LayeredSettingsForm = LayeredSettingsForm;
61201
61504
  exports.LightHeroCard = LightHeroCard;
61202
61505
  exports.LightInlineControl = LightInlineControl;
61203
61506
  exports.LockHeroCard = LockHeroCard;
@@ -61245,7 +61548,6 @@ exports.PhaseIcon = PhaseIcon;
61245
61548
  exports.PipelineBuilder = PipelineBuilder;
61246
61549
  exports.PipelineRuntimeSelector = PipelineRuntimeSelector;
61247
61550
  exports.PipelineStep = PipelineStep;
61248
- exports.PipelineTreeMatrix = PipelineTreeMatrix;
61249
61551
  exports.PlaceholderChipField = PlaceholderChipField;
61250
61552
  exports.PlayerOverlaysProvider = PlayerOverlaysProvider;
61251
61553
  exports.Popover = Popover;
@@ -61268,6 +61570,8 @@ exports.RecordingPanel = RecordingPanel;
61268
61570
  exports.ResponseLog = ResponseLog;
61269
61571
  exports.RoleDisplayDefaultsFromServer = RoleDisplayDefaultsFromServer;
61270
61572
  exports.RoleDisplayDefaultsProvider = RoleDisplayDefaultsProvider;
61573
+ exports.RootModelSourceControl = RootModelSourceControl;
61574
+ exports.RootSuppliedModelNote = RootSuppliedModelNote;
61271
61575
  exports.RotateCcw = RotateCcw;
61272
61576
  exports.RunReport = RunReport;
61273
61577
  exports.SCENE_CAPTURE_VARIANTS = SCENE_CAPTURE_VARIANTS;
@@ -61291,6 +61595,7 @@ exports.ScriptHeroCard = ScriptHeroCard;
61291
61595
  exports.ScriptInlineControl = ScriptInlineControl;
61292
61596
  exports.ScrollArea = ScrollArea;
61293
61597
  exports.SectionCard = SectionCard;
61598
+ exports.SegmentedChoice = SegmentedChoice;
61294
61599
  exports.Select = Select;
61295
61600
  exports.SelectedChip = SelectedChip;
61296
61601
  exports.SemanticBadge = SemanticBadge;
@@ -61302,7 +61607,6 @@ exports.SettingRow = SettingRow;
61302
61607
  exports.Sidebar = Sidebar;
61303
61608
  exports.SidebarItem = SidebarItem;
61304
61609
  exports.Skeleton = Skeleton;
61305
- exports.SlideOverPanel = SlideOverPanel;
61306
61610
  exports.SlideToggle = SlideToggle;
61307
61611
  exports.SnapshotButton = SnapshotButton;
61308
61612
  exports.SortHeaderButton = SortHeaderButton;
@@ -61310,8 +61614,13 @@ exports.Square = Square;
61310
61614
  exports.StatCard = StatCard;
61311
61615
  exports.StateValuesStream = StateValuesStream;
61312
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;
61313
61623
  exports.StepTimings = StepTimings;
61314
- exports.StepTreeMaster = StepTreeMaster;
61315
61624
  exports.Stepper = Stepper;
61316
61625
  exports.StreamBrokerSelector = StreamBrokerSelector;
61317
61626
  exports.StreamDebugOverlay = StreamDebugOverlay;
@@ -61365,6 +61674,7 @@ exports.applyPickerFilters = applyPickerFilters;
61365
61674
  exports.ariaSortForColumn = ariaSortForColumn;
61366
61675
  exports.badgeFor = badgeFor;
61367
61676
  exports.badgeFor$1 = badgeFor;
61677
+ exports.bindsToModel = bindsToModel;
61368
61678
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
61369
61679
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
61370
61680
  exports.catalogHasDraft = catalogHasDraft;
@@ -61389,6 +61699,7 @@ exports.createTheme = require_theme_index.createTheme$1;
61389
61699
  exports.cursorFractionFor = cursorFractionFor;
61390
61700
  exports.darkColors = require_theme_index.darkColors$1;
61391
61701
  exports.dataTableFacets = dataTableFacets;
61702
+ exports.defaultOfField = defaultOfField;
61392
61703
  exports.defaultTheme = require_theme_index.defaultTheme$1;
61393
61704
  exports.deriveDeviceKind = deriveDeviceKind;
61394
61705
  exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
@@ -61408,6 +61719,7 @@ exports.emptyDevicesWidens = emptyDevicesWidens;
61408
61719
  exports.emptySessionRun = emptySessionRun;
61409
61720
  exports.ensureMfHostInit = ensureMfHostInit;
61410
61721
  exports.eventKindLabel = eventKindLabel;
61722
+ exports.fillHeightPx = fillHeightPx;
61411
61723
  exports.filterDataTableRows = filterDataTableRows;
61412
61724
  exports.filterDeviceOptions = filterDeviceOptions;
61413
61725
  exports.filterOptionSelectOptions = filterOptions;
@@ -61420,6 +61732,7 @@ exports.fleetViewOf = fleetViewOf;
61420
61732
  exports.formatControlDateTime = formatControlDateTime;
61421
61733
  exports.formatLastSeen = formatLastSeen;
61422
61734
  exports.formatNumeric = formatNumeric;
61735
+ exports.formatSettingValue = formatSettingValue;
61423
61736
  exports.freezeVerdict = freezeVerdict;
61424
61737
  exports.fuzzyMatch = fuzzyMatch;
61425
61738
  exports.getClassColor = getClassColor;
@@ -61443,6 +61756,7 @@ exports.isAbsentProvider$1 = isAbsentProvider;
61443
61756
  exports.isDataTableFilterActive = isDataTableFilterActive;
61444
61757
  exports.isFieldVisible = isFieldVisible;
61445
61758
  exports.isGroupExpanded = isGroupExpanded;
61759
+ exports.isValueField = isValueField;
61446
61760
  exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
61447
61761
  exports.lightColors = require_theme_index.lightColors$1;
61448
61762
  exports.loadRemoteBundle = loadRemoteBundle;
@@ -61450,6 +61764,7 @@ exports.makeScrubBridge = makeScrubBridge;
61450
61764
  exports.metadataEntries = metadataEntries;
61451
61765
  exports.metadataString = metadataString;
61452
61766
  exports.mirror = mirror;
61767
+ exports.modelRowsAreFoldable = modelRowsAreFoldable;
61453
61768
  exports.mountAddonPage = mountAddonPage;
61454
61769
  exports.mountPath = mountPath;
61455
61770
  exports.nextReconnectAction = nextReconnectAction;
@@ -61507,9 +61822,11 @@ exports.rootSourceState = rootSourceState;
61507
61822
  exports.routerBasename = routerBasename;
61508
61823
  exports.sameLocationSuggestions = sameLocationSuggestions;
61509
61824
  exports.scrubReducer = scrubReducer;
61825
+ exports.searchSteps = searchSteps;
61510
61826
  exports.selectPickerGroup = selectPickerGroup;
61511
61827
  exports.selectedDeviceOptions = selectedDeviceOptions;
61512
61828
  exports.serializeRecordedCommand = serializeRecordedCommand;
61829
+ exports.settingsModelIdFor = settingsModelIdFor;
61513
61830
  exports.shouldCommit = shouldCommit;
61514
61831
  exports.shouldEmit = shouldEmit;
61515
61832
  exports.shouldShowAddNewFooter = shouldShowAddNewFooter;
@@ -61521,7 +61838,9 @@ exports.sortRowsByColumn = sortRowsByColumn;
61521
61838
  exports.statusIcons = statusIcons;
61522
61839
  exports.statusLine = statusLine;
61523
61840
  exports.statusLine$1 = statusLine;
61841
+ exports.stepFieldDomId = stepFieldDomId;
61524
61842
  exports.stepHasModelForFormat = stepHasModelForFormat;
61843
+ exports.stepLocalFields = stepLocalFields;
61525
61844
  exports.stepModelOptions = stepModelOptions;
61526
61845
  exports.stripParentNamePrefix = stripParentNamePrefix;
61527
61846
  exports.summarizeHubCandidates = summarizeHubCandidates;
@@ -62737,4 +63056,5 @@ exports.waterHeaterTint = waterHeaterTint;
62737
63056
  exports.weatherConditionMeta = weatherConditionMeta;
62738
63057
  exports.weatherTint = weatherTint;
62739
63058
  exports.withResolvedParentDetail = withResolvedParentDetail;
63059
+ exports.withUseRootModel = withUseRootModel;
62740
63060
  exports.writeStoredGroupBy = writeStoredGroupBy;