@camstack/ui-library 1.2.237 → 1.2.239

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.
@@ -0,0 +1,28 @@
1
+ import { ConfigField, ConfigNumberField, ConfigSliderField } from '@camstack/types';
2
+ /** A numeric field this control can render. */
3
+ export type ModelBoundNumericField = ConfigSliderField | ConfigNumberField;
4
+ export interface ModelBoundSliderProps {
5
+ readonly field: ModelBoundNumericField;
6
+ /** The model the edited row runs — pinned, or its effective default. */
7
+ readonly modelId: string;
8
+ readonly settings: Readonly<Record<string, unknown>> | undefined;
9
+ readonly disabled: boolean;
10
+ readonly onChangeSettings: (next: Record<string, unknown>) => void;
11
+ }
12
+ export declare function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings, }: ModelBoundSliderProps): import("react").JSX.Element;
13
+ /** The keys of the model-bound fields of a schema, groups flattened. */
14
+ export declare function modelBoundKeysOf(fields: readonly ConfigField[]): string[];
15
+ export interface ResetModelDefaultsButtonProps {
16
+ readonly fields: readonly ConfigField[];
17
+ /** The model the edited row runs — pinned, or its effective default. */
18
+ readonly modelId: string;
19
+ readonly settings: Readonly<Record<string, unknown>> | undefined;
20
+ readonly disabled?: boolean;
21
+ readonly onReset: (next: Record<string, unknown>) => void;
22
+ }
23
+ /**
24
+ * "Reset thresholds to model defaults" — every model-bound override in force
25
+ * for `modelId`, cleared in ONE settings change (one save). Rendered only when
26
+ * there is something to reset. An override stored for another model is kept.
27
+ */
28
+ export declare function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset, }: ResetModelDefaultsButtonProps): import("react").JSX.Element | null;
package/dist/index.cjs CHANGED
@@ -28752,18 +28752,31 @@ function GroupedModelSelector({ catalog, value, onChange, disabled }) {
28752
28752
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28753
28753
  className: "space-y-3",
28754
28754
  children: [
28755
- families.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
28756
- className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
28757
- children: "Family"
28758
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28759
- className: "flex flex-wrap gap-1.5",
28760
- children: families.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
28761
- active: f.family === family.family,
28762
- disabled,
28763
- onClick: () => onChange(f.tiers[0]?.baseModelId ?? ""),
28764
- children: f.label
28765
- }, f.family))
28766
- })] }),
28755
+ families.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
28756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
28757
+ className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
28758
+ children: "Family"
28759
+ }),
28760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28761
+ className: "flex flex-wrap gap-1.5",
28762
+ children: families.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
28763
+ active: f.family === family.family,
28764
+ disabled,
28765
+ ...f.hint !== void 0 ? { title: f.hint } : {},
28766
+ onClick: () => onChange(f.tiers[0]?.baseModelId ?? ""),
28767
+ children: f.label
28768
+ }, f.family))
28769
+ }),
28770
+ family.hint !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
28771
+ className: "mt-1 text-[10px] text-foreground-subtle",
28772
+ "data-testid": "model-family-hint",
28773
+ children: [
28774
+ family.label,
28775
+ ": ",
28776
+ family.hint
28777
+ ]
28778
+ })
28779
+ ] }),
28767
28780
  family.tiers.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
28768
28781
  className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
28769
28782
  children: [family.label, " — size"]
@@ -28957,6 +28970,106 @@ function ModelPicker({ models, value, onChange, disabled, defaultModelId }) {
28957
28970
  });
28958
28971
  }
28959
28972
  //#endregion
28973
+ //#region src/composites/model-bound-slider.tsx
28974
+ var SOURCE_LABEL = {
28975
+ override: "Override for this model",
28976
+ "model-default": "Model default",
28977
+ "global-default": "Default — not tuned for this model"
28978
+ };
28979
+ function formatNumber(value, scale) {
28980
+ return (scale !== void 0 ? value / scale : value).toFixed(2).replace(/\.?0+$/, "");
28981
+ }
28982
+ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings }) {
28983
+ const modelDefault = field.modelApplicability?.defaultByModel?.[modelId];
28984
+ const state = (0, _camstack_types.resolveModelBoundSetting)(settings, field.key, modelId.length > 0 ? modelId : void 0, modelDefault, field.default);
28985
+ const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
28986
+ const current = typeof state.value === "number" ? state.value : fallback;
28987
+ const scale = field.type === "slider" ? field.displayScale : void 0;
28988
+ const unit = "unit" in field ? field.unit : void 0;
28989
+ const stale = state.inapplicableOverride;
28990
+ const hasStoredValue = settings?.[field.key] !== void 0;
28991
+ const handleChange = (value) => {
28992
+ onChangeSettings(modelId.length > 0 ? (0, _camstack_types.writeModelBoundSetting)(settings, field.key, value, modelId) : {
28993
+ ...settings,
28994
+ [field.key]: value
28995
+ });
28996
+ };
28997
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
28998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
28999
+ className: "flex items-center justify-between mb-1",
29000
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29001
+ className: "text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
29002
+ children: field.label
29003
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
29004
+ className: "text-xs font-medium text-foreground tabular-nums",
29005
+ children: [formatNumber(current, scale), unit ? ` ${unit}` : ""]
29006
+ })]
29007
+ }),
29008
+ field.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
29009
+ className: "text-[10px] text-foreground-subtle/60 mb-1.5",
29010
+ children: field.description
29011
+ }),
29012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
29013
+ type: "range",
29014
+ min: field.min ?? 0,
29015
+ max: field.max ?? 1,
29016
+ step: field.step ?? .01,
29017
+ value: current,
29018
+ disabled: disabled || field.disabled,
29019
+ onChange: (e) => handleChange(Number(e.target.value)),
29020
+ className: "w-full accent-primary h-1.5"
29021
+ }),
29022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29023
+ className: "mt-1 flex items-center justify-between gap-2 text-[10px]",
29024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29025
+ "data-testid": "model-bound-source",
29026
+ className: state.source === "override" ? "text-primary font-medium" : "text-foreground-subtle",
29027
+ children: SOURCE_LABEL[state.source]
29028
+ }), hasStoredValue && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29029
+ type: "button",
29030
+ disabled,
29031
+ className: "text-foreground-subtle hover:text-foreground disabled:opacity-50",
29032
+ onClick: () => onChangeSettings((0, _camstack_types.clearModelBoundSetting)(settings, field.key)),
29033
+ children: "Reset to model default"
29034
+ })]
29035
+ }),
29036
+ stale !== void 0 && typeof stale.value === "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
29037
+ className: "mt-1 text-[10px] text-amber-400/90",
29038
+ children: [
29039
+ "The override ",
29040
+ formatNumber(stale.value, scale),
29041
+ " was set for ",
29042
+ stale.modelId,
29043
+ " and does not apply to this model."
29044
+ ]
29045
+ })
29046
+ ] });
29047
+ }
29048
+ /** The keys of the model-bound fields of a schema, groups flattened. */
29049
+ function modelBoundKeysOf(fields) {
29050
+ return fields.flatMap((field) => {
29051
+ if (field.type === "group") return modelBoundKeysOf(field.fields);
29052
+ return "key" in field && "bindsToModel" in field && field.bindsToModel === true ? [field.key] : [];
29053
+ });
29054
+ }
29055
+ /**
29056
+ * "Reset thresholds to model defaults" — every model-bound override in force
29057
+ * for `modelId`, cleared in ONE settings change (one save). Rendered only when
29058
+ * there is something to reset. An override stored for another model is kept.
29059
+ */
29060
+ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset }) {
29061
+ const keys = modelBoundKeysOf(fields);
29062
+ const model = modelId.length > 0 ? modelId : void 0;
29063
+ if (keys.length === 0 || !(0, _camstack_types.hasModelBoundOverrides)(settings, keys, model)) return null;
29064
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29065
+ type: "button",
29066
+ disabled,
29067
+ className: "text-[10px] text-primary hover:underline disabled:opacity-50",
29068
+ onClick: () => onReset((0, _camstack_types.clearModelBoundOverrides)(settings, keys, model)),
29069
+ children: "Reset thresholds to model defaults"
29070
+ });
29071
+ }
29072
+ //#endregion
28960
29073
  //#region src/composites/pipeline-step.tsx
28961
29074
  /**
28962
29075
  * PipelineStep — single step card in the pipeline builder.
@@ -29214,6 +29327,16 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29214
29327
  onChange: handleChange
29215
29328
  });
29216
29329
  }
29330
+ if ((field.type === "slider" || field.type === "number") && field.bindsToModel === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelBoundSlider, {
29331
+ field,
29332
+ modelId: step.modelId,
29333
+ settings: step.settings,
29334
+ disabled,
29335
+ onChangeSettings: (settings) => onChange({
29336
+ ...step,
29337
+ settings
29338
+ })
29339
+ });
29217
29340
  if (field.type === "slider" || field.type === "number") {
29218
29341
  const raw = step.settings?.[field.key];
29219
29342
  const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
@@ -29400,6 +29523,7 @@ function supplyNotes(supply) {
29400
29523
  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."];
29401
29524
  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."];
29402
29525
  }
29526
+ 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."];
29403
29527
  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."];
29404
29528
  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."];
29405
29529
  }
@@ -29471,7 +29595,19 @@ function ClusterSettingsHint({ addonId }) {
29471
29595
  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."
29472
29596
  });
29473
29597
  }
29474
- function buildDisplayStep(addon, cfg) {
29598
+ /**
29599
+ * The model the step's settings are read and written for — what a model-bound
29600
+ * field (D738) binds to. While the ROOT supplies the step's fine label (D741),
29601
+ * that is the root model: the step's threshold then gates the root's label, on
29602
+ * the root's score scale. Otherwise the step's own model: its pin, else the
29603
+ * model the row really runs (`effectiveModelId` — the device's hardware-aware
29604
+ * default when unpinned), else the step's flat catalog default.
29605
+ */
29606
+ function settingsModelIdFor(addon, cfg, effectiveModelId, rootModel) {
29607
+ if (rootModel !== void 0 && rootSourceState(addon.id, cfg.settings, rootModel)?.effective) return rootModel.modelId;
29608
+ return cfg.modelId ?? effectiveModelId ?? addon.defaultModelId;
29609
+ }
29610
+ function buildDisplayStep(addon, cfg, modelId) {
29475
29611
  return {
29476
29612
  addonId: addon.id,
29477
29613
  addonName: addon.name,
@@ -29479,7 +29615,7 @@ function buildDisplayStep(addon, cfg) {
29479
29615
  inputClasses: [...addon.inputClasses],
29480
29616
  outputClasses: [...addon.outputClasses],
29481
29617
  enabled: cfg.enabled,
29482
- modelId: cfg.modelId ?? addon.defaultModelId,
29618
+ modelId,
29483
29619
  children: [],
29484
29620
  settings: { ...cfg.settings }
29485
29621
  };
@@ -29544,13 +29680,21 @@ function AgentModeEditor({ addon, agentDefault, modelsForFormat, engineFormat, e
29544
29680
  })] }),
29545
29681
  localSchema.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29546
29682
  className: "pt-2 border-t border-border/50 space-y-3",
29547
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29548
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29549
- children: "Settings"
29683
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29684
+ className: "flex items-center justify-between",
29685
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29686
+ className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29687
+ children: "Settings"
29688
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29689
+ fields: localSchema,
29690
+ modelId: settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel),
29691
+ settings: agentDefault.settings,
29692
+ onReset: (settings) => update({ settings })
29693
+ })]
29550
29694
  }), localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29551
29695
  field,
29552
29696
  allFields: localSchema,
29553
- step: buildDisplayStep(addon, agentDefault),
29697
+ step: buildDisplayStep(addon, agentDefault, settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel)),
29554
29698
  disabled: false,
29555
29699
  onChange: (next) => update({ settings: { ...next.settings } })
29556
29700
  }, field.key))]
@@ -29646,11 +29790,19 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
29646
29790
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29647
29791
  className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29648
29792
  children: "Settings"
29649
- }), settingsOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29650
- type: "button",
29651
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29652
- onClick: () => setSettings(void 0),
29653
- children: "Clear"
29793
+ }), settingsOverridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29794
+ className: "flex items-center gap-3",
29795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetModelDefaultsButton, {
29796
+ fields: localSchema,
29797
+ modelId: settingsModelIdFor(addon, effective, inheritedModelId, rootModel),
29798
+ settings: currentPatch?.settings,
29799
+ onReset: (settings) => setSettings(settings)
29800
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29801
+ type: "button",
29802
+ className: "text-[10px] text-foreground-subtle hover:text-foreground",
29803
+ onClick: () => setSettings(void 0),
29804
+ children: "Clear"
29805
+ })]
29654
29806
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
29655
29807
  type: "button",
29656
29808
  className: "text-[10px] text-primary hover:underline",
@@ -29661,7 +29813,7 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
29661
29813
  localSchema.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
29662
29814
  field,
29663
29815
  allFields: localSchema,
29664
- step: buildDisplayStep(addon, effective),
29816
+ step: buildDisplayStep(addon, effective, settingsModelIdFor(addon, effective, inheritedModelId, rootModel)),
29665
29817
  disabled: !settingsOverridden,
29666
29818
  onChange: (next) => setSettings({ ...next.settings })
29667
29819
  }, field.key)),
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import * as __mfHostReactDOMClient from "react-dom/client";
13
13
  import { createRoot } from "react-dom/client";
14
14
  import * as __mfHostTrpcClient from "@trpc/client";
15
15
  import { createTRPCClient, createWSClient, httpLink, splitLink, wsLink } from "@trpc/client";
16
- import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, CONTAINER_CHILD_PRIORITY, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEFAULT_EVENT_COLOR, DEVICE_TYPE_CONTROL_KIND, DISPLAY_ICON_GROUPS, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EVENT_TAXONOMY, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, RecordingConfigSchema, RecordingStatusSchema, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, buildModelVariantGroups, canConvertUnit, clusterStepSettingFieldsFor, createDeviceProxy, createLazyTrpcSource, createSliceHandle, deriveRecordingMode, describeModelVariant, findTimezone, formatParkedFor, isRootSuppliableStepId, normalizeUnit, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchDisplayIcons, selectAssignedProfileSlots, tryConvertUnit, unitsForDimension } from "@camstack/types";
16
+ import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, CONTAINER_CHILD_PRIORITY, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEFAULT_EVENT_COLOR, DEVICE_TYPE_CONTROL_KIND, DISPLAY_ICON_GROUPS, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EVENT_TAXONOMY, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, RecordingConfigSchema, RecordingStatusSchema, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, buildModelVariantGroups, canConvertUnit, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, createDeviceProxy, createLazyTrpcSource, createSliceHandle, deriveRecordingMode, describeModelVariant, findTimezone, formatParkedFor, hasModelBoundOverrides, isRootSuppliableStepId, normalizeUnit, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchDisplayIcons, selectAssignedProfileSlots, tryConvertUnit, unitsForDimension, writeModelBoundSetting } from "@camstack/types";
17
17
  import { createSystem } from "@camstack/sdk";
18
18
  import { z } from "zod";
19
19
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.js
@@ -28728,18 +28728,31 @@ function GroupedModelSelector({ catalog, value, onChange, disabled }) {
28728
28728
  return /* @__PURE__ */ jsxs("div", {
28729
28729
  className: "space-y-3",
28730
28730
  children: [
28731
- families.length > 1 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
28732
- className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
28733
- children: "Family"
28734
- }), /* @__PURE__ */ jsx("div", {
28735
- className: "flex flex-wrap gap-1.5",
28736
- children: families.map((f) => /* @__PURE__ */ jsx(Chip, {
28737
- active: f.family === family.family,
28738
- disabled,
28739
- onClick: () => onChange(f.tiers[0]?.baseModelId ?? ""),
28740
- children: f.label
28741
- }, f.family))
28742
- })] }),
28731
+ families.length > 1 && /* @__PURE__ */ jsxs("div", { children: [
28732
+ /* @__PURE__ */ jsx("p", {
28733
+ className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
28734
+ children: "Family"
28735
+ }),
28736
+ /* @__PURE__ */ jsx("div", {
28737
+ className: "flex flex-wrap gap-1.5",
28738
+ children: families.map((f) => /* @__PURE__ */ jsx(Chip, {
28739
+ active: f.family === family.family,
28740
+ disabled,
28741
+ ...f.hint !== void 0 ? { title: f.hint } : {},
28742
+ onClick: () => onChange(f.tiers[0]?.baseModelId ?? ""),
28743
+ children: f.label
28744
+ }, f.family))
28745
+ }),
28746
+ family.hint !== void 0 && /* @__PURE__ */ jsxs("p", {
28747
+ className: "mt-1 text-[10px] text-foreground-subtle",
28748
+ "data-testid": "model-family-hint",
28749
+ children: [
28750
+ family.label,
28751
+ ": ",
28752
+ family.hint
28753
+ ]
28754
+ })
28755
+ ] }),
28743
28756
  family.tiers.length > 1 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("p", {
28744
28757
  className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
28745
28758
  children: [family.label, " — size"]
@@ -28933,6 +28946,106 @@ function ModelPicker({ models, value, onChange, disabled, defaultModelId }) {
28933
28946
  });
28934
28947
  }
28935
28948
  //#endregion
28949
+ //#region src/composites/model-bound-slider.tsx
28950
+ var SOURCE_LABEL = {
28951
+ override: "Override for this model",
28952
+ "model-default": "Model default",
28953
+ "global-default": "Default — not tuned for this model"
28954
+ };
28955
+ function formatNumber(value, scale) {
28956
+ return (scale !== void 0 ? value / scale : value).toFixed(2).replace(/\.?0+$/, "");
28957
+ }
28958
+ function ModelBoundSlider({ field, modelId, settings, disabled, onChangeSettings }) {
28959
+ const modelDefault = field.modelApplicability?.defaultByModel?.[modelId];
28960
+ const state = resolveModelBoundSetting(settings, field.key, modelId.length > 0 ? modelId : void 0, modelDefault, field.default);
28961
+ const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
28962
+ const current = typeof state.value === "number" ? state.value : fallback;
28963
+ const scale = field.type === "slider" ? field.displayScale : void 0;
28964
+ const unit = "unit" in field ? field.unit : void 0;
28965
+ const stale = state.inapplicableOverride;
28966
+ const hasStoredValue = settings?.[field.key] !== void 0;
28967
+ const handleChange = (value) => {
28968
+ onChangeSettings(modelId.length > 0 ? writeModelBoundSetting(settings, field.key, value, modelId) : {
28969
+ ...settings,
28970
+ [field.key]: value
28971
+ });
28972
+ };
28973
+ return /* @__PURE__ */ jsxs("div", { children: [
28974
+ /* @__PURE__ */ jsxs("div", {
28975
+ className: "flex items-center justify-between mb-1",
28976
+ children: [/* @__PURE__ */ jsx("span", {
28977
+ className: "text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
28978
+ children: field.label
28979
+ }), /* @__PURE__ */ jsxs("span", {
28980
+ className: "text-xs font-medium text-foreground tabular-nums",
28981
+ children: [formatNumber(current, scale), unit ? ` ${unit}` : ""]
28982
+ })]
28983
+ }),
28984
+ field.description && /* @__PURE__ */ jsx("p", {
28985
+ className: "text-[10px] text-foreground-subtle/60 mb-1.5",
28986
+ children: field.description
28987
+ }),
28988
+ /* @__PURE__ */ jsx("input", {
28989
+ type: "range",
28990
+ min: field.min ?? 0,
28991
+ max: field.max ?? 1,
28992
+ step: field.step ?? .01,
28993
+ value: current,
28994
+ disabled: disabled || field.disabled,
28995
+ onChange: (e) => handleChange(Number(e.target.value)),
28996
+ className: "w-full accent-primary h-1.5"
28997
+ }),
28998
+ /* @__PURE__ */ jsxs("div", {
28999
+ className: "mt-1 flex items-center justify-between gap-2 text-[10px]",
29000
+ children: [/* @__PURE__ */ jsx("span", {
29001
+ "data-testid": "model-bound-source",
29002
+ className: state.source === "override" ? "text-primary font-medium" : "text-foreground-subtle",
29003
+ children: SOURCE_LABEL[state.source]
29004
+ }), hasStoredValue && /* @__PURE__ */ jsx("button", {
29005
+ type: "button",
29006
+ disabled,
29007
+ className: "text-foreground-subtle hover:text-foreground disabled:opacity-50",
29008
+ onClick: () => onChangeSettings(clearModelBoundSetting(settings, field.key)),
29009
+ children: "Reset to model default"
29010
+ })]
29011
+ }),
29012
+ stale !== void 0 && typeof stale.value === "number" && /* @__PURE__ */ jsxs("p", {
29013
+ className: "mt-1 text-[10px] text-amber-400/90",
29014
+ children: [
29015
+ "The override ",
29016
+ formatNumber(stale.value, scale),
29017
+ " was set for ",
29018
+ stale.modelId,
29019
+ " and does not apply to this model."
29020
+ ]
29021
+ })
29022
+ ] });
29023
+ }
29024
+ /** The keys of the model-bound fields of a schema, groups flattened. */
29025
+ function modelBoundKeysOf(fields) {
29026
+ return fields.flatMap((field) => {
29027
+ if (field.type === "group") return modelBoundKeysOf(field.fields);
29028
+ return "key" in field && "bindsToModel" in field && field.bindsToModel === true ? [field.key] : [];
29029
+ });
29030
+ }
29031
+ /**
29032
+ * "Reset thresholds to model defaults" — every model-bound override in force
29033
+ * for `modelId`, cleared in ONE settings change (one save). Rendered only when
29034
+ * there is something to reset. An override stored for another model is kept.
29035
+ */
29036
+ function ResetModelDefaultsButton({ fields, modelId, settings, disabled, onReset }) {
29037
+ const keys = modelBoundKeysOf(fields);
29038
+ const model = modelId.length > 0 ? modelId : void 0;
29039
+ if (keys.length === 0 || !hasModelBoundOverrides(settings, keys, model)) return null;
29040
+ return /* @__PURE__ */ jsx("button", {
29041
+ type: "button",
29042
+ disabled,
29043
+ className: "text-[10px] text-primary hover:underline disabled:opacity-50",
29044
+ onClick: () => onReset(clearModelBoundOverrides(settings, keys, model)),
29045
+ children: "Reset thresholds to model defaults"
29046
+ });
29047
+ }
29048
+ //#endregion
28936
29049
  //#region src/composites/pipeline-step.tsx
28937
29050
  /**
28938
29051
  * PipelineStep — single step card in the pipeline builder.
@@ -29190,6 +29303,16 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
29190
29303
  onChange: handleChange
29191
29304
  });
29192
29305
  }
29306
+ if ((field.type === "slider" || field.type === "number") && field.bindsToModel === true) return /* @__PURE__ */ jsx(ModelBoundSlider, {
29307
+ field,
29308
+ modelId: step.modelId,
29309
+ settings: step.settings,
29310
+ disabled,
29311
+ onChangeSettings: (settings) => onChange({
29312
+ ...step,
29313
+ settings
29314
+ })
29315
+ });
29193
29316
  if (field.type === "slider" || field.type === "number") {
29194
29317
  const raw = step.settings?.[field.key];
29195
29318
  const fallback = typeof field.default === "number" ? field.default : field.min ?? 0;
@@ -29376,6 +29499,7 @@ function supplyNotes(supply) {
29376
29499
  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."];
29377
29500
  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."];
29378
29501
  }
29502
+ 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."];
29379
29503
  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."];
29380
29504
  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."];
29381
29505
  }
@@ -29447,7 +29571,19 @@ function ClusterSettingsHint({ addonId }) {
29447
29571
  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."
29448
29572
  });
29449
29573
  }
29450
- function buildDisplayStep(addon, cfg) {
29574
+ /**
29575
+ * The model the step's settings are read and written for — what a model-bound
29576
+ * field (D738) binds to. While the ROOT supplies the step's fine label (D741),
29577
+ * that is the root model: the step's threshold then gates the root's label, on
29578
+ * the root's score scale. Otherwise the step's own model: its pin, else the
29579
+ * model the row really runs (`effectiveModelId` — the device's hardware-aware
29580
+ * default when unpinned), else the step's flat catalog default.
29581
+ */
29582
+ function settingsModelIdFor(addon, cfg, effectiveModelId, rootModel) {
29583
+ if (rootModel !== void 0 && rootSourceState(addon.id, cfg.settings, rootModel)?.effective) return rootModel.modelId;
29584
+ return cfg.modelId ?? effectiveModelId ?? addon.defaultModelId;
29585
+ }
29586
+ function buildDisplayStep(addon, cfg, modelId) {
29451
29587
  return {
29452
29588
  addonId: addon.id,
29453
29589
  addonName: addon.name,
@@ -29455,7 +29591,7 @@ function buildDisplayStep(addon, cfg) {
29455
29591
  inputClasses: [...addon.inputClasses],
29456
29592
  outputClasses: [...addon.outputClasses],
29457
29593
  enabled: cfg.enabled,
29458
- modelId: cfg.modelId ?? addon.defaultModelId,
29594
+ modelId,
29459
29595
  children: [],
29460
29596
  settings: { ...cfg.settings }
29461
29597
  };
@@ -29520,13 +29656,21 @@ function AgentModeEditor({ addon, agentDefault, modelsForFormat, engineFormat, e
29520
29656
  })] }),
29521
29657
  localSchema.length > 0 && /* @__PURE__ */ jsxs("div", {
29522
29658
  className: "pt-2 border-t border-border/50 space-y-3",
29523
- children: [/* @__PURE__ */ jsx("div", {
29524
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29525
- children: "Settings"
29659
+ children: [/* @__PURE__ */ jsxs("div", {
29660
+ className: "flex items-center justify-between",
29661
+ children: [/* @__PURE__ */ jsx("div", {
29662
+ className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29663
+ children: "Settings"
29664
+ }), /* @__PURE__ */ jsx(ResetModelDefaultsButton, {
29665
+ fields: localSchema,
29666
+ modelId: settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel),
29667
+ settings: agentDefault.settings,
29668
+ onReset: (settings) => update({ settings })
29669
+ })]
29526
29670
  }), localSchema.map((field) => /* @__PURE__ */ jsx(ConfigSchemaField, {
29527
29671
  field,
29528
29672
  allFields: localSchema,
29529
- step: buildDisplayStep(addon, agentDefault),
29673
+ step: buildDisplayStep(addon, agentDefault, settingsModelIdFor(addon, agentDefault, effectiveModelId, rootModel)),
29530
29674
  disabled: false,
29531
29675
  onChange: (next) => update({ settings: { ...next.settings } })
29532
29676
  }, field.key))]
@@ -29622,11 +29766,19 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
29622
29766
  children: [/* @__PURE__ */ jsx("div", {
29623
29767
  className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
29624
29768
  children: "Settings"
29625
- }), settingsOverridden ? /* @__PURE__ */ jsx("button", {
29626
- type: "button",
29627
- className: "text-[10px] text-foreground-subtle hover:text-foreground",
29628
- onClick: () => setSettings(void 0),
29629
- children: "Clear"
29769
+ }), settingsOverridden ? /* @__PURE__ */ jsxs("div", {
29770
+ className: "flex items-center gap-3",
29771
+ children: [/* @__PURE__ */ jsx(ResetModelDefaultsButton, {
29772
+ fields: localSchema,
29773
+ modelId: settingsModelIdFor(addon, effective, inheritedModelId, rootModel),
29774
+ settings: currentPatch?.settings,
29775
+ onReset: (settings) => setSettings(settings)
29776
+ }), /* @__PURE__ */ jsx("button", {
29777
+ type: "button",
29778
+ className: "text-[10px] text-foreground-subtle hover:text-foreground",
29779
+ onClick: () => setSettings(void 0),
29780
+ children: "Clear"
29781
+ })]
29630
29782
  }) : /* @__PURE__ */ jsx("button", {
29631
29783
  type: "button",
29632
29784
  className: "text-[10px] text-primary hover:underline",
@@ -29637,7 +29789,7 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
29637
29789
  localSchema.map((field) => /* @__PURE__ */ jsx(ConfigSchemaField, {
29638
29790
  field,
29639
29791
  allFields: localSchema,
29640
- step: buildDisplayStep(addon, effective),
29792
+ step: buildDisplayStep(addon, effective, settingsModelIdFor(addon, effective, inheritedModelId, rootModel)),
29641
29793
  disabled: !settingsOverridden,
29642
29794
  onChange: (next) => setSettings({ ...next.settings })
29643
29795
  }, field.key)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.237",
3
+ "version": "1.2.239",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",