@camstack/ui-library 1.2.321 → 1.2.322

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.
@@ -38,9 +38,11 @@
38
38
  */
39
39
  import { type JSX } from "react";
40
40
  import type { PickerFleetView } from "./device-fleet-view";
41
- import { type PickerGroup, type PickerId, type PickerItem } from "./device-picker-model";
41
+ import { type PickerGroup, type PickerGroupBy, type PickerId, type PickerItem } from "./device-picker-model";
42
42
  import { type DeviceListState } from "./device-selection";
43
43
  import { type PickerNoun } from "./device-selection-rows";
44
+ /** One device, or the checklist every rule editor uses. */
45
+ export type DevicePickerSelection = "single" | "multiple";
44
46
  export interface DevicePickerProps<Id extends PickerId = number> {
45
47
  /** The devices the list DRAWS — the caller's answered list, `[]` otherwise. */
46
48
  readonly devices: readonly PickerItem<Id>[];
@@ -50,6 +52,16 @@ export interface DevicePickerProps<Id extends PickerId = number> {
50
52
  readonly fleet?: PickerFleetView<Id>;
51
53
  readonly value: readonly Id[];
52
54
  readonly onChange: (next: Id[]) => void;
55
+ /**
56
+ * `single`: one radio, no All/Clear, no "N selected". Choosing a row
57
+ * replaces `value` with that id. Default is the checklist.
58
+ */
59
+ readonly selection?: DevicePickerSelection;
60
+ /**
61
+ * Group this list by zone, location or none. Absent uses the remembered
62
+ * choice. A value here does not change that choice.
63
+ */
64
+ readonly groupBy?: PickerGroupBy;
53
65
  /** Empty selection means EVERY device: say so, and warn when it widens. */
54
66
  readonly emptyMeansAll?: boolean;
55
67
  /** Whether the saved rule had devices — the widening warning survives a remount. */
@@ -79,4 +91,4 @@ export interface DevicePickerProps<Id extends PickerId = number> {
79
91
  /** The line under a locked device. Default "Already added". */
80
92
  readonly lockedNote?: string;
81
93
  }
82
- export declare function DevicePicker<Id extends PickerId = number>({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll, savedHadDevices, noun, groupTitle: groupTitleProp, emptyLabel, annotate, disabled, testId, showAll, lockedIds, lockedNote: lockedNoteProp }: DevicePickerProps<Id>): JSX.Element;
94
+ export declare function DevicePicker<Id extends PickerId = number>({ devices: devicesIn, listState, fleet, value, onChange, selection, groupBy: pinnedGroupBy, emptyMeansAll, savedHadDevices, noun, groupTitle: groupTitleProp, emptyLabel, annotate, disabled, testId, showAll, lockedIds, lockedNote: lockedNoteProp }: DevicePickerProps<Id>): JSX.Element;
@@ -1,4 +1,4 @@
1
- export { DevicePicker, type DevicePickerProps } from "./DevicePicker.js";
1
+ export { DevicePicker, type DevicePickerProps, type DevicePickerSelection } from "./DevicePicker.js";
2
2
  export { DeviceFilterBar, typeLabel, type DeviceFilterBarGrouping, type DeviceFilterBarProps } from "./device-filter-bar.js";
3
3
  export { fleetViewOf, type DeviceFleetView, type PickerFleetView } from "./device-fleet-view.js";
4
4
  export { DEVICE_PICKER_GROUP_STORAGE_KEY, readStoredGroupBy, writeStoredGroupBy, useStoredGroupBy, usePickerSiteZones, type StoredGroupBy } from "./device-picker-grouping.js";
package/dist/index.cjs CHANGED
@@ -65327,7 +65327,7 @@ function noItemsSentence(noun, emptyMeansAll, t) {
65327
65327
  noun: pickerNounLabel(noun, t)
65328
65328
  });
65329
65329
  }
65330
- function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle: groupTitleProp, emptyLabel, annotate, disabled = false, testId, showAll = false, lockedIds, lockedNote: lockedNoteProp }) {
65330
+ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, selection = "multiple", groupBy: pinnedGroupBy, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle: groupTitleProp, emptyLabel, annotate, disabled = false, testId, showAll = false, lockedIds, lockedNote: lockedNoteProp }) {
65331
65331
  const t = useUiText();
65332
65332
  const groupTitle = groupTitleProp ?? ((group) => deviceGroupTitle(group, t));
65333
65333
  const lockedNote = lockedNoteProp ?? t("uiLibrary.devicePicker.alreadyAdded", "Already added");
@@ -65360,7 +65360,7 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65360
65360
  children: [
65361
65361
  listState === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesLoadingLine, { noun: plural }),
65362
65362
  listState === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesFailedLine, { noun: noun === "camera" ? "devices" : plural }),
65363
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65363
+ !(selection === "single" && chips.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65364
65364
  className: "flex flex-wrap items-center gap-1",
65365
65365
  children: [
65366
65366
  chips.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -65404,7 +65404,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65404
65404
  groupTitle,
65405
65405
  noun,
65406
65406
  disabled,
65407
- showAll
65407
+ showAll,
65408
+ selection,
65409
+ ...pinnedGroupBy !== void 0 ? { pinnedGroupBy } : {}
65408
65410
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
65409
65411
  devices,
65410
65412
  value,
@@ -65415,7 +65417,8 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65415
65417
  groupTitle,
65416
65418
  noun,
65417
65419
  disabled,
65418
- showAll
65420
+ showAll,
65421
+ selection
65419
65422
  })),
65420
65423
  widening && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesWidenWarning, { noun })
65421
65424
  ]
@@ -65426,13 +65429,23 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65426
65429
  * read below happens only then (for Zone grouping and the Zone filter).
65427
65430
  */
65428
65431
  function GroupedPickerPanel(props) {
65429
- const { groupBy, setGroupBy } = useStoredGroupBy();
65432
+ const stored = useStoredGroupBy();
65433
+ const [localGroupBy, setLocalGroupBy] = (0, react$1.useState)(null);
65434
+ const pinned = props.pinnedGroupBy;
65435
+ const groupBy = pinned !== void 0 ? localGroupBy ?? pinned : stored.groupBy;
65436
+ const onGroupByChange = (next) => {
65437
+ if (pinned !== void 0) {
65438
+ setLocalGroupBy(next);
65439
+ return;
65440
+ }
65441
+ stored.setGroupBy(next);
65442
+ };
65430
65443
  const zones = usePickerSiteZones(true);
65431
65444
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
65432
65445
  ...props,
65433
65446
  grouping: {
65434
65447
  groupBy,
65435
- onGroupByChange: setGroupBy,
65448
+ onGroupByChange,
65436
65449
  zones
65437
65450
  }
65438
65451
  });
@@ -65450,8 +65463,9 @@ var NO_ZONES = {
65450
65463
  zones: []
65451
65464
  };
65452
65465
  /** The searchable, grouped list — rendered one page at a time. */
65453
- function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, grouping }) {
65466
+ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, selection, grouping }) {
65454
65467
  const t = useUiText();
65468
+ const radioName = (0, react$1.useId)();
65455
65469
  const [filters, setFilters] = (0, react$1.useState)(PICKER_NO_FILTERS);
65456
65470
  const [toggles, setToggles] = (0, react$1.useState)(/* @__PURE__ */ new Map());
65457
65471
  const [shown, setShown] = (0, react$1.useState)(60);
@@ -65525,7 +65539,10 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
65525
65539
  }),
65526
65540
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
65527
65541
  className: "mt-1 text-[10px] text-foreground-subtle",
65528
- children: t("uiLibrary.devicePicker.shownOf", "{{shown}} of {{total}} shown · {{selected}} selected", {
65542
+ children: selection === "single" ? t("uiLibrary.devicePicker.shownOfSingle", "{{shown}} of {{total}} shown", {
65543
+ shown: matches.length,
65544
+ total: devices.length
65545
+ }) : t("uiLibrary.devicePicker.shownOf", "{{shown}} of {{total}} shown · {{selected}} selected", {
65529
65546
  shown: matches.length,
65530
65547
  total: devices.length,
65531
65548
  selected: value.length
@@ -65547,7 +65564,9 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
65547
65564
  groupBy: shownBy,
65548
65565
  disabled,
65549
65566
  locked,
65550
- lockedNote
65567
+ lockedNote,
65568
+ selection,
65569
+ radioName
65551
65570
  }, row.key)), page.remaining > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65552
65571
  type: "button",
65553
65572
  className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
@@ -65577,8 +65596,9 @@ function secondLine(device, groupBy) {
65577
65596
  ].filter((part) => part !== null);
65578
65597
  return parts.length > 0 ? parts.join(" · ") : void 0;
65579
65598
  }
65580
- function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote }) {
65599
+ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote, selection, radioName }) {
65581
65600
  const t = useUiText();
65601
+ const single = selection === "single";
65582
65602
  if (row.kind === "header") {
65583
65603
  const { group, expanded } = row;
65584
65604
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -65596,7 +65616,7 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65596
65616
  className: "truncate",
65597
65617
  children: groupTitle(group)
65598
65618
  }),
65599
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
65619
+ !single && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
65600
65620
  className: "font-normal normal-case",
65601
65621
  children: [
65602
65622
  "(",
@@ -65608,14 +65628,14 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65608
65628
  })
65609
65629
  ]
65610
65630
  }),
65611
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65631
+ !single && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65612
65632
  type: "button",
65613
65633
  disabled,
65614
65634
  className: "text-[10px] text-primary hover:underline disabled:opacity-50",
65615
65635
  onClick: () => onChange(selectPickerGroup(value, group)),
65616
65636
  children: t("uiLibrary.devicePicker.all", "All")
65617
65637
  }),
65618
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65638
+ !single && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65619
65639
  type: "button",
65620
65640
  disabled,
65621
65641
  className: "text-[10px] text-foreground-subtle hover:underline disabled:opacity-50",
@@ -65632,10 +65652,11 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65632
65652
  className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
65633
65653
  "data-testid": "device-picker-row",
65634
65654
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
65635
- type: "checkbox",
65655
+ type: single ? "radio" : "checkbox",
65656
+ ...single ? { name: radioName } : {},
65636
65657
  checked: row.selected || isLocked,
65637
65658
  disabled: disabled || isLocked,
65638
- onChange: () => onChange(togglePickerDevice(value, row.device.id)),
65659
+ onChange: () => onChange(single ? [row.device.id] : togglePickerDevice(value, row.device.id)),
65639
65660
  className: "accent-primary",
65640
65661
  "data-device-id": String(row.device.id)
65641
65662
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
package/dist/index.js CHANGED
@@ -65303,7 +65303,7 @@ function noItemsSentence(noun, emptyMeansAll, t) {
65303
65303
  noun: pickerNounLabel(noun, t)
65304
65304
  });
65305
65305
  }
65306
- function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle: groupTitleProp, emptyLabel, annotate, disabled = false, testId, showAll = false, lockedIds, lockedNote: lockedNoteProp }) {
65306
+ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, selection = "multiple", groupBy: pinnedGroupBy, emptyMeansAll = false, savedHadDevices = false, noun = "camera", groupTitle: groupTitleProp, emptyLabel, annotate, disabled = false, testId, showAll = false, lockedIds, lockedNote: lockedNoteProp }) {
65307
65307
  const t = useUiText();
65308
65308
  const groupTitle = groupTitleProp ?? ((group) => deviceGroupTitle(group, t));
65309
65309
  const lockedNote = lockedNoteProp ?? t("uiLibrary.devicePicker.alreadyAdded", "Already added");
@@ -65336,7 +65336,7 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65336
65336
  children: [
65337
65337
  listState === "pending" && /* @__PURE__ */ jsx(DevicesLoadingLine, { noun: plural }),
65338
65338
  listState === "failed" && /* @__PURE__ */ jsx(DevicesFailedLine, { noun: noun === "camera" ? "devices" : plural }),
65339
- /* @__PURE__ */ jsxs("div", {
65339
+ !(selection === "single" && chips.length === 0) && /* @__PURE__ */ jsxs("div", {
65340
65340
  className: "flex flex-wrap items-center gap-1",
65341
65341
  children: [
65342
65342
  chips.length === 0 ? /* @__PURE__ */ jsx("span", {
@@ -65380,7 +65380,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65380
65380
  groupTitle,
65381
65381
  noun,
65382
65382
  disabled,
65383
- showAll
65383
+ showAll,
65384
+ selection,
65385
+ ...pinnedGroupBy !== void 0 ? { pinnedGroupBy } : {}
65384
65386
  }) : /* @__PURE__ */ jsx(PickerPanel, {
65385
65387
  devices,
65386
65388
  value,
@@ -65391,7 +65393,8 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65391
65393
  groupTitle,
65392
65394
  noun,
65393
65395
  disabled,
65394
- showAll
65396
+ showAll,
65397
+ selection
65395
65398
  })),
65396
65399
  widening && /* @__PURE__ */ jsx(DevicesWidenWarning, { noun })
65397
65400
  ]
@@ -65402,13 +65405,23 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65402
65405
  * read below happens only then (for Zone grouping and the Zone filter).
65403
65406
  */
65404
65407
  function GroupedPickerPanel(props) {
65405
- const { groupBy, setGroupBy } = useStoredGroupBy();
65408
+ const stored = useStoredGroupBy();
65409
+ const [localGroupBy, setLocalGroupBy] = useState(null);
65410
+ const pinned = props.pinnedGroupBy;
65411
+ const groupBy = pinned !== void 0 ? localGroupBy ?? pinned : stored.groupBy;
65412
+ const onGroupByChange = (next) => {
65413
+ if (pinned !== void 0) {
65414
+ setLocalGroupBy(next);
65415
+ return;
65416
+ }
65417
+ stored.setGroupBy(next);
65418
+ };
65406
65419
  const zones = usePickerSiteZones(true);
65407
65420
  return /* @__PURE__ */ jsx(PickerPanel, {
65408
65421
  ...props,
65409
65422
  grouping: {
65410
65423
  groupBy,
65411
- onGroupByChange: setGroupBy,
65424
+ onGroupByChange,
65412
65425
  zones
65413
65426
  }
65414
65427
  });
@@ -65426,8 +65439,9 @@ var NO_ZONES = {
65426
65439
  zones: []
65427
65440
  };
65428
65441
  /** The searchable, grouped list — rendered one page at a time. */
65429
- function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, grouping }) {
65442
+ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, selection, grouping }) {
65430
65443
  const t = useUiText();
65444
+ const radioName = useId();
65431
65445
  const [filters, setFilters] = useState(PICKER_NO_FILTERS);
65432
65446
  const [toggles, setToggles] = useState(/* @__PURE__ */ new Map());
65433
65447
  const [shown, setShown] = useState(60);
@@ -65501,7 +65515,10 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
65501
65515
  }),
65502
65516
  /* @__PURE__ */ jsx("p", {
65503
65517
  className: "mt-1 text-[10px] text-foreground-subtle",
65504
- children: t("uiLibrary.devicePicker.shownOf", "{{shown}} of {{total}} shown · {{selected}} selected", {
65518
+ children: selection === "single" ? t("uiLibrary.devicePicker.shownOfSingle", "{{shown}} of {{total}} shown", {
65519
+ shown: matches.length,
65520
+ total: devices.length
65521
+ }) : t("uiLibrary.devicePicker.shownOf", "{{shown}} of {{total}} shown · {{selected}} selected", {
65505
65522
  shown: matches.length,
65506
65523
  total: devices.length,
65507
65524
  selected: value.length
@@ -65523,7 +65540,9 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
65523
65540
  groupBy: shownBy,
65524
65541
  disabled,
65525
65542
  locked,
65526
- lockedNote
65543
+ lockedNote,
65544
+ selection,
65545
+ radioName
65527
65546
  }, row.key)), page.remaining > 0 && /* @__PURE__ */ jsx("button", {
65528
65547
  type: "button",
65529
65548
  className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
@@ -65553,8 +65572,9 @@ function secondLine(device, groupBy) {
65553
65572
  ].filter((part) => part !== null);
65554
65573
  return parts.length > 0 ? parts.join(" · ") : void 0;
65555
65574
  }
65556
- function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote }) {
65575
+ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote, selection, radioName }) {
65557
65576
  const t = useUiText();
65577
+ const single = selection === "single";
65558
65578
  if (row.kind === "header") {
65559
65579
  const { group, expanded } = row;
65560
65580
  return /* @__PURE__ */ jsxs("div", {
@@ -65572,7 +65592,7 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65572
65592
  className: "truncate",
65573
65593
  children: groupTitle(group)
65574
65594
  }),
65575
- /* @__PURE__ */ jsxs("span", {
65595
+ !single && /* @__PURE__ */ jsxs("span", {
65576
65596
  className: "font-normal normal-case",
65577
65597
  children: [
65578
65598
  "(",
@@ -65584,14 +65604,14 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65584
65604
  })
65585
65605
  ]
65586
65606
  }),
65587
- /* @__PURE__ */ jsx("button", {
65607
+ !single && /* @__PURE__ */ jsx("button", {
65588
65608
  type: "button",
65589
65609
  disabled,
65590
65610
  className: "text-[10px] text-primary hover:underline disabled:opacity-50",
65591
65611
  onClick: () => onChange(selectPickerGroup(value, group)),
65592
65612
  children: t("uiLibrary.devicePicker.all", "All")
65593
65613
  }),
65594
- /* @__PURE__ */ jsx("button", {
65614
+ !single && /* @__PURE__ */ jsx("button", {
65595
65615
  type: "button",
65596
65616
  disabled,
65597
65617
  className: "text-[10px] text-foreground-subtle hover:underline disabled:opacity-50",
@@ -65608,10 +65628,11 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65608
65628
  className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
65609
65629
  "data-testid": "device-picker-row",
65610
65630
  children: [/* @__PURE__ */ jsx("input", {
65611
- type: "checkbox",
65631
+ type: single ? "radio" : "checkbox",
65632
+ ...single ? { name: radioName } : {},
65612
65633
  checked: row.selected || isLocked,
65613
65634
  disabled: disabled || isLocked,
65614
- onChange: () => onChange(togglePickerDevice(value, row.device.id)),
65635
+ onChange: () => onChange(single ? [row.device.id] : togglePickerDevice(value, row.device.id)),
65615
65636
  className: "accent-primary",
65616
65637
  "data-device-id": String(row.device.id)
65617
65638
  }), /* @__PURE__ */ jsxs("span", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.321",
3
+ "version": "1.2.322",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",