@camstack/ui-library 1.2.221 → 1.2.223

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,35 @@
1
+ import { PickerFleetView } from './device-fleet-view';
2
+ import { PickerGroup, PickerId, PickerItem } from './device-picker-model';
3
+ import { DeviceListState } from './device-selection';
4
+ import { PickerNoun } from './device-selection-rows';
5
+ export interface DevicePickerProps<Id extends PickerId = number> {
6
+ /** The devices the list DRAWS — the caller's answered list, `[]` otherwise. */
7
+ readonly devices: readonly PickerItem<Id>[];
8
+ /** Whether `devices` ANSWERED (D315). */
9
+ readonly listState: DeviceListState;
10
+ /** What an off-list selected id is judged against. Defaults to the list. */
11
+ readonly fleet?: PickerFleetView<Id>;
12
+ readonly value: readonly Id[];
13
+ readonly onChange: (next: Id[]) => void;
14
+ /** Empty selection means EVERY device: say so, and warn when it widens. */
15
+ readonly emptyMeansAll?: boolean;
16
+ /** Whether the saved rule had devices — the widening warning survives a remount. */
17
+ readonly savedHadDevices?: boolean;
18
+ /** What the list holds, for its sentences. */
19
+ readonly noun?: PickerNoun;
20
+ /** A group header's title. Defaults to "<type> · <room>". */
21
+ readonly groupTitle?: (group: PickerGroup<Id>) => string;
22
+ /** What an empty selection reads as, when it does not mean "every device". */
23
+ readonly emptyLabel?: string;
24
+ /** A per-device note drawn beside the name (the alarm's "not covered"). */
25
+ readonly annotate?: (id: Id) => string | undefined;
26
+ readonly disabled?: boolean;
27
+ readonly testId?: string;
28
+ /**
29
+ * List EVERYTHING, always open: no Edit/Done toggle, no "+N more" chips, no
30
+ * "Show N more" rows, every group open until the operator folds one. For a
31
+ * surface whose whole job is the list — the rule's Devices tab (R15).
32
+ */
33
+ readonly showAll?: boolean;
34
+ }
35
+ export declare function DevicePicker<Id extends PickerId = number>({ devices, listState, fleet, value, onChange, emptyMeansAll, savedHadDevices, noun, groupTitle, emptyLabel, annotate, disabled, testId, showAll, }: DevicePickerProps<Id>): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ /** The search box's shell — shared with multiselect fields that mirror this picker. */
2
+ export declare const INPUT_SHELL = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
3
+ export interface SelectedChipProps {
4
+ readonly label: string;
5
+ readonly onRemove?: () => void;
6
+ /** A selection the hub no longer has — drawn in warning colour. */
7
+ readonly warning?: boolean;
8
+ readonly iconId?: string;
9
+ readonly testId?: string;
10
+ }
11
+ /** One selected entry as a removable chip. */
12
+ export declare function SelectedChip({ label, onRemove, warning, iconId, testId, }: SelectedChipProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { PickerFacetOption, PickerFilters, PickerGroupBy, PickerZonesFallback, PickerZonesRead } from './device-picker-model';
2
+ /** The Group-by control's inputs, as `DevicePicker.tsx` passes them. */
3
+ export interface DeviceFilterBarGrouping {
4
+ readonly groupBy: PickerGroupBy;
5
+ readonly onGroupByChange: (next: PickerGroupBy) => void;
6
+ /** Set when Zone mode is showing locations because the zones had not answered. */
7
+ readonly fallback: PickerZonesFallback | null;
8
+ }
9
+ export interface DeviceFilterBarProps {
10
+ readonly filters: PickerFilters;
11
+ readonly onFiltersChange: (next: PickerFilters) => void;
12
+ readonly typeOptions: readonly PickerFacetOption[];
13
+ readonly zoneOptions: readonly PickerFacetOption[];
14
+ readonly locationOptions: readonly PickerFacetOption[];
15
+ /** The zones read — decides whether the Zone dropdown is enabled and what it says. */
16
+ readonly zones: PickerZonesRead;
17
+ /** Group by, when the list offers it (the device picker and the alarm table do). */
18
+ readonly grouping?: DeviceFilterBarGrouping;
19
+ readonly searchPlaceholder: string;
20
+ readonly searchLabel: string;
21
+ readonly disabled?: boolean;
22
+ }
23
+ /** "media-player" → "Media player"; null → "No type". */
24
+ export declare function typeLabel(type: string | null): string;
25
+ export declare function DeviceFilterBar({ filters, onFiltersChange, typeOptions, zoneOptions, locationOptions, zones, grouping, searchPlaceholder, searchLabel, disabled, }: DeviceFilterBarProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { DeviceListQueryLike, DeviceListState, NamedDevice, SelectionId } from './device-selection';
2
+ export interface PickerFleetView<Id extends SelectionId = number> {
3
+ readonly state: DeviceListState;
4
+ readonly devices: readonly NamedDevice<Id>[] | undefined;
5
+ }
6
+ export type DeviceFleetView = PickerFleetView<number>;
7
+ export declare function fleetViewOf(query: DeviceListQueryLike): DeviceFleetView;
@@ -0,0 +1,14 @@
1
+ import { PickerGroupBy, PickerZonesRead } from './device-picker-model';
2
+ export declare const DEVICE_PICKER_GROUP_STORAGE_KEY = "camstack.admin.device-picker.group-by";
3
+ /** The stored choice; the default when storage is absent, throws or is unreadable. */
4
+ export declare function readStoredGroupBy(): PickerGroupBy;
5
+ /** Remember the choice; a storage that refuses keeps it for this page only. */
6
+ export declare function writeStoredGroupBy(next: PickerGroupBy): void;
7
+ /** The group-by choice, seeded from and written to `localStorage`. */
8
+ export interface StoredGroupBy {
9
+ readonly groupBy: PickerGroupBy;
10
+ readonly setGroupBy: (next: PickerGroupBy) => void;
11
+ }
12
+ export declare function useStoredGroupBy(): StoredGroupBy;
13
+ /** The site zones, with whether the read ANSWERED — read only while `enabled`. */
14
+ export declare function usePickerSiteZones(enabled: boolean): PickerZonesRead;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * The smart device picker's decisions — search, groups, per-group bulk
3
+ * actions, the chip overflow and the bounded render — with no UI in it.
4
+ *
5
+ * One picker replaces every long device checklist in the notification editors
6
+ * (a rule's devices, a system rule's include/exclude, the timelapse and digest
7
+ * cameras, the alarm's non-blocking openings). A checklist of 293 rows on the
8
+ * admin, and of every camera on a phone, took the whole screen to say "two
9
+ * devices"; the picker shows the selection as chips and opens a searchable,
10
+ * grouped list only to change it.
11
+ *
12
+ * Since D637 the same model also serves the system family's node and package
13
+ * pickers, keyed by string: every item and function is generic over
14
+ * {@link PickerId}, and a device picker is simply `PickerItem<number>`.
15
+ *
16
+ * It has to hold at HUNDREDS of devices of ONE type (a fleet of HA sensors),
17
+ * which is what shapes it:
18
+ *
19
+ * - search is a debounced, case-insensitive AND over name, room, type,
20
+ * container and id — "sala temp" finds the living-room thermometer among
21
+ * nine "Temperatura";
22
+ * - the DEVICE picker groups by what the operator chose at the top of the
23
+ * list — site zone (default), location, or nothing
24
+ * ({@link groupPickerDevicesBy}); the node and package pickers keep
25
+ * (type, liveness) groups;
26
+ * - (type, room) groups, where they are still used — and any group bigger than
27
+ * {@link PICKER_GROUP_COLLAPSE_AT} starts COLLAPSED unless a search is
28
+ * running or it holds a selected device — a collapsed header still says how
29
+ * many it holds and offers select-all / clear;
30
+ * - the list is rendered in bounded pieces ({@link pageRows} for a capped
31
+ * render with "show N more"; a virtualised list sizes itself from
32
+ * {@link PICKER_WINDOW});
33
+ * - the list can be NARROWED by search plus Type / Zone / Location filters
34
+ * ({@link applyPickerFilters}); "Reset filters" clears those and leaves
35
+ * "Group by" alone ({@link resetPickerFilters}, D697);
36
+ * - the chips show at most {@link PICKER_CHIP_LIMIT} and a "+N more".
37
+ *
38
+ * It reads NOTHING. It filters the list the caller already holds; an id the
39
+ * list lacks is the caller's to resolve (`device-selection.ts`), which is also
40
+ * where D315 and D49 live.
41
+ *
42
+ * TWO BYTE-IDENTICAL COPIES: `camstack/src/components/notifications/
43
+ * device-picker-model.ts` (viewer) and `packages/addon-admin-ui/src/pages/
44
+ * system/device-picker-model.ts` (admin), for the reason `device-selection.ts`
45
+ * has two. `scripts/check-device-selection-in-sync.ts` fails the commit when
46
+ * they differ. No `toSorted` here: the viewer runs on Hermes.
47
+ */
48
+ /** What a picker item is keyed by: a device id, or a node id / package name (D637). */
49
+ export type PickerId = number | string;
50
+ /** One item as the picker lists it. */
51
+ export interface PickerItem<Id extends PickerId> {
52
+ readonly id: Id;
53
+ readonly name: string;
54
+ /** Groups by this (`DeviceType`, node role, package kind); absent groups under "no type". */
55
+ readonly type?: string;
56
+ /** The second grouping axis (room, liveness); absent or null groups under "none". */
57
+ readonly location?: string | null;
58
+ /** A second line — the container a child device belongs to, a node's id, a version. */
59
+ readonly detail?: string;
60
+ }
61
+ /** One device as the picker lists it. */
62
+ export type PickerDevice = PickerItem<number>;
63
+ /** How long the search waits after the last keystroke. */
64
+ export declare const PICKER_SEARCH_DEBOUNCE_MS = 200;
65
+ /** A group with more devices than this starts collapsed (unless searching). */
66
+ export declare const PICKER_GROUP_COLLAPSE_AT = 8;
67
+ /** How many selected chips are drawn before "+N more". */
68
+ export declare const PICKER_CHIP_LIMIT = 6;
69
+ /** Rows a capped render draws per "show more". */
70
+ export declare const PICKER_PAGE_SIZE = 60;
71
+ /** Rows a virtualised list renders per batch and initially. */
72
+ export declare const PICKER_WINDOW = 30;
73
+ /** Everything a query can match on, lower-cased. */
74
+ export declare function pickerSearchText(device: PickerItem<PickerId>): string;
75
+ /** The query's tokens; empty for a blank query. */
76
+ export declare function pickerQueryTokens(query: string): readonly string[];
77
+ /** Devices matching EVERY token. A blank query keeps the list as it is. */
78
+ export declare function filterPickerDevices<Id extends PickerId>(devices: readonly PickerItem<Id>[], query: string): readonly PickerItem<Id>[];
79
+ /**
80
+ * What a "Group by" group is named after. Absent on the (type, room) groups
81
+ * the node and package pickers still use.
82
+ */
83
+ export type PickerGroupHeading = {
84
+ readonly kind: 'zone';
85
+ readonly label: string;
86
+ } | {
87
+ readonly kind: 'location';
88
+ readonly label: string;
89
+ } | {
90
+ readonly kind: 'no-zone';
91
+ } | {
92
+ readonly kind: 'no-location';
93
+ };
94
+ /** One (type, room) group — or, under "Group by", one zone or location. */
95
+ export interface PickerGroup<Id extends PickerId = number> {
96
+ readonly key: string;
97
+ readonly type: string | null;
98
+ readonly location: string | null;
99
+ readonly devices: readonly PickerItem<Id>[];
100
+ readonly selectedCount: number;
101
+ readonly heading?: PickerGroupHeading;
102
+ }
103
+ /** The key a group is remembered by (collapsed / expanded). */
104
+ export declare function pickerGroupKey(type: string | null, location: string | null): string;
105
+ /** Group by type, then room; both ascending, "none" last; devices by name. */
106
+ export declare function groupPickerDevices<Id extends PickerId>(devices: readonly PickerItem<Id>[], selected: readonly Id[]): readonly PickerGroup<Id>[];
107
+ /**
108
+ * Whether a group is open. The operator's own toggle wins; otherwise a small
109
+ * group is open, a big one is closed — unless a search is running (the result
110
+ * IS what they asked to see) or it holds a selected device.
111
+ */
112
+ export declare function isGroupExpanded<Id extends PickerId>(group: PickerGroup<Id>, toggles: ReadonlyMap<string, boolean>, searching: boolean): boolean;
113
+ /** One row of the flattened list: a group header, or a device under it. */
114
+ export type PickerRow<Id extends PickerId = number> = {
115
+ readonly kind: 'header';
116
+ readonly key: string;
117
+ readonly group: PickerGroup<Id>;
118
+ readonly expanded: boolean;
119
+ } | {
120
+ readonly kind: 'device';
121
+ readonly key: string;
122
+ readonly device: PickerItem<Id>;
123
+ readonly groupKey: string;
124
+ readonly selected: boolean;
125
+ };
126
+ /** The groups as ONE list of rows — what a virtualised list renders. */
127
+ export declare function flattenPickerRows<Id extends PickerId>(groups: readonly PickerGroup<Id>[], selected: readonly Id[], toggles: ReadonlyMap<string, boolean>, searching: boolean): readonly PickerRow<Id>[];
128
+ /** Tick or untick one id. The order of the rest is kept. */
129
+ export declare function togglePickerDevice<Id extends PickerId>(value: readonly Id[], id: Id): Id[];
130
+ /** Add every device of the group (only the ones the current search shows). */
131
+ export declare function selectPickerGroup<Id extends PickerId>(value: readonly Id[], group: PickerGroup<Id>): Id[];
132
+ /** Remove every device of the group; ids outside it are untouched. */
133
+ export declare function clearPickerGroup<Id extends PickerId>(value: readonly Id[], group: PickerGroup<Id>): Id[];
134
+ /** The chips to draw, and how many are behind "+N more". */
135
+ export interface ChipOverflow<T> {
136
+ readonly shown: readonly T[];
137
+ readonly hidden: number;
138
+ }
139
+ export declare function chipOverflow<T>(items: readonly T[], expanded: boolean): ChipOverflow<T>;
140
+ /** A capped render: the first `shown` rows, and how many are left. */
141
+ export interface PickerPage<Id extends PickerId = number> {
142
+ readonly rows: readonly PickerRow<Id>[];
143
+ readonly remaining: number;
144
+ }
145
+ export declare function pageRows<Id extends PickerId>(rows: readonly PickerRow<Id>[], shown: number): PickerPage<Id>;
146
+ /** How the device picker groups its list. */
147
+ export type PickerGroupBy = 'zone' | 'location' | 'none';
148
+ /** The modes, in the order the control draws them. */
149
+ export declare const PICKER_GROUP_BY_MODES: readonly PickerGroupBy[];
150
+ /** What an operator who never chose gets. */
151
+ export declare const PICKER_DEFAULT_GROUP_BY: PickerGroupBy;
152
+ /** A stored choice; anything unreadable is the default. */
153
+ export declare function parsePickerGroupBy(raw: unknown): PickerGroupBy;
154
+ /** One site zone as the picker groups by it. */
155
+ export interface PickerZone {
156
+ readonly zoneKey: string;
157
+ readonly label: string;
158
+ readonly locationKeys: readonly string[];
159
+ }
160
+ /** The zone read, with whether it ANSWERED (D315). */
161
+ export type PickerZonesRead = {
162
+ readonly state: 'answered';
163
+ readonly zones: readonly PickerZone[];
164
+ } | {
165
+ readonly state: 'pending';
166
+ } | {
167
+ readonly state: 'failed';
168
+ };
169
+ /** Why Zone mode is grouping by location instead. */
170
+ export type PickerZonesFallback = 'pending' | 'failed';
171
+ /** The folded key a location label joins a zone by — `siteZoneKey`'s fold. */
172
+ export declare function pickerLocationKey(label: string): string;
173
+ /** The picker's groups under one "Group by" mode. */
174
+ export interface PickerGrouping<Id extends PickerId = number> {
175
+ /** The mode the groups ARE — `location` when Zone could not be honoured. */
176
+ readonly groupBy: PickerGroupBy;
177
+ /** Set when Zone mode fell back to location: the zone read had not answered. */
178
+ readonly fallback: PickerZonesFallback | null;
179
+ /** One headerless list (None): no group to expand, select or clear. */
180
+ readonly flat: boolean;
181
+ readonly groups: readonly PickerGroup<Id>[];
182
+ }
183
+ /**
184
+ * Group the list by zone, location or nothing.
185
+ *
186
+ * Zone: a group per site zone that holds a listed device (by label), then
187
+ * "No zone" (a location no zone holds), then "No location". Location: a group
188
+ * per location (folded; named after the first device's trimmed spelling), then "No
189
+ * location". None: one flat list by name, then id.
190
+ *
191
+ * A zone read that is pending or failed can NOT say a location is in no zone
192
+ * (D315), so Zone mode then groups by location and returns the reason as
193
+ * {@link PickerGrouping.fallback} for the UI's one-line note.
194
+ */
195
+ export declare function groupPickerDevicesBy<Id extends PickerId>(devices: readonly PickerItem<Id>[], selected: readonly Id[], groupBy: PickerGroupBy, zones: PickerZonesRead): PickerGrouping<Id>;
196
+ /** The grouping as rows: a flat grouping draws no header and is always open. */
197
+ export declare function flattenPickerGrouping<Id extends PickerId>(grouping: PickerGrouping<Id>, selected: readonly Id[], toggles: ReadonlyMap<string, boolean>, searching: boolean): readonly PickerRow<Id>[];
198
+ /**
199
+ * What a row's second line adds under "Group by": the device TYPE (it left the
200
+ * group key), and the room wherever the header does not already say it.
201
+ */
202
+ export interface PickerRowFacts {
203
+ readonly location: string | null;
204
+ readonly type: string | null;
205
+ }
206
+ export declare function pickerRowFacts<Id extends PickerId>(device: PickerItem<Id>, groupBy: PickerGroupBy): PickerRowFacts;
207
+ /** The filter key of an item with no type / no location / no zone. */
208
+ export declare const PICKER_NONE_KEY = "(none)";
209
+ export type PickerFacet = 'type' | 'zone' | 'location';
210
+ /** One choosable value of a facet, with how many listed items carry it. */
211
+ export interface PickerFacetOption {
212
+ readonly key: string;
213
+ /** The type, the location (first trimmed spelling seen), the zone label; null for "(none)". */
214
+ readonly label: string | null;
215
+ readonly count: number;
216
+ }
217
+ /** What a picker (or the alarm table) narrows by, applied in this order. */
218
+ export interface PickerFilters {
219
+ readonly query: string;
220
+ readonly types: readonly string[];
221
+ readonly zones: readonly string[];
222
+ readonly locations: readonly string[];
223
+ }
224
+ export declare const PICKER_NO_FILTERS: PickerFilters;
225
+ /** The key an item filters by under one facet; `(none)` when it has no such value. */
226
+ export declare function pickerFacetKey<Id extends PickerId>(item: PickerItem<Id>, facet: PickerFacet, zones: PickerZonesRead): string;
227
+ /** Every value of a facet the list holds (plus chosen ones it no longer holds, at 0). */
228
+ export declare function pickerFacetOptions<Id extends PickerId>(items: readonly PickerItem<Id>[], facet: PickerFacet, chosen: readonly string[], zones: PickerZonesRead): readonly PickerFacetOption[];
229
+ /** A zone filter is chosen but the zones read has not answered: it is paused, not applied. */
230
+ export declare function pickerZoneFilterSuspended(filters: PickerFilters, zones: PickerZonesRead): boolean;
231
+ /** Search, then Type, Zone (only from an answered read), Location. No filter: the list itself. */
232
+ export declare function applyPickerFilters<Id extends PickerId>(items: readonly PickerItem<Id>[], filters: PickerFilters, zones: PickerZonesRead): readonly PickerItem<Id>[];
233
+ /** Tick or untick one filter key. */
234
+ export declare function togglePickerKey(chosen: readonly string[], key: string): string[];
235
+ export declare function pickerFiltersActive(filters: PickerFilters): boolean;
236
+ /** Reset clears search, Type, Zone and Location — never "Group by". */
237
+ export declare function resetPickerFilters(): PickerFilters;
238
+ /**
239
+ * Selected ids the current filters hide — they stay selected, they are only not listed. Only a
240
+ * LISTED device can be hidden: an id the list does not hold at all (removed, unknown, off-offer)
241
+ * is not "hidden", so it is not counted.
242
+ */
243
+ export declare function pickerHiddenSelectedCount<Id extends PickerId>(selected: readonly Id[], list: readonly PickerItem<Id>[], matches: readonly PickerItem<Id>[]): number;
244
+ /** A dropdown is drawn when it can narrow (two values or more) or a key is chosen. */
245
+ export declare function pickerFacetFilterShown(options: readonly PickerFacetOption[], chosen: readonly string[]): boolean;
@@ -0,0 +1,39 @@
1
+ import { SelectedDeviceRow, SelectionId } from './device-selection';
2
+ /**
3
+ * What a picker lists, for its sentences. `class` is a detection class (D641);
4
+ * `zone` and `face` are a rule's zone and known-face scopes (D690).
5
+ */
6
+ export type PickerNoun = 'camera' | 'device' | 'node' | 'package' | 'class' | 'zone' | 'face';
7
+ /** Each noun's plural. */
8
+ export declare const PICKER_NOUN_PLURAL: {
9
+ readonly camera: "cameras";
10
+ readonly device: "devices";
11
+ readonly node: "nodes";
12
+ readonly package: "packages";
13
+ readonly class: "classes";
14
+ readonly zone: "zones";
15
+ readonly face: "faces";
16
+ };
17
+ /** What a picker lists, plural — the only words the loading / failed lines say. */
18
+ export type PickerNounPlural = (typeof PICKER_NOUN_PLURAL)[PickerNoun];
19
+ /** The label a picker gives a selected id its drawn list does not contain. */
20
+ export declare function orphanDeviceLabel<Id extends SelectionId>(row: SelectedDeviceRow<Id>, noun?: PickerNoun): string;
21
+ interface DevicesWidenWarningProps {
22
+ /** What the picker lists: the rule's Devices tab lists every device. */
23
+ readonly noun?: PickerNoun;
24
+ }
25
+ /** Said under a picker whose empty selection means EVERY device. */
26
+ export declare function DevicesWidenWarning({ noun }: DevicesWidenWarningProps): import("react").JSX.Element;
27
+ interface DevicesLoadingLineProps {
28
+ /** What the picker lists, plural — the rule's Devices tab lists every device. */
29
+ readonly noun?: PickerNounPlural;
30
+ }
31
+ /** A list that has not answered: never an absence claim (D315). */
32
+ export declare function DevicesLoadingLine({ noun }: DevicesLoadingLineProps): import("react").JSX.Element;
33
+ interface DevicesFailedLineProps {
34
+ /** What the picker lists, plural. */
35
+ readonly noun?: PickerNounPlural;
36
+ }
37
+ /** A list that failed: said, while the selected ids stay removable. */
38
+ export declare function DevicesFailedLine({ noun }: DevicesFailedLineProps): import("react").JSX.Element;
39
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * What a device picker says about the ids it has SELECTED — including the ones
3
+ * the hub no longer has.
4
+ *
5
+ * The picker draws the live camera list; a selected id outside that list would
6
+ * otherwise be invisible and impossible to deselect (operator report
7
+ * 2026-09-25: a deleted camera kept a rule matching nothing while the editor
8
+ * showed "all devices"). Two rules shape this module:
9
+ *
10
+ * - D315: absence is claimed only from a list that ANSWERED. Pending and
11
+ * failed lists never mark an id `removed`.
12
+ * - D49: a read never destroys. Every selected id comes back as a row; the
13
+ * selection is changed only by the operator.
14
+ *
15
+ * Generic over {@link SelectionId} since D637: the system family's node and
16
+ * package pickers select string ids under the same two rules. It declares its
17
+ * own id type rather than importing the picker model's, so the byte-identical
18
+ * copies share no import specifier that would differ between them.
19
+ *
20
+ * TWO BYTE-IDENTICAL COPIES: `camstack/src/components/notifications/
21
+ * device-selection.ts` (viewer) and `packages/addon-admin-ui/src/pages/system/
22
+ * device-selection.ts` (admin). The viewer resolves `@camstack/types` from a
23
+ * published release, so a helper added to the workspace would not exist
24
+ * there. `scripts/check-device-selection-in-sync.ts` (camstack-server) fails
25
+ * the commit when the copies differ; edit both.
26
+ */
27
+ /** A selected id: a device id, or a node id / package name (D637). */
28
+ export type SelectionId = number | string;
29
+ /** Where the camera list is: answered (data present), pending, or failed. */
30
+ export type DeviceListState = 'answered' | 'pending' | 'failed';
31
+ /** The minimum a camera needs to name a selected row. */
32
+ export interface NamedDevice<Id extends SelectionId = number> {
33
+ readonly id: Id;
34
+ readonly name: string;
35
+ }
36
+ /** One selected id. `name` only when the answered list has it. */
37
+ export interface SelectedDeviceRow<Id extends SelectionId = number> {
38
+ readonly id: Id;
39
+ readonly name?: string;
40
+ readonly removed: boolean;
41
+ }
42
+ /** The slice of a query result the state is derived from. */
43
+ export interface DeviceListQueryLike<Id extends SelectionId = number> {
44
+ readonly data: readonly NamedDevice<Id>[] | undefined;
45
+ readonly isError: boolean;
46
+ }
47
+ export declare function deviceListStateOf<Id extends SelectionId = number>(query: DeviceListQueryLike<Id>): DeviceListState;
48
+ export declare function resolveSelectedDevices<Id extends SelectionId = number>(selectedIds: readonly Id[], cameras: readonly NamedDevice<Id>[] | undefined, state: DeviceListState): SelectedDeviceRow<Id>[];
49
+ /**
50
+ * The selected ids a picker's DRAWN list does not contain, judged against the
51
+ * WHOLE fleet — never against the drawn list itself. A camera picker draws
52
+ * cameras, but a rule's devices can be a sensor picked on another surface: the
53
+ * camera list lacking it says nothing about whether it still exists. Each row
54
+ * is `removed` (the answered fleet lacks it), named (the fleet has it) or
55
+ * unnamed (the fleet has not answered).
56
+ */
57
+ export declare function offListSelections<Id extends SelectionId = number>(selectedIds: readonly Id[], listed: readonly NamedDevice<Id>[] | undefined, fleet: readonly NamedDevice<Id>[] | undefined, fleetState: DeviceListState): SelectedDeviceRow<Id>[];
58
+ /** The "no cameras" hint: only an ANSWERED empty list with nothing selected. */
59
+ export declare function showsNoCamerasHint(state: DeviceListState, cameraCount: number, selectedCount: number): boolean;
60
+ /**
61
+ * The widening warning: on a picker where an empty selection means ALL
62
+ * devices, an empty selection that once held devices must say so.
63
+ */
64
+ export declare function emptyDevicesWidens(emptyMeansAll: boolean, selectedCount: number, hadDevices: boolean): boolean;
@@ -0,0 +1,8 @@
1
+ export { DevicePicker, type DevicePickerProps } from './DevicePicker.js';
2
+ export { DeviceFilterBar, typeLabel, type DeviceFilterBarGrouping, type DeviceFilterBarProps, } from './device-filter-bar.js';
3
+ export type { PickerFleetView } from './device-fleet-view.js';
4
+ export { DEVICE_PICKER_GROUP_STORAGE_KEY, readStoredGroupBy, writeStoredGroupBy, useStoredGroupBy, usePickerSiteZones, type StoredGroupBy, } from './device-picker-grouping.js';
5
+ export * from './device-picker-model.js';
6
+ export * from './device-selection.js';
7
+ export { DevicesFailedLine, DevicesLoadingLine, DevicesWidenWarning, orphanDeviceLabel, PICKER_NOUN_PLURAL, type PickerNoun, type PickerNounPlural, } from './device-selection-rows.js';
8
+ export { INPUT_SHELL, SelectedChip, type SelectedChipProps } from './SelectedChip.js';
@@ -170,3 +170,6 @@ export * from './version-badge';
170
170
  export * from './widget-panel.js';
171
171
  export type { WidgetSlotProps } from './widget-slot';
172
172
  export { WidgetSlot } from './widget-slot';
173
+ export * from './device-picker/index.js';
174
+ export * from './unit-picker/index.js';
175
+ export * from './placeholder-field/index.js';
@@ -0,0 +1,29 @@
1
+ export interface PlaceholderChip {
2
+ readonly id: string;
3
+ readonly label: string;
4
+ /** Exact text inserted at the cursor. */
5
+ readonly token: string;
6
+ }
7
+ export interface PlaceholderChipFieldProps {
8
+ readonly label: string;
9
+ readonly value: string;
10
+ readonly onChange: (next: string) => void;
11
+ /**
12
+ * `undefined` = catalog has not answered: no chips, no warning (D315).
13
+ * `[]` = answered empty.
14
+ */
15
+ readonly chips: readonly PlaceholderChip[] | undefined;
16
+ readonly multiline?: boolean;
17
+ readonly placeholder?: string;
18
+ readonly disabled?: boolean;
19
+ readonly testId: string;
20
+ readonly maxLength?: number;
21
+ }
22
+ export declare function insertAtSelection(value: string, selection: {
23
+ readonly start: number;
24
+ readonly end: number;
25
+ }, token: string): {
26
+ readonly value: string;
27
+ readonly cursor: number;
28
+ };
29
+ export declare function PlaceholderChipField({ label, value, onChange, chips, multiline, placeholder, disabled, testId, maxLength, }: PlaceholderChipFieldProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export { PlaceholderChipField, insertAtSelection, type PlaceholderChip, type PlaceholderChipFieldProps, } from './PlaceholderChipField.js';
@@ -0,0 +1,16 @@
1
+ import { UnitDimension } from '@camstack/types';
2
+ import { ReactNode } from 'react';
3
+ export interface UnitPickerProps {
4
+ readonly dimension: UnitDimension | undefined;
5
+ readonly value: string;
6
+ readonly onChange: (unit: string) => void;
7
+ readonly disabled?: boolean;
8
+ /** Shown when `dimension` is undefined; never offered as a pickable unit. */
9
+ readonly emptyReason?: string;
10
+ /** Optional empty-value option label (inherit / source). */
11
+ readonly inheritLabel?: string;
12
+ readonly className?: string;
13
+ readonly id?: string;
14
+ readonly 'aria-label'?: string;
15
+ }
16
+ export declare function UnitPicker({ dimension, value, onChange, disabled, emptyReason, inheritLabel, className, id, 'aria-label': ariaLabel, }: UnitPickerProps): ReactNode;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export { UnitPicker, type UnitPickerProps } from './UnitPicker.js';
@@ -1151,6 +1151,12 @@ export declare const useOsdManagerCopyDeviceConfiguration: typeof trpc.osdManage
1151
1151
  export declare const useOsdManagerPreviewSlot: typeof trpc.osdManager.previewSlot.useMutation;
1152
1152
  /** Generated alias around `trpc.osdManager.renderDevice.useMutation`. */
1153
1153
  export declare const useOsdManagerRenderDevice: typeof trpc.osdManager.renderDevice.useMutation;
1154
+ /** Generated alias around `trpc.osdManager.listTemplates.useQuery`. */
1155
+ export declare const useOsdManagerListTemplates: typeof trpc.osdManager.listTemplates.useQuery;
1156
+ /** Generated alias around `trpc.osdManager.upsertTemplate.useMutation`. */
1157
+ export declare const useOsdManagerUpsertTemplate: typeof trpc.osdManager.upsertTemplate.useMutation;
1158
+ /** Generated alias around `trpc.osdManager.deleteTemplate.useMutation`. */
1159
+ export declare const useOsdManagerDeleteTemplate: typeof trpc.osdManager.deleteTemplate.useMutation;
1154
1160
  /** Generated alias around `trpc.petFeeder.feed.useMutation`. */
1155
1161
  export declare const usePetFeederFeed: typeof trpc.petFeeder.feed.useMutation;
1156
1162
  /** Generated alias around `trpc.petFeeder.cancelFeed.useMutation`. */