@camstack/ui-library 1.2.290 → 1.2.291

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.
@@ -70,5 +70,13 @@ export interface DevicePickerProps<Id extends PickerId = number> {
70
70
  * surface whose whole job is the list — the rule's Devices tab (R15).
71
71
  */
72
72
  readonly showAll?: boolean;
73
+ /**
74
+ * Devices that are ALREADY part of the thing being edited (linked to the
75
+ * camera, assigned to the room): drawn checked and disabled with
76
+ * `lockedNote`, never part of `value`, never emitted by `onChange`.
77
+ */
78
+ readonly lockedIds?: readonly Id[];
79
+ /** The line under a locked device. Default "Already added". */
80
+ readonly lockedNote?: string;
73
81
  }
74
- export declare function DevicePicker<Id extends PickerId = number>({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll, savedHadDevices, noun, groupTitle, emptyLabel, annotate, disabled, testId, showAll }: DevicePickerProps<Id>): JSX.Element;
82
+ export declare function DevicePicker<Id extends PickerId = number>({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll, savedHadDevices, noun, groupTitle, emptyLabel, annotate, disabled, testId, showAll, lockedIds, lockedNote }: DevicePickerProps<Id>): JSX.Element;
@@ -17,6 +17,12 @@ type OptionSelectSharedProps = {
17
17
  };
18
18
  export type OptionSelectSingleProps = OptionSelectSharedProps & {
19
19
  readonly mode: "single";
20
+ /**
21
+ * `chip` (default): the choice is a removable chip above the field.
22
+ * `field`: the field itself shows the current choice (placeholder-style, so
23
+ * typing searches), no chip and no clear — for a choice that always exists.
24
+ */
25
+ readonly singleDisplay?: "chip" | "field";
20
26
  readonly value: string;
21
27
  readonly onChange: (next: string) => void;
22
28
  };
package/dist/index.cjs CHANGED
@@ -13614,6 +13614,7 @@ function OptionSelect(props) {
13614
13614
  }, opt.value);
13615
13615
  }) });
13616
13616
  const footerAddNewIndex = dropdownRows.length;
13617
+ const fieldShowsChoice = mode === "single" && props.singleDisplay === "field" && selectedOptions.length > 0;
13617
13618
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13618
13619
  className: "min-w-0 space-y-1",
13619
13620
  "data-testid": testId,
@@ -13632,7 +13633,7 @@ function OptionSelect(props) {
13632
13633
  ...optionTestIdPrefix !== void 0 ? { testId: `${optionTestIdPrefix}-chip-${opt.value}` } : {}
13633
13634
  }, opt.value))
13634
13635
  }),
13635
- mode === "single" && selectedOptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13636
+ mode === "single" && props.singleDisplay !== "field" && selectedOptions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13636
13637
  className: "flex flex-wrap gap-1 mb-1.5",
13637
13638
  children: selectedOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectedChip, {
13638
13639
  label: opt.label,
@@ -13655,8 +13656,8 @@ function OptionSelect(props) {
13655
13656
  "aria-expanded": open,
13656
13657
  "aria-controls": listboxId,
13657
13658
  className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-foreground-disabled",
13658
- value: query,
13659
- placeholder: triggerPlaceholder,
13659
+ value: fieldShowsChoice && !open && query === "" ? selectedOptions[0]?.label ?? "" : query,
13660
+ placeholder: fieldShowsChoice ? selectedOptions[0]?.label ?? "" : triggerPlaceholder,
13660
13661
  disabled: isDisabled,
13661
13662
  onChange: (e) => {
13662
13663
  setQuery(e.target.value);
@@ -60755,12 +60756,14 @@ function noItemsSentence(noun, emptyMeansAll) {
60755
60756
  if (noun === "camera" || noun === "device") return "No cameras available — the rule applies to all devices.";
60756
60757
  return `No ${plural} available — the rule applies to every ${noun}.`;
60757
60758
  }
60758
- function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle = deviceGroupTitle, emptyLabel, annotate, disabled = false, testId, showAll = false }) {
60759
+ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle = deviceGroupTitle, emptyLabel, annotate, disabled = false, testId, showAll = false, lockedIds, lockedNote = "Already added" }) {
60759
60760
  const devices = (0, react$1.useMemo)(() => withResolvedParentDetail(devicesIn), [devicesIn]);
60760
60761
  const judge = fleet ?? {
60761
60762
  state: listState,
60762
60763
  devices: listState === "answered" ? devices : void 0
60763
60764
  };
60765
+ const locked = (0, react$1.useMemo)(() => new Set(lockedIds ?? []), [lockedIds]);
60766
+ const emit = (next) => onChange(locked.size === 0 ? next : next.filter((id) => !locked.has(id)));
60764
60767
  const [open, setOpen] = (0, react$1.useState)(false);
60765
60768
  const [chipsExpanded, setChipsExpanded] = (0, react$1.useState)(false);
60766
60769
  const [hadDevices, setHadDevices] = (0, react$1.useState)(savedHadDevices || value.length > 0);
@@ -60824,7 +60827,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
60824
60827
  listOpen && listState === "answered" && (isDeviceNoun(noun) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedPickerPanel, {
60825
60828
  devices,
60826
60829
  value,
60827
- onChange,
60830
+ onChange: emit,
60831
+ locked,
60832
+ lockedNote,
60828
60833
  annotate,
60829
60834
  groupTitle,
60830
60835
  noun,
@@ -60833,7 +60838,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
60833
60838
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
60834
60839
  devices,
60835
60840
  value,
60836
- onChange,
60841
+ onChange: emit,
60842
+ locked,
60843
+ lockedNote,
60837
60844
  annotate,
60838
60845
  groupTitle,
60839
60846
  noun,
@@ -60873,7 +60880,7 @@ var NO_ZONES = {
60873
60880
  zones: []
60874
60881
  };
60875
60882
  /** The searchable, grouped list — rendered one page at a time. */
60876
- function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, showAll, grouping }) {
60883
+ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, grouping }) {
60877
60884
  const [filters, setFilters] = (0, react$1.useState)(PICKER_NO_FILTERS);
60878
60885
  const [toggles, setToggles] = (0, react$1.useState)(/* @__PURE__ */ new Map());
60879
60886
  const [shown, setShown] = (0, react$1.useState)(60);
@@ -60970,7 +60977,9 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
60970
60977
  annotate,
60971
60978
  groupTitle,
60972
60979
  groupBy: shownBy,
60973
- disabled
60980
+ disabled,
60981
+ locked,
60982
+ lockedNote
60974
60983
  }, row.key)), page.remaining > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
60975
60984
  type: "button",
60976
60985
  className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
@@ -61003,7 +61012,7 @@ function secondLine(device, groupBy) {
61003
61012
  ].filter((part) => part !== null);
61004
61013
  return parts.length > 0 ? parts.join(" · ") : void 0;
61005
61014
  }
61006
- function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled }) {
61015
+ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote }) {
61007
61016
  if (row.kind === "header") {
61008
61017
  const { group, expanded } = row;
61009
61018
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -61050,15 +61059,16 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
61050
61059
  ]
61051
61060
  });
61052
61061
  }
61053
- const note = annotate?.(row.device.id);
61062
+ const isLocked = locked.has(row.device.id);
61063
+ const note = isLocked ? lockedNote : annotate?.(row.device.id);
61054
61064
  const second = secondLine(row.device, groupBy);
61055
61065
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
61056
61066
  className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
61057
61067
  "data-testid": "device-picker-row",
61058
61068
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
61059
61069
  type: "checkbox",
61060
- checked: row.selected,
61061
- disabled,
61070
+ checked: row.selected || isLocked,
61071
+ disabled: disabled || isLocked,
61062
61072
  onChange: () => onChange(togglePickerDevice(value, row.device.id)),
61063
61073
  className: "accent-primary",
61064
61074
  "data-device-id": String(row.device.id)
@@ -61074,7 +61084,7 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
61074
61084
  children: second
61075
61085
  }),
61076
61086
  note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
61077
- className: "text-[10px] text-warning",
61087
+ className: isLocked ? "text-[10px] text-foreground-subtle" : "text-[10px] text-warning",
61078
61088
  children: note
61079
61089
  })
61080
61090
  ]
package/dist/index.js CHANGED
@@ -13590,6 +13590,7 @@ function OptionSelect(props) {
13590
13590
  }, opt.value);
13591
13591
  }) });
13592
13592
  const footerAddNewIndex = dropdownRows.length;
13593
+ const fieldShowsChoice = mode === "single" && props.singleDisplay === "field" && selectedOptions.length > 0;
13593
13594
  return /* @__PURE__ */ jsxs("div", {
13594
13595
  className: "min-w-0 space-y-1",
13595
13596
  "data-testid": testId,
@@ -13608,7 +13609,7 @@ function OptionSelect(props) {
13608
13609
  ...optionTestIdPrefix !== void 0 ? { testId: `${optionTestIdPrefix}-chip-${opt.value}` } : {}
13609
13610
  }, opt.value))
13610
13611
  }),
13611
- mode === "single" && selectedOptions.length > 0 && /* @__PURE__ */ jsx("div", {
13612
+ mode === "single" && props.singleDisplay !== "field" && selectedOptions.length > 0 && /* @__PURE__ */ jsx("div", {
13612
13613
  className: "flex flex-wrap gap-1 mb-1.5",
13613
13614
  children: selectedOptions.map((opt) => /* @__PURE__ */ jsx(SelectedChip, {
13614
13615
  label: opt.label,
@@ -13631,8 +13632,8 @@ function OptionSelect(props) {
13631
13632
  "aria-expanded": open,
13632
13633
  "aria-controls": listboxId,
13633
13634
  className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-foreground-disabled",
13634
- value: query,
13635
- placeholder: triggerPlaceholder,
13635
+ value: fieldShowsChoice && !open && query === "" ? selectedOptions[0]?.label ?? "" : query,
13636
+ placeholder: fieldShowsChoice ? selectedOptions[0]?.label ?? "" : triggerPlaceholder,
13636
13637
  disabled: isDisabled,
13637
13638
  onChange: (e) => {
13638
13639
  setQuery(e.target.value);
@@ -60731,12 +60732,14 @@ function noItemsSentence(noun, emptyMeansAll) {
60731
60732
  if (noun === "camera" || noun === "device") return "No cameras available — the rule applies to all devices.";
60732
60733
  return `No ${plural} available — the rule applies to every ${noun}.`;
60733
60734
  }
60734
- function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle = deviceGroupTitle, emptyLabel, annotate, disabled = false, testId, showAll = false }) {
60735
+ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle = deviceGroupTitle, emptyLabel, annotate, disabled = false, testId, showAll = false, lockedIds, lockedNote = "Already added" }) {
60735
60736
  const devices = useMemo(() => withResolvedParentDetail(devicesIn), [devicesIn]);
60736
60737
  const judge = fleet ?? {
60737
60738
  state: listState,
60738
60739
  devices: listState === "answered" ? devices : void 0
60739
60740
  };
60741
+ const locked = useMemo(() => new Set(lockedIds ?? []), [lockedIds]);
60742
+ const emit = (next) => onChange(locked.size === 0 ? next : next.filter((id) => !locked.has(id)));
60740
60743
  const [open, setOpen] = useState(false);
60741
60744
  const [chipsExpanded, setChipsExpanded] = useState(false);
60742
60745
  const [hadDevices, setHadDevices] = useState(savedHadDevices || value.length > 0);
@@ -60800,7 +60803,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
60800
60803
  listOpen && listState === "answered" && (isDeviceNoun(noun) ? /* @__PURE__ */ jsx(GroupedPickerPanel, {
60801
60804
  devices,
60802
60805
  value,
60803
- onChange,
60806
+ onChange: emit,
60807
+ locked,
60808
+ lockedNote,
60804
60809
  annotate,
60805
60810
  groupTitle,
60806
60811
  noun,
@@ -60809,7 +60814,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
60809
60814
  }) : /* @__PURE__ */ jsx(PickerPanel, {
60810
60815
  devices,
60811
60816
  value,
60812
- onChange,
60817
+ onChange: emit,
60818
+ locked,
60819
+ lockedNote,
60813
60820
  annotate,
60814
60821
  groupTitle,
60815
60822
  noun,
@@ -60849,7 +60856,7 @@ var NO_ZONES = {
60849
60856
  zones: []
60850
60857
  };
60851
60858
  /** The searchable, grouped list — rendered one page at a time. */
60852
- function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, showAll, grouping }) {
60859
+ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, grouping }) {
60853
60860
  const [filters, setFilters] = useState(PICKER_NO_FILTERS);
60854
60861
  const [toggles, setToggles] = useState(/* @__PURE__ */ new Map());
60855
60862
  const [shown, setShown] = useState(60);
@@ -60946,7 +60953,9 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
60946
60953
  annotate,
60947
60954
  groupTitle,
60948
60955
  groupBy: shownBy,
60949
- disabled
60956
+ disabled,
60957
+ locked,
60958
+ lockedNote
60950
60959
  }, row.key)), page.remaining > 0 && /* @__PURE__ */ jsxs("button", {
60951
60960
  type: "button",
60952
60961
  className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
@@ -60979,7 +60988,7 @@ function secondLine(device, groupBy) {
60979
60988
  ].filter((part) => part !== null);
60980
60989
  return parts.length > 0 ? parts.join(" · ") : void 0;
60981
60990
  }
60982
- function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled }) {
60991
+ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote }) {
60983
60992
  if (row.kind === "header") {
60984
60993
  const { group, expanded } = row;
60985
60994
  return /* @__PURE__ */ jsxs("div", {
@@ -61026,15 +61035,16 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
61026
61035
  ]
61027
61036
  });
61028
61037
  }
61029
- const note = annotate?.(row.device.id);
61038
+ const isLocked = locked.has(row.device.id);
61039
+ const note = isLocked ? lockedNote : annotate?.(row.device.id);
61030
61040
  const second = secondLine(row.device, groupBy);
61031
61041
  return /* @__PURE__ */ jsxs("label", {
61032
61042
  className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
61033
61043
  "data-testid": "device-picker-row",
61034
61044
  children: [/* @__PURE__ */ jsx("input", {
61035
61045
  type: "checkbox",
61036
- checked: row.selected,
61037
- disabled,
61046
+ checked: row.selected || isLocked,
61047
+ disabled: disabled || isLocked,
61038
61048
  onChange: () => onChange(togglePickerDevice(value, row.device.id)),
61039
61049
  className: "accent-primary",
61040
61050
  "data-device-id": String(row.device.id)
@@ -61050,7 +61060,7 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
61050
61060
  children: second
61051
61061
  }),
61052
61062
  note !== void 0 && /* @__PURE__ */ jsx("span", {
61053
- className: "text-[10px] text-warning",
61063
+ className: isLocked ? "text-[10px] text-foreground-subtle" : "text-[10px] text-warning",
61054
61064
  children: note
61055
61065
  })
61056
61066
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.290",
3
+ "version": "1.2.291",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",