@camstack/ui-library 1.2.45 → 1.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/device-selector-picker.d.ts +37 -0
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/scope-picker.d.ts +7 -2
- package/dist/index.cjs +466 -79
- package/dist/index.js +464 -81
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -10362,6 +10362,29 @@ var Lightbulb = createLucideIcon("lightbulb", [
|
|
|
10362
10362
|
* This source code is licensed under the ISC license.
|
|
10363
10363
|
* See the LICENSE file in the root directory of this source tree.
|
|
10364
10364
|
*/
|
|
10365
|
+
var Link2 = createLucideIcon("link-2", [
|
|
10366
|
+
["path", {
|
|
10367
|
+
d: "M9 17H7A5 5 0 0 1 7 7h2",
|
|
10368
|
+
key: "8i5ue5"
|
|
10369
|
+
}],
|
|
10370
|
+
["path", {
|
|
10371
|
+
d: "M15 7h2a5 5 0 1 1 0 10h-2",
|
|
10372
|
+
key: "1b9ql8"
|
|
10373
|
+
}],
|
|
10374
|
+
["line", {
|
|
10375
|
+
x1: "8",
|
|
10376
|
+
x2: "16",
|
|
10377
|
+
y1: "12",
|
|
10378
|
+
y2: "12",
|
|
10379
|
+
key: "1jonct"
|
|
10380
|
+
}]
|
|
10381
|
+
]);
|
|
10382
|
+
/**
|
|
10383
|
+
* @license lucide-react v0.576.0 - ISC
|
|
10384
|
+
*
|
|
10385
|
+
* This source code is licensed under the ISC license.
|
|
10386
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
10387
|
+
*/
|
|
10365
10388
|
var ListChecks = createLucideIcon("list-checks", [
|
|
10366
10389
|
["path", {
|
|
10367
10390
|
d: "M13 5h8",
|
|
@@ -46880,6 +46903,345 @@ function KebabMenu({ items, header, triggerClassName, title = "More actions" })
|
|
|
46880
46903
|
});
|
|
46881
46904
|
}
|
|
46882
46905
|
//#endregion
|
|
46906
|
+
//#region src/composites/device-selector-picker.tsx
|
|
46907
|
+
/**
|
|
46908
|
+
* <DeviceSelectorPicker> — the operator-facing editor for a v3
|
|
46909
|
+
* {@link DeviceSelector}: the "on WHICH devices" half of a scope grant.
|
|
46910
|
+
*
|
|
46911
|
+
* Four tabs, one per selector kind, because the kinds are not interchangeable
|
|
46912
|
+
* and the operator has to choose between them deliberately:
|
|
46913
|
+
*
|
|
46914
|
+
* All → every device, now and forever. DYNAMIC.
|
|
46915
|
+
* By type → every device of a `DeviceType`. DYNAMIC — a camera added
|
|
46916
|
+
* tomorrow is covered with no re-grant.
|
|
46917
|
+
* By room → every device carrying an operator-assigned `location` label.
|
|
46918
|
+
* DYNAMIC. Rendered ONLY when the fleet actually has labels;
|
|
46919
|
+
* an empty tab reads as a broken feature, not as "unused".
|
|
46920
|
+
* Chosen → an explicit id list. STATIC — this is the one that does NOT
|
|
46921
|
+
* cover a new device, and the tab says so.
|
|
46922
|
+
*
|
|
46923
|
+
* Counts are LIVE (computed off `deviceManager.listAll`) so "Camera · 12"
|
|
46924
|
+
* answers "how much am I granting" without leaving the panel.
|
|
46925
|
+
*
|
|
46926
|
+
* Shared, not forked: the raw `<ScopePicker>` device row and the simple
|
|
46927
|
+
* Access panel both render this. Two editors for one wire shape is how a
|
|
46928
|
+
* selector ends up meaning different things in two places.
|
|
46929
|
+
*/
|
|
46930
|
+
/** Human label for a `DeviceType` — 'lawn-mower' → 'Lawn mower'. */
|
|
46931
|
+
function deviceTypeLabel(type) {
|
|
46932
|
+
const spaced = type.replaceAll("-", " ");
|
|
46933
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
46934
|
+
}
|
|
46935
|
+
/** Which tab a selector belongs to. */
|
|
46936
|
+
function tabForSelector(selector) {
|
|
46937
|
+
return selector.kind;
|
|
46938
|
+
}
|
|
46939
|
+
/**
|
|
46940
|
+
* A fresh selector when the operator switches tabs. Switching is destructive
|
|
46941
|
+
* by nature — an `ids` list has no meaning as a `types` list — so each tab
|
|
46942
|
+
* starts empty rather than guessing a translation.
|
|
46943
|
+
*/
|
|
46944
|
+
function emptySelectorFor(tab) {
|
|
46945
|
+
switch (tab) {
|
|
46946
|
+
case "all": return { kind: "all" };
|
|
46947
|
+
case "types": return {
|
|
46948
|
+
kind: "types",
|
|
46949
|
+
types: []
|
|
46950
|
+
};
|
|
46951
|
+
case "locations": return {
|
|
46952
|
+
kind: "locations",
|
|
46953
|
+
locations: []
|
|
46954
|
+
};
|
|
46955
|
+
case "ids": return {
|
|
46956
|
+
kind: "ids",
|
|
46957
|
+
ids: []
|
|
46958
|
+
};
|
|
46959
|
+
}
|
|
46960
|
+
}
|
|
46961
|
+
function DeviceSelectorPicker({ value, onChange, includeLinked, onIncludeLinkedChange, clampToDeviceIds, dense = false }) {
|
|
46962
|
+
const { data: rawDevices } = useDeviceManagerListAll({});
|
|
46963
|
+
const [search, setSearch] = (0, react$1.useState)("");
|
|
46964
|
+
const devices = (0, react$1.useMemo)(() => {
|
|
46965
|
+
const all = (rawDevices ?? []).map((d) => ({
|
|
46966
|
+
id: d.id,
|
|
46967
|
+
name: d.name,
|
|
46968
|
+
type: d.type,
|
|
46969
|
+
location: d.location,
|
|
46970
|
+
parentDeviceId: d.parentDeviceId
|
|
46971
|
+
}));
|
|
46972
|
+
if (clampToDeviceIds == null) return all;
|
|
46973
|
+
const allowed = new Set(clampToDeviceIds);
|
|
46974
|
+
return all.filter((d) => allowed.has(d.id));
|
|
46975
|
+
}, [rawDevices, clampToDeviceIds]);
|
|
46976
|
+
const typeOptions = (0, react$1.useMemo)(() => {
|
|
46977
|
+
const counts = /* @__PURE__ */ new Map();
|
|
46978
|
+
for (const d of devices) counts.set(d.type, (counts.get(d.type) ?? 0) + 1);
|
|
46979
|
+
if (value.kind === "types") {
|
|
46980
|
+
for (const t of value.types) if (!counts.has(t)) counts.set(t, 0);
|
|
46981
|
+
}
|
|
46982
|
+
return [...counts.entries()].map(([type, count]) => ({
|
|
46983
|
+
value: type,
|
|
46984
|
+
label: deviceTypeLabel(type),
|
|
46985
|
+
count
|
|
46986
|
+
})).sort((a, b) => a.label.localeCompare(b.label));
|
|
46987
|
+
}, [devices, value]);
|
|
46988
|
+
const locationOptions = (0, react$1.useMemo)(() => {
|
|
46989
|
+
const counts = /* @__PURE__ */ new Map();
|
|
46990
|
+
for (const d of devices) {
|
|
46991
|
+
if (d.location === null || d.location.length === 0) continue;
|
|
46992
|
+
counts.set(d.location, (counts.get(d.location) ?? 0) + 1);
|
|
46993
|
+
}
|
|
46994
|
+
if (value.kind === "locations") {
|
|
46995
|
+
for (const l of value.locations) if (!counts.has(l)) counts.set(l, 0);
|
|
46996
|
+
}
|
|
46997
|
+
return [...counts.entries()].map(([location, count]) => ({
|
|
46998
|
+
value: location,
|
|
46999
|
+
label: location,
|
|
47000
|
+
count
|
|
47001
|
+
})).sort((a, b) => a.label.localeCompare(b.label));
|
|
47002
|
+
}, [devices, value]);
|
|
47003
|
+
/** The Room tab exists only when the fleet has rooms. */
|
|
47004
|
+
const hasLocations = locationOptions.length > 0;
|
|
47005
|
+
const visibleDevices = (0, react$1.useMemo)(() => {
|
|
47006
|
+
const q = search.trim().toLowerCase();
|
|
47007
|
+
return [...q.length === 0 ? devices : devices.filter((d) => d.name.toLowerCase().includes(q) || String(d.id).includes(q) || (d.location ?? "").toLowerCase().includes(q))].sort((a, b) => a.name.localeCompare(b.name));
|
|
47008
|
+
}, [devices, search]);
|
|
47009
|
+
const activeTab = tabForSelector(value);
|
|
47010
|
+
const tabs = [
|
|
47011
|
+
{
|
|
47012
|
+
id: "all",
|
|
47013
|
+
label: "All"
|
|
47014
|
+
},
|
|
47015
|
+
{
|
|
47016
|
+
id: "types",
|
|
47017
|
+
label: "By type"
|
|
47018
|
+
},
|
|
47019
|
+
...hasLocations || activeTab === "locations" ? [{
|
|
47020
|
+
id: "locations",
|
|
47021
|
+
label: "By room"
|
|
47022
|
+
}] : [],
|
|
47023
|
+
{
|
|
47024
|
+
id: "ids",
|
|
47025
|
+
label: "Chosen"
|
|
47026
|
+
}
|
|
47027
|
+
];
|
|
47028
|
+
const toggleType = (type) => {
|
|
47029
|
+
const current = value.kind === "types" ? value.types : [];
|
|
47030
|
+
onChange({
|
|
47031
|
+
kind: "types",
|
|
47032
|
+
types: current.some((t) => String(t) === type) ? current.filter((t) => String(t) !== type) : [...current, ...typeFromString(type)]
|
|
47033
|
+
});
|
|
47034
|
+
};
|
|
47035
|
+
const toggleLocation = (location) => {
|
|
47036
|
+
const current = value.kind === "locations" ? value.locations : [];
|
|
47037
|
+
onChange({
|
|
47038
|
+
kind: "locations",
|
|
47039
|
+
locations: current.includes(location) ? current.filter((l) => l !== location) : [...current, location]
|
|
47040
|
+
});
|
|
47041
|
+
};
|
|
47042
|
+
const toggleId = (id) => {
|
|
47043
|
+
const current = value.kind === "ids" ? value.ids : [];
|
|
47044
|
+
onChange({
|
|
47045
|
+
kind: "ids",
|
|
47046
|
+
ids: current.includes(id) ? current.filter((v) => v !== id) : [...current, id]
|
|
47047
|
+
});
|
|
47048
|
+
};
|
|
47049
|
+
const pad = dense ? "p-2" : "p-3";
|
|
47050
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47051
|
+
className: "space-y-2",
|
|
47052
|
+
children: [
|
|
47053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
47054
|
+
className: "flex flex-wrap gap-1 border-b border-border pb-1",
|
|
47055
|
+
children: tabs.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
47056
|
+
type: "button",
|
|
47057
|
+
onClick: () => onChange(emptySelectorFor(t.id)),
|
|
47058
|
+
className: `shrink-0 whitespace-nowrap rounded px-2 py-1 text-[11px] transition-colors ${activeTab === t.id ? "bg-primary/15 text-primary font-medium" : "text-foreground-subtle hover:bg-foreground-subtle/10 hover:text-foreground"}`,
|
|
47059
|
+
children: t.label
|
|
47060
|
+
}, t.id))
|
|
47061
|
+
}),
|
|
47062
|
+
activeTab === "all" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47063
|
+
className: `rounded border border-border bg-surface ${pad}`,
|
|
47064
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
47065
|
+
className: "text-[11px] text-foreground",
|
|
47066
|
+
children: [
|
|
47067
|
+
"Every device in this deployment — ",
|
|
47068
|
+
devices.length,
|
|
47069
|
+
" right now."
|
|
47070
|
+
]
|
|
47071
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
47072
|
+
className: "mt-1 text-[10px] text-foreground-subtle",
|
|
47073
|
+
children: "Devices added later are covered automatically."
|
|
47074
|
+
})]
|
|
47075
|
+
}),
|
|
47076
|
+
activeTab === "types" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckboxGrid, {
|
|
47077
|
+
options: typeOptions,
|
|
47078
|
+
selected: value.kind === "types" ? value.types.map((t) => String(t)) : [],
|
|
47079
|
+
onToggle: toggleType,
|
|
47080
|
+
emptyLabel: "No devices to pick a type from.",
|
|
47081
|
+
footnote: "Devices of a checked type are covered automatically as they are added.",
|
|
47082
|
+
pad
|
|
47083
|
+
}),
|
|
47084
|
+
activeTab === "locations" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckboxGrid, {
|
|
47085
|
+
options: locationOptions,
|
|
47086
|
+
selected: value.kind === "locations" ? [...value.locations] : [],
|
|
47087
|
+
onToggle: toggleLocation,
|
|
47088
|
+
emptyLabel: "No device has a room assigned yet.",
|
|
47089
|
+
footnote: "A device with no room set is never covered by a room rule.",
|
|
47090
|
+
pad
|
|
47091
|
+
}),
|
|
47092
|
+
activeTab === "ids" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47093
|
+
className: `rounded border border-border bg-surface ${pad} space-y-2`,
|
|
47094
|
+
children: [
|
|
47095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47096
|
+
className: "relative",
|
|
47097
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-foreground-subtle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
47098
|
+
type: "search",
|
|
47099
|
+
value: search,
|
|
47100
|
+
onChange: (e) => setSearch(e.target.value),
|
|
47101
|
+
placeholder: "Search devices…",
|
|
47102
|
+
className: "w-full rounded border border-border bg-background py-1 pl-7 pr-2 text-[11px] text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
|
|
47103
|
+
})]
|
|
47104
|
+
}),
|
|
47105
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47106
|
+
className: "max-h-56 overflow-auto rounded border border-border",
|
|
47107
|
+
children: [visibleDevices.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
47108
|
+
className: "px-2 py-3 text-[10px] italic text-foreground-subtle",
|
|
47109
|
+
children: "No device matches."
|
|
47110
|
+
}), visibleDevices.map((d) => {
|
|
47111
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
47112
|
+
className: "flex cursor-pointer items-center gap-2 px-2 py-1.5 text-[11px] hover:bg-foreground-subtle/10",
|
|
47113
|
+
children: [
|
|
47114
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
47115
|
+
type: "checkbox",
|
|
47116
|
+
checked: value.kind === "ids" && value.ids.includes(d.id),
|
|
47117
|
+
onChange: () => toggleId(d.id),
|
|
47118
|
+
className: "rounded border-border focus:ring-primary"
|
|
47119
|
+
}),
|
|
47120
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47121
|
+
className: "min-w-0 flex-1 truncate text-foreground",
|
|
47122
|
+
children: d.name
|
|
47123
|
+
}),
|
|
47124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
47125
|
+
className: "shrink-0 font-mono text-[10px] text-foreground-subtle",
|
|
47126
|
+
children: [
|
|
47127
|
+
"#",
|
|
47128
|
+
d.id,
|
|
47129
|
+
d.location !== null && d.location.length > 0 ? ` · ${d.location}` : ""
|
|
47130
|
+
]
|
|
47131
|
+
})
|
|
47132
|
+
]
|
|
47133
|
+
}, d.id);
|
|
47134
|
+
})]
|
|
47135
|
+
}),
|
|
47136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
47137
|
+
className: "text-[10px] text-foreground-subtle",
|
|
47138
|
+
children: [
|
|
47139
|
+
"A fixed list — a device added later is ",
|
|
47140
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47141
|
+
className: "font-medium",
|
|
47142
|
+
children: "not"
|
|
47143
|
+
}),
|
|
47144
|
+
" covered until you edit this."
|
|
47145
|
+
]
|
|
47146
|
+
})
|
|
47147
|
+
]
|
|
47148
|
+
}),
|
|
47149
|
+
includeLinked !== void 0 && onIncludeLinkedChange !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IncludeLinkedToggle, {
|
|
47150
|
+
value: includeLinked,
|
|
47151
|
+
onChange: onIncludeLinkedChange,
|
|
47152
|
+
pad
|
|
47153
|
+
})
|
|
47154
|
+
]
|
|
47155
|
+
});
|
|
47156
|
+
}
|
|
47157
|
+
/**
|
|
47158
|
+
* Turn a raw string back into the `DeviceType` union without a cast. The
|
|
47159
|
+
* enum's values ARE the strings, so a lookup over its members is both exact
|
|
47160
|
+
* and total — an unknown string yields no entry and selects nothing.
|
|
47161
|
+
*/
|
|
47162
|
+
function typeFromString(raw) {
|
|
47163
|
+
return Object.values(_camstack_types.DeviceType).filter((t) => String(t) === raw);
|
|
47164
|
+
}
|
|
47165
|
+
function CheckboxGrid({ options, selected, onToggle, emptyLabel, footnote, pad }) {
|
|
47166
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47167
|
+
className: `rounded border border-border bg-surface ${pad} space-y-2`,
|
|
47168
|
+
children: [options.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
47169
|
+
className: "text-[10px] italic text-foreground-subtle",
|
|
47170
|
+
children: emptyLabel
|
|
47171
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
47172
|
+
className: "grid max-h-56 grid-cols-[repeat(auto-fill,minmax(11rem,1fr))] gap-1 overflow-auto",
|
|
47173
|
+
children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
47174
|
+
className: "flex cursor-pointer items-center gap-2 rounded px-1.5 py-1 text-[11px] hover:bg-foreground-subtle/10",
|
|
47175
|
+
children: [
|
|
47176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
47177
|
+
type: "checkbox",
|
|
47178
|
+
checked: selected.includes(o.value),
|
|
47179
|
+
onChange: () => onToggle(o.value),
|
|
47180
|
+
className: "rounded border-border focus:ring-primary"
|
|
47181
|
+
}),
|
|
47182
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47183
|
+
className: "min-w-0 flex-1 truncate text-foreground",
|
|
47184
|
+
children: o.label
|
|
47185
|
+
}),
|
|
47186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47187
|
+
className: "shrink-0 font-mono text-[10px] text-foreground-subtle",
|
|
47188
|
+
children: o.count
|
|
47189
|
+
})
|
|
47190
|
+
]
|
|
47191
|
+
}, o.value))
|
|
47192
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
47193
|
+
className: "text-[10px] text-foreground-subtle",
|
|
47194
|
+
children: footnote
|
|
47195
|
+
})]
|
|
47196
|
+
});
|
|
47197
|
+
}
|
|
47198
|
+
/**
|
|
47199
|
+
* The toggle whose OFF state is a real semantic restriction, so the hint says
|
|
47200
|
+
* what it costs rather than describing the mechanism: without it, an operator
|
|
47201
|
+
* granted a camera cannot fire the siren or floodlight attached to it,
|
|
47202
|
+
* because `create` does not inherit parent → child by default.
|
|
47203
|
+
*/
|
|
47204
|
+
function IncludeLinkedToggle({ value, onChange, pad }) {
|
|
47205
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
47206
|
+
className: `rounded border border-border bg-surface ${pad}`,
|
|
47207
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
47208
|
+
className: "flex cursor-pointer items-start gap-2",
|
|
47209
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
47210
|
+
type: "checkbox",
|
|
47211
|
+
checked: value,
|
|
47212
|
+
onChange: (e) => onChange(e.target.checked),
|
|
47213
|
+
className: "mt-0.5 rounded border-border focus:ring-primary"
|
|
47214
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
47215
|
+
className: "min-w-0",
|
|
47216
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
47217
|
+
className: "flex items-center gap-1.5 text-[11px] font-medium text-foreground",
|
|
47218
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Link2, { className: "h-3 w-3" }), "Include linked devices"]
|
|
47219
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47220
|
+
className: "mt-0.5 block text-[10px] text-foreground-subtle",
|
|
47221
|
+
children: value ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: "Accessories attached to a granted camera — siren, floodlight, PIR — are covered too." }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
47222
|
+
"Only the devices selected above. An operator granted a camera will",
|
|
47223
|
+
" ",
|
|
47224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47225
|
+
className: "font-medium text-foreground",
|
|
47226
|
+
children: "not"
|
|
47227
|
+
}),
|
|
47228
|
+
" be able to trigger the siren or light attached to it."
|
|
47229
|
+
] })
|
|
47230
|
+
})]
|
|
47231
|
+
})]
|
|
47232
|
+
})
|
|
47233
|
+
});
|
|
47234
|
+
}
|
|
47235
|
+
/** Summary line for a selector — used by the raw picker's collapsed row. */
|
|
47236
|
+
function describeDeviceSelector(selector) {
|
|
47237
|
+
switch (selector.kind) {
|
|
47238
|
+
case "all": return "All devices";
|
|
47239
|
+
case "ids": return selector.ids.length === 0 ? "No device chosen" : `${selector.ids.length} device${selector.ids.length === 1 ? "" : "s"}`;
|
|
47240
|
+
case "types": return selector.types.length === 0 ? "No type chosen" : selector.types.map((t) => deviceTypeLabel(String(t))).join(", ");
|
|
47241
|
+
case "locations": return selector.locations.length === 0 ? "No room chosen" : selector.locations.join(", ");
|
|
47242
|
+
}
|
|
47243
|
+
}
|
|
47244
|
+
//#endregion
|
|
46883
47245
|
//#region src/composites/scope-picker.tsx
|
|
46884
47246
|
/**
|
|
46885
47247
|
* <ScopePicker> — operator-facing editor for a `TokenScope[]` value.
|
|
@@ -46893,13 +47255,24 @@ function KebabMenu({ items, header, triggerClassName, title = "More actions" })
|
|
|
46893
47255
|
* - category → target: 'device' | 'system'
|
|
46894
47256
|
* - capability → target: cap name from `KNOWN_CAP_NAMES`
|
|
46895
47257
|
* - addon → target: addon id from `useAddonsList`
|
|
46896
|
-
* - device →
|
|
47258
|
+
* - device → a v3 `DeviceSelector` (all / ids / types / locations)
|
|
47259
|
+
* plus `includeLinked`, edited by `<DeviceSelectorPicker>`
|
|
46897
47260
|
*
|
|
46898
47261
|
* `access` is always 3 independent checkboxes (view / create / delete).
|
|
46899
47262
|
*
|
|
46900
47263
|
* `clampToParent` narrows the target dropdown + access checkboxes to
|
|
46901
47264
|
* what the CALLER possesses. Admins pass `null` to allow everything.
|
|
47265
|
+
*
|
|
47266
|
+
* This is the RAW editor. The simple, two-question Access panel on
|
|
47267
|
+
* `/system/users` composes the same wire shape through `ACCESS_ROLES`; this
|
|
47268
|
+
* one is what an operator drops into when a grant set is not a plain role.
|
|
46902
47269
|
*/
|
|
47270
|
+
/** `<select>` value → the tri-state `includeLinked` it means. */
|
|
47271
|
+
var INCLUDE_LINKED_VALUES = {
|
|
47272
|
+
"": void 0,
|
|
47273
|
+
yes: true,
|
|
47274
|
+
no: false
|
|
47275
|
+
};
|
|
46903
47276
|
var ACCESS_OPTIONS = [
|
|
46904
47277
|
{
|
|
46905
47278
|
value: "view",
|
|
@@ -46930,14 +47303,24 @@ function defaultRow() {
|
|
|
46930
47303
|
};
|
|
46931
47304
|
}
|
|
46932
47305
|
/**
|
|
46933
|
-
*
|
|
46934
|
-
* the device picker so a child grant can't refer to a
|
|
46935
|
-
* caller themselves doesn't have.
|
|
47306
|
+
* The deviceIds a parent's grants actually resolve to, against the live
|
|
47307
|
+
* fleet. Used to clamp the device picker so a child grant can't refer to a
|
|
47308
|
+
* device the caller themselves doesn't have.
|
|
47309
|
+
*
|
|
47310
|
+
* v3 selectors are not a literal id list, so this RESOLVES them through the
|
|
47311
|
+
* shared resolver rather than reading ids off the grant — a `types:['camera']`
|
|
47312
|
+
* parent must clamp to its cameras, and re-deriving that here is exactly the
|
|
47313
|
+
* drift D103 warns about. A parent holding `category:device` (unrestricted)
|
|
47314
|
+
* yields the whole fleet.
|
|
46936
47315
|
*/
|
|
46937
|
-
function
|
|
47316
|
+
function parentDeviceIds(parent, fleet) {
|
|
46938
47317
|
if (parent == null) return [];
|
|
46939
47318
|
const out = /* @__PURE__ */ new Set();
|
|
46940
|
-
for (const
|
|
47319
|
+
for (const access of [
|
|
47320
|
+
"view",
|
|
47321
|
+
"create",
|
|
47322
|
+
"delete"
|
|
47323
|
+
]) for (const id of (0, _camstack_types.resolveViewableDeviceIds)(parent, fleet, access)) out.add(id);
|
|
46941
47324
|
return [...out];
|
|
46942
47325
|
}
|
|
46943
47326
|
function clampedSingleTargets(type, parent, addonChoices, capChoices) {
|
|
@@ -46968,7 +47351,7 @@ function clampedAccessForRow(row, parent) {
|
|
|
46968
47351
|
];
|
|
46969
47352
|
const matches = parent.filter((s) => {
|
|
46970
47353
|
if (s.type !== row.type) return false;
|
|
46971
|
-
if (s.type === "device" && row.type === "device") return
|
|
47354
|
+
if (s.type === "device" && row.type === "device") return true;
|
|
46972
47355
|
if (s.type !== "device" && row.type !== "device") return s.target === row.target;
|
|
46973
47356
|
return false;
|
|
46974
47357
|
});
|
|
@@ -46988,11 +47371,13 @@ function ScopePicker({ value, onChange, clampToParent, emptyHint }) {
|
|
|
46988
47371
|
value: c,
|
|
46989
47372
|
label: c
|
|
46990
47373
|
})), []);
|
|
46991
|
-
const
|
|
46992
|
-
|
|
46993
|
-
|
|
46994
|
-
|
|
46995
|
-
|
|
47374
|
+
const fleet = (0, react$1.useMemo)(() => (devices ?? []).map((d) => ({
|
|
47375
|
+
id: d.id,
|
|
47376
|
+
type: d.type,
|
|
47377
|
+
location: d.location,
|
|
47378
|
+
parentDeviceId: d.parentDeviceId
|
|
47379
|
+
})), [devices]);
|
|
47380
|
+
const parentDeviceClamp = (0, react$1.useMemo)(() => clampToParent == null ? null : parentDeviceIds(clampToParent, fleet), [clampToParent, fleet]);
|
|
46996
47381
|
/**
|
|
46997
47382
|
* Build a fresh scope row when `type` changes. The discriminated union
|
|
46998
47383
|
* forces us to re-construct the object per variant so the resulting
|
|
@@ -47018,7 +47403,10 @@ function ScopePicker({ value, onChange, clampToParent, emptyHint }) {
|
|
|
47018
47403
|
};
|
|
47019
47404
|
case "device": return {
|
|
47020
47405
|
type: "device",
|
|
47021
|
-
|
|
47406
|
+
selector: {
|
|
47407
|
+
kind: "ids",
|
|
47408
|
+
ids: []
|
|
47409
|
+
},
|
|
47022
47410
|
access: accessArr
|
|
47023
47411
|
};
|
|
47024
47412
|
}
|
|
@@ -47028,11 +47416,18 @@ function ScopePicker({ value, onChange, clampToParent, emptyHint }) {
|
|
|
47028
47416
|
if (i !== idx) return s;
|
|
47029
47417
|
if (patch.type !== void 0 && patch.type !== s.type) return rebuildForType(patch.type, patch.access ?? s.access);
|
|
47030
47418
|
const access = [...patch.access ?? s.access];
|
|
47031
|
-
if (s.type === "device")
|
|
47032
|
-
|
|
47033
|
-
|
|
47034
|
-
|
|
47035
|
-
|
|
47419
|
+
if (s.type === "device") {
|
|
47420
|
+
const base = {
|
|
47421
|
+
type: "device",
|
|
47422
|
+
selector: patch.selector ?? s.selector,
|
|
47423
|
+
access
|
|
47424
|
+
};
|
|
47425
|
+
const linked = "includeLinked" in patch ? patch.includeLinked : s.includeLinked;
|
|
47426
|
+
return linked === void 0 ? base : {
|
|
47427
|
+
...base,
|
|
47428
|
+
includeLinked: linked
|
|
47429
|
+
};
|
|
47430
|
+
}
|
|
47036
47431
|
if (s.type === "category") return {
|
|
47037
47432
|
type: "category",
|
|
47038
47433
|
target: patch.target ?? s.target,
|
|
@@ -47091,11 +47486,35 @@ function ScopePicker({ value, onChange, clampToParent, emptyHint }) {
|
|
|
47091
47486
|
})
|
|
47092
47487
|
]
|
|
47093
47488
|
}),
|
|
47094
|
-
row.type === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
47095
|
-
|
|
47096
|
-
|
|
47097
|
-
|
|
47098
|
-
|
|
47489
|
+
row.type === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47490
|
+
className: "min-w-[200px] flex-1 space-y-1.5",
|
|
47491
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceSelectorPicker, {
|
|
47492
|
+
dense: true,
|
|
47493
|
+
value: row.selector,
|
|
47494
|
+
onChange: (selector) => update(idx, { selector }),
|
|
47495
|
+
clampToDeviceIds: parentDeviceClamp
|
|
47496
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
47497
|
+
className: "flex flex-wrap items-center gap-1.5 text-[10px] text-foreground-subtle",
|
|
47498
|
+
children: ["Linked devices", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
47499
|
+
value: row.includeLinked === void 0 ? "" : row.includeLinked ? "yes" : "no",
|
|
47500
|
+
onChange: (e) => update(idx, { includeLinked: INCLUDE_LINKED_VALUES[e.target.value] }),
|
|
47501
|
+
className: "rounded border border-border bg-background px-1.5 py-0.5 text-[10px] focus:outline-none focus:ring-1 focus:ring-primary",
|
|
47502
|
+
children: [
|
|
47503
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
47504
|
+
value: "",
|
|
47505
|
+
children: "Automatic (view inherits)"
|
|
47506
|
+
}),
|
|
47507
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
47508
|
+
value: "yes",
|
|
47509
|
+
children: "Always included"
|
|
47510
|
+
}),
|
|
47511
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
47512
|
+
value: "no",
|
|
47513
|
+
children: "Never included"
|
|
47514
|
+
})
|
|
47515
|
+
]
|
|
47516
|
+
})]
|
|
47517
|
+
})]
|
|
47099
47518
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
47100
47519
|
value: row.target,
|
|
47101
47520
|
onChange: (e) => update(idx, { target: e.target.value }),
|
|
@@ -47158,76 +47577,40 @@ function ScopePicker({ value, onChange, clampToParent, emptyHint }) {
|
|
|
47158
47577
|
]
|
|
47159
47578
|
});
|
|
47160
47579
|
}
|
|
47161
|
-
function DeviceTargetPicker({ value, choices, clampToParent, onChange }) {
|
|
47162
|
-
const [pendingAdd, setPendingAdd] = (0, react$1.useState)("");
|
|
47163
|
-
const visibleChoices = (0, react$1.useMemo)(() => {
|
|
47164
|
-
const filtered = clampToParent == null ? choices : choices.filter((c) => clampToParent.includes(c.value));
|
|
47165
|
-
const taken = new Set(value);
|
|
47166
|
-
return filtered.filter((c) => !taken.has(c.value));
|
|
47167
|
-
}, [
|
|
47168
|
-
choices,
|
|
47169
|
-
clampToParent,
|
|
47170
|
-
value
|
|
47171
|
-
]);
|
|
47172
|
-
const labelFor = (id) => choices.find((c) => c.value === id)?.label ?? `#${id}`;
|
|
47173
|
-
const handleAdd = (id) => {
|
|
47174
|
-
if (!id) return;
|
|
47175
|
-
onChange([...value, id]);
|
|
47176
|
-
setPendingAdd("");
|
|
47177
|
-
};
|
|
47178
|
-
const handleRemove = (id) => {
|
|
47179
|
-
onChange(value.filter((v) => v !== id));
|
|
47180
|
-
};
|
|
47181
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47182
|
-
className: "flex-1 min-w-[160px] flex flex-wrap items-center gap-1",
|
|
47183
|
-
children: [
|
|
47184
|
-
value.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47185
|
-
className: "text-[10px] text-foreground-subtle italic px-1",
|
|
47186
|
-
children: "no devices selected"
|
|
47187
|
-
}),
|
|
47188
|
-
value.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
47189
|
-
className: "inline-flex items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-mono text-primary",
|
|
47190
|
-
children: [labelFor(id), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
47191
|
-
type: "button",
|
|
47192
|
-
onClick: () => handleRemove(id),
|
|
47193
|
-
className: "hover:text-danger transition-colors",
|
|
47194
|
-
title: "Remove device",
|
|
47195
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
|
|
47196
|
-
})]
|
|
47197
|
-
}, id)),
|
|
47198
|
-
visibleChoices.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
47199
|
-
value: pendingAdd,
|
|
47200
|
-
onChange: (e) => handleAdd(e.target.value),
|
|
47201
|
-
className: "rounded border border-border bg-background px-1.5 py-0.5 text-[10px] focus:outline-none focus:ring-1 focus:ring-primary",
|
|
47202
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
47203
|
-
value: "",
|
|
47204
|
-
children: "+ add device…"
|
|
47205
|
-
}), visibleChoices.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
47206
|
-
value: c.value,
|
|
47207
|
-
children: c.label
|
|
47208
|
-
}, c.value))]
|
|
47209
|
-
})
|
|
47210
|
-
]
|
|
47211
|
-
});
|
|
47212
|
-
}
|
|
47213
47580
|
/**
|
|
47214
47581
|
* Validation helper. The wire schema (`TokenScopeSchema`) requires every
|
|
47215
|
-
* scope to carry a
|
|
47216
|
-
*
|
|
47582
|
+
* scope to carry a usable target and `access.length >= 1`; a v3 device
|
|
47583
|
+
* selector additionally requires a non-empty list for every kind except
|
|
47584
|
+
* `all`. Use this before passing the picker output to a mutate call so the
|
|
47217
47585
|
* user sees a clean inline error instead of a tRPC ZodError.
|
|
47586
|
+
*
|
|
47587
|
+
* An EMPTY selector list is rejected rather than silently treated as "all":
|
|
47588
|
+
* the schema's `.min(1)` would reject it server-side anyway, and the failure
|
|
47589
|
+
* mode of guessing wrong here is granting the whole fleet.
|
|
47218
47590
|
*/
|
|
47219
47591
|
function validateScopes(scopes) {
|
|
47220
47592
|
for (const s of scopes) {
|
|
47221
47593
|
if (s.type === "device") {
|
|
47222
|
-
|
|
47594
|
+
const problem = validateSelector(s.selector);
|
|
47595
|
+
if (problem !== null) return problem;
|
|
47223
47596
|
} else if (s.target.trim().length === 0) return `Scope ${s.type}: pick a target`;
|
|
47224
47597
|
if (s.access.length === 0) {
|
|
47225
|
-
const targetDesc = s.type === "device" ?
|
|
47598
|
+
const targetDesc = s.type === "device" ? describeDeviceSelector(s.selector) : s.target;
|
|
47226
47599
|
return `Scope ${s.type}:${targetDesc}: pick at least one access`;
|
|
47227
47600
|
}
|
|
47228
47601
|
}
|
|
47229
47602
|
return null;
|
|
47230
47603
|
}
|
|
47604
|
+
/** Non-empty check per selector kind. `all` selects the fleet and is always
|
|
47605
|
+
* complete. */
|
|
47606
|
+
function validateSelector(selector) {
|
|
47607
|
+
switch (selector.kind) {
|
|
47608
|
+
case "all": return null;
|
|
47609
|
+
case "ids": return selector.ids.length === 0 ? "Scope device: pick at least one device" : null;
|
|
47610
|
+
case "types": return selector.types.length === 0 ? "Scope device: pick at least one device type" : null;
|
|
47611
|
+
case "locations": return selector.locations.length === 0 ? "Scope device: pick at least one room" : null;
|
|
47612
|
+
}
|
|
47613
|
+
}
|
|
47231
47614
|
//#endregion
|
|
47232
47615
|
//#region src/contexts/zone-editing.tsx
|
|
47233
47616
|
/**
|
|
@@ -48153,6 +48536,7 @@ exports.DeviceItem = DeviceItem;
|
|
|
48153
48536
|
exports.DeviceList = DeviceList;
|
|
48154
48537
|
exports.DeviceMultiSelectField = DeviceMultiSelectField;
|
|
48155
48538
|
exports.DeviceSelectField = DeviceSelectField;
|
|
48539
|
+
exports.DeviceSelectorPicker = DeviceSelectorPicker;
|
|
48156
48540
|
exports.DeviceStepMatrix = DeviceStepMatrix;
|
|
48157
48541
|
exports.Dialog = Dialog;
|
|
48158
48542
|
exports.DialogContent = DialogContent;
|
|
@@ -48350,10 +48734,12 @@ exports.cursorFractionFor = cursorFractionFor;
|
|
|
48350
48734
|
exports.darkColors = require_theme_index.darkColors$1;
|
|
48351
48735
|
exports.defaultTheme = require_theme_index.defaultTheme$1;
|
|
48352
48736
|
exports.deriveDeviceKind = deriveDeviceKind;
|
|
48737
|
+
exports.describeDeviceSelector = describeDeviceSelector;
|
|
48353
48738
|
exports.deviceMatchesFilter = deviceMatchesFilter;
|
|
48354
48739
|
exports.deviceOptionLabel = deviceOptionLabel;
|
|
48355
48740
|
exports.deviceRoleMeta = deviceRoleMeta;
|
|
48356
48741
|
exports.deviceRoleMetaOf = deviceRoleMetaOf;
|
|
48742
|
+
exports.deviceTypeLabel = deviceTypeLabel;
|
|
48357
48743
|
exports.deviceTypeMeta = deviceTypeMeta;
|
|
48358
48744
|
exports.deviceTypeMetaOf = deviceTypeMetaOf;
|
|
48359
48745
|
exports.devicesToOptions = devicesToOptions;
|
|
@@ -48411,6 +48797,7 @@ exports.statusIcons = statusIcons;
|
|
|
48411
48797
|
exports.stepHasModelForFormat = stepHasModelForFormat;
|
|
48412
48798
|
exports.stepModelOptions = stepModelOptions;
|
|
48413
48799
|
exports.stripParentNamePrefix = stripParentNamePrefix;
|
|
48800
|
+
exports.tabForSelector = tabForSelector;
|
|
48414
48801
|
exports.tankAlert = tankAlert;
|
|
48415
48802
|
exports.themeToCss = require_theme_index.themeToCss$1;
|
|
48416
48803
|
exports.trpc = trpc;
|