@camstack/ui-library 1.2.321 → 1.2.323

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/index.cjs CHANGED
@@ -11061,6 +11061,12 @@ var useDishwasherSetOption = trpc.dishwasher.setOption.useMutation;
11061
11061
  var useDishwasherGetOptions = trpc.dishwasher.getOptions.useQuery;
11062
11062
  /** Generated alias around `trpc.dishwasher.getStatus.useQuery`. */
11063
11063
  var useDishwasherGetStatus = trpc.dishwasher.getStatus.useQuery;
11064
+ /** Generated alias around `trpc.displayPalette.getPalette.useQuery`. */
11065
+ var useDisplayPaletteGetPalette = trpc.displayPalette.getPalette.useQuery;
11066
+ /** Generated alias around `trpc.displayPalette.listPresets.useQuery`. */
11067
+ var useDisplayPaletteListPresets = trpc.displayPalette.listPresets.useQuery;
11068
+ /** Generated alias around `trpc.displayPalette.setPalette.useMutation`. */
11069
+ var useDisplayPaletteSetPalette = trpc.displayPalette.setPalette.useMutation;
11064
11070
  /** Generated alias around `trpc.doorbell.getStatus.useQuery`. */
11065
11071
  var useDoorbellGetStatus = trpc.doorbell.getStatus.useQuery;
11066
11072
  /** Generated alias around `trpc.enumSensor.getStatus.useQuery`. */
@@ -33143,6 +33149,143 @@ var Switch = (0, react$1.forwardRef)(({ className, checked, onCheckedChange, lab
33143
33149
  });
33144
33150
  Switch.displayName = "Switch";
33145
33151
  //#endregion
33152
+ //#region src/primitives/tabs-list-scroll.tsx
33153
+ /** Scroll offsets and sizes — jsdom defaults are 0, so tests set these explicitly. */
33154
+ function readTabStripOverflow(el) {
33155
+ const epsilon = 1;
33156
+ const { scrollLeft, scrollWidth, clientWidth } = el;
33157
+ const hasOverflow = scrollWidth > clientWidth + epsilon;
33158
+ return {
33159
+ overflowLeft: hasOverflow && scrollLeft > epsilon,
33160
+ overflowRight: hasOverflow && scrollLeft + clientWidth < scrollWidth - epsilon
33161
+ };
33162
+ }
33163
+ function scrollTabIntoViewNearest(tab) {
33164
+ if (typeof tab.scrollIntoView === "function") tab.scrollIntoView({
33165
+ inline: "nearest",
33166
+ block: "nearest"
33167
+ });
33168
+ }
33169
+ function scrollStripByOneTab(el, direction) {
33170
+ const tabs = el.querySelectorAll("[role=\"tab\"]");
33171
+ if (tabs.length === 0) return;
33172
+ const scrollLeft = el.scrollLeft;
33173
+ if (direction === "right") {
33174
+ for (const tab of tabs) if (tab.offsetLeft + tab.offsetWidth > scrollLeft + 1) {
33175
+ scrollTabIntoViewNearest(tab);
33176
+ return;
33177
+ }
33178
+ return;
33179
+ }
33180
+ for (let i = tabs.length - 1; i >= 0; i--) {
33181
+ const tab = tabs[i];
33182
+ if (tab === void 0) continue;
33183
+ if (tab.offsetLeft < scrollLeft - 1) {
33184
+ scrollTabIntoViewNearest(tab);
33185
+ return;
33186
+ }
33187
+ }
33188
+ }
33189
+ function scrollActiveTabIntoView(el) {
33190
+ const active = el.querySelector("[role=\"tab\"][aria-selected=\"true\"]");
33191
+ if (active !== null && typeof active.scrollIntoView === "function") active.scrollIntoView({
33192
+ inline: "nearest",
33193
+ block: "nearest"
33194
+ });
33195
+ }
33196
+ /**
33197
+ * Horizontally scrollable tab strip with edge fades when tabs are hidden and,
33198
+ * on fine-pointer devices, chevron buttons that scroll one tab at a time.
33199
+ * Touch keeps native swipe — no chevrons on coarse pointers.
33200
+ */
33201
+ function TabsListScrollShell({ activeTabKey, listClassName, className, children, ...props }) {
33202
+ const scrollRef = (0, react$1.useRef)(null);
33203
+ const [overflow, setOverflow] = (0, react$1.useState)({
33204
+ overflowLeft: false,
33205
+ overflowRight: false
33206
+ });
33207
+ const [finePointer, setFinePointer] = (0, react$1.useState)(false);
33208
+ const remeasure = (0, react$1.useCallback)(() => {
33209
+ const el = scrollRef.current;
33210
+ if (el === null) return;
33211
+ setOverflow(readTabStripOverflow(el));
33212
+ }, []);
33213
+ (0, react$1.useEffect)(() => {
33214
+ remeasure();
33215
+ const el = scrollRef.current;
33216
+ if (el === null) return;
33217
+ el.addEventListener("scroll", remeasure, { passive: true });
33218
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(remeasure) : null;
33219
+ ro?.observe(el);
33220
+ for (const child of el.children) ro?.observe(child);
33221
+ return () => {
33222
+ el.removeEventListener("scroll", remeasure);
33223
+ ro?.disconnect();
33224
+ };
33225
+ }, [remeasure, children]);
33226
+ (0, react$1.useEffect)(() => {
33227
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
33228
+ const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
33229
+ const apply = () => {
33230
+ setFinePointer(mq.matches);
33231
+ };
33232
+ apply();
33233
+ mq.addEventListener("change", apply);
33234
+ return () => mq.removeEventListener("change", apply);
33235
+ }, []);
33236
+ (0, react$1.useEffect)(() => {
33237
+ const el = scrollRef.current;
33238
+ if (el === null || activeTabKey === void 0) return;
33239
+ scrollActiveTabIntoView(el);
33240
+ remeasure();
33241
+ }, [activeTabKey, remeasure]);
33242
+ const scrollBy = (direction) => {
33243
+ const el = scrollRef.current;
33244
+ if (el === null) return;
33245
+ scrollStripByOneTab(el, direction);
33246
+ };
33247
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
33248
+ className: cn("relative min-w-0", className),
33249
+ "data-tab-strip-overflow-left": overflow.overflowLeft ? "" : void 0,
33250
+ "data-tab-strip-overflow-right": overflow.overflowRight ? "" : void 0,
33251
+ children: [
33252
+ overflow.overflowLeft && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
33253
+ "aria-hidden": true,
33254
+ className: "pointer-events-none absolute inset-y-0 left-0 z-[1] w-8 bg-gradient-to-r from-background to-transparent"
33255
+ }),
33256
+ overflow.overflowRight && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
33257
+ "aria-hidden": true,
33258
+ className: "pointer-events-none absolute inset-y-0 right-0 z-[1] w-8 bg-gradient-to-l from-background to-transparent"
33259
+ }),
33260
+ finePointer && overflow.overflowLeft && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
33261
+ type: "button",
33262
+ variant: "ghost",
33263
+ size: "sm",
33264
+ icon: ChevronLeft,
33265
+ "aria-label": "Scroll tabs left",
33266
+ className: "absolute left-0 top-1/2 z-[2] -translate-y-1/2 bg-background/80 shadow-sm",
33267
+ onClick: () => scrollBy("left")
33268
+ }),
33269
+ finePointer && overflow.overflowRight && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
33270
+ type: "button",
33271
+ variant: "ghost",
33272
+ size: "sm",
33273
+ icon: ChevronRight,
33274
+ "aria-label": "Scroll tabs right",
33275
+ className: "absolute right-0 top-1/2 z-[2] -translate-y-1/2 bg-background/80 shadow-sm",
33276
+ onClick: () => scrollBy("right")
33277
+ }),
33278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
33279
+ ref: scrollRef,
33280
+ role: "tablist",
33281
+ className: listClassName,
33282
+ ...props,
33283
+ children
33284
+ })
33285
+ ]
33286
+ });
33287
+ }
33288
+ //#endregion
33146
33289
  //#region src/primitives/tabs.tsx
33147
33290
  var TabsContext = (0, react$1.createContext)(null);
33148
33291
  function useTabsContext() {
@@ -33187,17 +33330,18 @@ function Tabs({ value: controlledValue, onValueChange, defaultValue = "", appear
33187
33330
  * pinned to `hidden` makes it impossible here.
33188
33331
  */
33189
33332
  function TabsList({ className, ...props }) {
33190
- const { appearance } = useTabsContext();
33191
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
33192
- role: "tablist",
33193
- className: cn("flex flex-row overflow-x-auto overflow-y-hidden", appearance === "pills" ? "gap-1 border-b border-border-subtle py-1" : "border-b border-border-subtle", className),
33333
+ const { appearance, value } = useTabsContext();
33334
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabsListScrollShell, {
33335
+ activeTabKey: value,
33336
+ listClassName: cn("flex flex-row overflow-x-auto overflow-y-hidden", appearance === "pills" ? "gap-1 border-b border-border-subtle py-1" : "border-b border-border-subtle", className),
33194
33337
  ...props
33195
33338
  });
33196
33339
  }
33197
- function TabsTrigger({ value, className, ...props }) {
33340
+ var TabsTrigger = (0, react$1.forwardRef)(function TabsTrigger({ value, className, ...props }, ref) {
33198
33341
  const { value: activeValue, setValue, appearance } = useTabsContext();
33199
33342
  const isActive = value === activeValue;
33200
33343
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
33344
+ ref,
33201
33345
  type: "button",
33202
33346
  role: "tab",
33203
33347
  "aria-selected": isActive,
@@ -33207,7 +33351,7 @@ function TabsTrigger({ value, className, ...props }) {
33207
33351
  onClick: () => setValue(value),
33208
33352
  ...props
33209
33353
  });
33210
- }
33354
+ });
33211
33355
  function TabsContent({ value, className, ...props }) {
33212
33356
  const { value: activeValue } = useTabsContext();
33213
33357
  if (value !== activeValue) return null;
@@ -39971,58 +40115,66 @@ function useDataTableFilter(rows, config, initialState = EMPTY_DATA_TABLE_FILTER
39971
40115
  //#endregion
39972
40116
  //#region src/composites/detection-colors.ts
39973
40117
  /**
39974
- * Detection class colors — a thin adapter over the SINGLE event-kind
39975
- * taxonomy dictionary in `@camstack/types` (`EVENT_TAXONOMY`). Colors are no
39976
- * longer duplicated here; this module only adds the UI-only classes the
39977
- * taxonomy doesn't model (face / plate) and the deterministic hash fallback.
40118
+ * Detection class colours — the bundled display palette (D939).
39978
40119
  *
39979
- * Used by: DetectionCanvas / DetectionResultTree / DetectionOverlay (UI),
39980
- * and any other place that visualizes detection classes.
40120
+ * A mounted {@link DisplayPaletteProvider} paints the cluster's effective
40121
+ * palette instead. There is no second hex table here.
39981
40122
  */
39982
- /** UI-only classes not modelled in the event taxonomy (sub-detections). */
39983
- var UI_ONLY_COLORS = {
39984
- face: "#a855f7",
39985
- plate: "#ec4899"
39986
- };
40123
+ var BUNDLED$1 = (0, _camstack_types.bundledCamstack)();
40124
+ /** Marks that are palette entries and not detection classes. */
40125
+ var MARK_IDS = new Set([
40126
+ "live",
40127
+ "recording",
40128
+ "heatmapScrub"
40129
+ ]);
39987
40130
  /**
39988
- * Fixed colors for known detection classes — derived from the taxonomy
39989
- * (every detection/motion kind) plus the UI-only face/plate classes. Kept as
39990
- * a named export for back-compat with existing consumers.
40131
+ * Fixed colours for known classes, from the bundled CamStack preset.
40132
+ * Kept for callers that read the map directly.
39991
40133
  */
39992
- var CLASS_COLORS = Object.freeze({
39993
- ...Object.fromEntries(Object.values(_camstack_types.EVENT_TAXONOMY).filter((e) => e.category === "detection" || e.kind === "motion").map((e) => [e.kind, e.color])),
39994
- ...UI_ONLY_COLORS
39995
- });
39996
- /** Fallback color palette for unknown classes (deterministic hash-based) */
39997
- var FALLBACK_PALETTE = [
39998
- "#ef4444",
39999
- "#8b5cf6",
40000
- "#06b6d4",
40001
- "#f97316",
40002
- "#10b981",
40003
- "#6366f1",
40004
- "#e11d48",
40005
- "#0891b2",
40006
- "#7c3aed",
40007
- "#059669"
40008
- ];
40009
- /** Primary/default color when nothing else matches */
40010
- var DEFAULT_COLOR = _camstack_types.DEFAULT_EVENT_COLOR;
40011
- /**
40012
- * Get the color for a detection class name.
40013
- * Resolves: custom override → taxonomy/UI-only fixed color → deterministic
40014
- * hash-based color for unknown classes.
40015
- */
40016
- function getClassColor(className, customColors) {
40017
- if (customColors?.[className]) return customColors[className];
40018
- if (customColors?.[className.toLowerCase()]) return customColors[className.toLowerCase()];
40019
- const fromTaxonomy = _camstack_types.EVENT_TAXONOMY[className] ?? _camstack_types.EVENT_TAXONOMY[className.toLowerCase()];
40020
- if (fromTaxonomy !== void 0) return fromTaxonomy.color;
40021
- if (CLASS_COLORS[className]) return CLASS_COLORS[className];
40022
- if (CLASS_COLORS[className.toLowerCase()]) return CLASS_COLORS[className.toLowerCase()];
40023
- let hash = 0;
40024
- for (let i = 0; i < className.length; i++) hash = hash * 31 + (className.codePointAt(i) ?? 0) >>> 0;
40025
- return FALLBACK_PALETTE[hash % FALLBACK_PALETTE.length] ?? DEFAULT_COLOR;
40134
+ var CLASS_COLORS = Object.freeze(Object.fromEntries(_camstack_types.PALETTE_ENTRY_IDS.filter((id) => !MARK_IDS.has(id)).map((id) => [id, (0, _camstack_types.paintHex)(BUNDLED$1.swatches, BUNDLED$1.fallbacks, id)])));
40135
+ function firstFallback(fallbacks) {
40136
+ const hex = fallbacks[0];
40137
+ if (hex === void 0) throw new Error("display palette has no fallback colour");
40138
+ return hex;
40139
+ }
40140
+ /** Primary colour when nothing else matches — the first bundled fallback. */
40141
+ var DEFAULT_COLOR = firstFallback(BUNDLED$1.fallbacks);
40142
+ /**
40143
+ * Colour for a detection class. Custom overrides win, then the palette
40144
+ * (the bundled preset when none is passed). Unknown classes hash into
40145
+ * the palette's fallbacks.
40146
+ */
40147
+ function getClassColor(className, customColors, palette) {
40148
+ const direct = customColors?.[className];
40149
+ if (direct !== void 0 && direct.length > 0) return direct;
40150
+ const lower = className.toLowerCase();
40151
+ const lowered = customColors?.[lower];
40152
+ if (lowered !== void 0 && lowered.length > 0) return lowered;
40153
+ return (0, _camstack_types.paintHex)(palette?.swatches ?? BUNDLED$1.swatches, palette?.fallbacks ?? BUNDLED$1.fallbacks, className);
40154
+ }
40155
+ //#endregion
40156
+ //#region src/composites/display-palette-context.tsx
40157
+ /**
40158
+ * The cluster palette, when a host has read `display-palette` (D939).
40159
+ * No provider, a pending read, or an unknown store all paint the bundled
40160
+ * CamStack preset. Only `ready` replaces it. Callers that must say the
40161
+ * store was unreadable read {@link useDisplayPaletteView}.
40162
+ */
40163
+ var BUNDLED = (0, _camstack_types.bundledCamstack)();
40164
+ var DisplayPaletteContext = (0, react$1.createContext)({ status: "absent" });
40165
+ function DisplayPaletteProvider({ value, children }) {
40166
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisplayPaletteContext.Provider, {
40167
+ value,
40168
+ children
40169
+ });
40170
+ }
40171
+ function useDisplayPaletteView() {
40172
+ return (0, react$1.useContext)(DisplayPaletteContext);
40173
+ }
40174
+ function useClassPainter() {
40175
+ const view = (0, react$1.useContext)(DisplayPaletteContext);
40176
+ const palette = view.status === "ready" ? view.palette : BUNDLED;
40177
+ return (className, customColors) => getClassColor(className, customColors, palette);
40026
40178
  }
40027
40179
  //#endregion
40028
40180
  //#region src/composites/detection-canvas.tsx
@@ -40037,8 +40189,9 @@ function getClassColor(className, customColors) {
40037
40189
  */
40038
40190
  function DetectionCanvas({ src, imageWidth, imageHeight, detections = [], classColors, aspectRatio, className, placeholder, showConfidence = true, minConfidence = 0, borderWidth = 2 }) {
40039
40191
  const t = useUiText();
40192
+ const paint = useClassPainter();
40040
40193
  function getColor(cls) {
40041
- return getClassColor(cls, classColors);
40194
+ return paint(cls, classColors);
40042
40195
  }
40043
40196
  const ratio = aspectRatio ?? (imageWidth && imageHeight ? `${imageWidth}/${imageHeight}` : "16/9");
40044
40197
  const filtered = detections.filter((d) => d.score >= minConfidence);
@@ -40095,6 +40248,7 @@ function DetectionCanvas({ src, imageWidth, imageHeight, detections = [], classC
40095
40248
  });
40096
40249
  }
40097
40250
  function BoundingBox({ detection, imageWidth, imageHeight, color, showConfidence, borderWidth, children }) {
40251
+ const paint = useClassPainter();
40098
40252
  const { x, y, width, height } = detection.bbox;
40099
40253
  const labelsElement = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
40100
40254
  className: "absolute left-0 flex flex-col items-start gap-px",
@@ -40108,7 +40262,7 @@ function BoundingBox({ detection, imageWidth, imageHeight, color, showConfidence
40108
40262
  children: [detection.macroClass, showConfidence && ` ${(detection.score * 100).toFixed(0)}%`]
40109
40263
  }), detection.labels.map((l, k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40110
40264
  className: "text-[9px] font-semibold px-1 rounded-sm whitespace-nowrap text-white",
40111
- style: { backgroundColor: getClassColor(l.label) },
40265
+ style: { backgroundColor: paint(l.label) },
40112
40266
  children: [
40113
40267
  l.label,
40114
40268
  " ",
@@ -40179,6 +40333,7 @@ function MaskOverlay({ mask, maskWidth, maskHeight, bbox, imageWidth, imageHeigh
40179
40333
  });
40180
40334
  }
40181
40335
  function ChildBoundingBox({ child, parentBbox, color, showConfidence }) {
40336
+ const paint = useClassPainter();
40182
40337
  if (parentBbox.width <= 0 || parentBbox.height <= 0) return null;
40183
40338
  const relLeft = (child.bbox.x - parentBbox.x) / parentBbox.width * 100;
40184
40339
  const relTop = (child.bbox.y - parentBbox.y) / parentBbox.height * 100;
@@ -40207,7 +40362,7 @@ function ChildBoundingBox({ child, parentBbox, color, showConfidence }) {
40207
40362
  children: [child.macroClass, showConfidence && ` ${(child.score * 100).toFixed(0)}%`]
40208
40363
  }), child.labels.map((l, k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40209
40364
  className: "text-[8px] font-semibold px-0.5 rounded-sm whitespace-nowrap text-white",
40210
- style: { backgroundColor: getClassColor(l.label) },
40365
+ style: { backgroundColor: paint(l.label) },
40211
40366
  children: [
40212
40367
  l.label,
40213
40368
  " ",
@@ -40385,9 +40540,10 @@ function MotionZones({ motion }) {
40385
40540
  * one a detection box can never answer.
40386
40541
  */
40387
40542
  function StationaryBoxes({ boxes }) {
40543
+ const paint = useClassPainter();
40388
40544
  const now = Date.now();
40389
40545
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: boxes.map((b) => {
40390
- const color = getClassColor(b.className);
40546
+ const color = paint(b.className);
40391
40547
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40392
40548
  "data-census-box": "",
40393
40549
  className: "absolute",
@@ -40418,6 +40574,7 @@ function StationaryBoxes({ boxes }) {
40418
40574
  }) });
40419
40575
  }
40420
40576
  function DetectionBoxes({ detections, frameWidth, frameHeight }) {
40577
+ const paint = useClassPainter();
40421
40578
  if (frameWidth === 0 || frameHeight === 0) return null;
40422
40579
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: detections.map((d) => {
40423
40580
  const { x, y, width: bw, height: bh } = d.bbox;
@@ -40425,7 +40582,7 @@ function DetectionBoxes({ detections, frameWidth, frameHeight }) {
40425
40582
  const top = y / frameHeight * 100;
40426
40583
  const width = bw / frameWidth * 100;
40427
40584
  const height = bh / frameHeight * 100;
40428
- const color = getClassColor(d.macroClass);
40585
+ const color = paint(d.macroClass);
40429
40586
  const parts = [`${d.macroClass} ${(d.score * 100).toFixed(0)}%`];
40430
40587
  for (const l of d.labels) parts.push(l.label);
40431
40588
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -40500,7 +40657,8 @@ function DetectionResultTree({ detections, classColors, className, hiddenKeys, o
40500
40657
  });
40501
40658
  }
40502
40659
  function DetectionNode({ detection, children_, colors, hiddenKeys, onToggleVisibility }) {
40503
- const color = getClassColor(detection.macroClass, colors);
40660
+ const paint = useClassPainter();
40661
+ const color = paint(detection.macroClass, colors);
40504
40662
  const isVisible = !hiddenKeys?.has(detection.id);
40505
40663
  const mask = detection.debug?.mask;
40506
40664
  const maskWidth = detection.debug?.maskWidth;
@@ -40565,8 +40723,8 @@ function DetectionNode({ detection, children_, colors, hiddenKeys, onToggleVisib
40565
40723
  children: detection.labels.map((l, k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40566
40724
  className: "inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded-full",
40567
40725
  style: {
40568
- backgroundColor: getClassColor(l.label, colors) + "20",
40569
- color: getClassColor(l.label, colors)
40726
+ backgroundColor: paint(l.label, colors) + "20",
40727
+ color: paint(l.label, colors)
40570
40728
  },
40571
40729
  children: [
40572
40730
  l.label,
@@ -40595,10 +40753,11 @@ function DetectionNode({ detection, children_, colors, hiddenKeys, onToggleVisib
40595
40753
  });
40596
40754
  }
40597
40755
  function ChildrenTree({ items, colors, hiddenKeys, onToggleVisibility }) {
40756
+ const paint = useClassPainter();
40598
40757
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
40599
40758
  className: "ml-4 mt-1.5 space-y-1.5 border-l-2 border-border pl-3",
40600
40759
  children: items.map((child) => {
40601
- const childColor = getClassColor(child.macroClass, colors);
40760
+ const childColor = paint(child.macroClass, colors);
40602
40761
  const isVisible = !hiddenKeys?.has(child.id);
40603
40762
  const mask = child.debug?.mask;
40604
40763
  const maskWidth = child.debug?.maskWidth;
@@ -40642,8 +40801,8 @@ function ChildrenTree({ items, colors, hiddenKeys, onToggleVisibility }) {
40642
40801
  children: child.labels.map((l, k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40643
40802
  className: "inline-flex items-center gap-0.5 text-[9px] font-medium px-1 py-0.5 rounded-full",
40644
40803
  style: {
40645
- backgroundColor: getClassColor(l.label, colors) + "20",
40646
- color: getClassColor(l.label, colors)
40804
+ backgroundColor: paint(l.label, colors) + "20",
40805
+ color: paint(l.label, colors)
40647
40806
  },
40648
40807
  children: [
40649
40808
  l.label,
@@ -40661,6 +40820,7 @@ function ChildrenTree({ items, colors, hiddenKeys, onToggleVisibility }) {
40661
40820
  }
40662
40821
  function AlternateLabelsSection({ alternateLabels, colors }) {
40663
40822
  const [expanded, setExpanded] = (0, react$1.useState)(false);
40823
+ const paint = useClassPainter();
40664
40824
  const t = useUiText();
40665
40825
  const sources = Object.keys(alternateLabels);
40666
40826
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -40688,8 +40848,8 @@ function AlternateLabelsSection({ alternateLabels, colors }) {
40688
40848
  children: labels.map((l, k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
40689
40849
  className: "inline-flex items-center gap-0.5 text-[9px] px-1 py-0.5 rounded-full",
40690
40850
  style: {
40691
- backgroundColor: getClassColor(source, colors) + "10",
40692
- color: getClassColor(source, colors),
40851
+ backgroundColor: paint(source, colors) + "10",
40852
+ color: paint(source, colors),
40693
40853
  opacity: Math.max(.4, l.score)
40694
40854
  },
40695
40855
  children: [l.label, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -65327,7 +65487,7 @@ function noItemsSentence(noun, emptyMeansAll, t) {
65327
65487
  noun: pickerNounLabel(noun, t)
65328
65488
  });
65329
65489
  }
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 }) {
65490
+ 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
65491
  const t = useUiText();
65332
65492
  const groupTitle = groupTitleProp ?? ((group) => deviceGroupTitle(group, t));
65333
65493
  const lockedNote = lockedNoteProp ?? t("uiLibrary.devicePicker.alreadyAdded", "Already added");
@@ -65360,7 +65520,7 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65360
65520
  children: [
65361
65521
  listState === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesLoadingLine, { noun: plural }),
65362
65522
  listState === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesFailedLine, { noun: noun === "camera" ? "devices" : plural }),
65363
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65523
+ !(selection === "single" && chips.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
65364
65524
  className: "flex flex-wrap items-center gap-1",
65365
65525
  children: [
65366
65526
  chips.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -65404,7 +65564,9 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65404
65564
  groupTitle,
65405
65565
  noun,
65406
65566
  disabled,
65407
- showAll
65567
+ showAll,
65568
+ selection,
65569
+ ...pinnedGroupBy !== void 0 ? { pinnedGroupBy } : {}
65408
65570
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
65409
65571
  devices,
65410
65572
  value,
@@ -65415,7 +65577,8 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65415
65577
  groupTitle,
65416
65578
  noun,
65417
65579
  disabled,
65418
- showAll
65580
+ showAll,
65581
+ selection
65419
65582
  })),
65420
65583
  widening && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesWidenWarning, { noun })
65421
65584
  ]
@@ -65426,13 +65589,23 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, e
65426
65589
  * read below happens only then (for Zone grouping and the Zone filter).
65427
65590
  */
65428
65591
  function GroupedPickerPanel(props) {
65429
- const { groupBy, setGroupBy } = useStoredGroupBy();
65592
+ const stored = useStoredGroupBy();
65593
+ const [localGroupBy, setLocalGroupBy] = (0, react$1.useState)(null);
65594
+ const pinned = props.pinnedGroupBy;
65595
+ const groupBy = pinned !== void 0 ? localGroupBy ?? pinned : stored.groupBy;
65596
+ const onGroupByChange = (next) => {
65597
+ if (pinned !== void 0) {
65598
+ setLocalGroupBy(next);
65599
+ return;
65600
+ }
65601
+ stored.setGroupBy(next);
65602
+ };
65430
65603
  const zones = usePickerSiteZones(true);
65431
65604
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
65432
65605
  ...props,
65433
65606
  grouping: {
65434
65607
  groupBy,
65435
- onGroupByChange: setGroupBy,
65608
+ onGroupByChange,
65436
65609
  zones
65437
65610
  }
65438
65611
  });
@@ -65450,8 +65623,9 @@ var NO_ZONES = {
65450
65623
  zones: []
65451
65624
  };
65452
65625
  /** The searchable, grouped list — rendered one page at a time. */
65453
- function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, grouping }) {
65626
+ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, selection, grouping }) {
65454
65627
  const t = useUiText();
65628
+ const radioName = (0, react$1.useId)();
65455
65629
  const [filters, setFilters] = (0, react$1.useState)(PICKER_NO_FILTERS);
65456
65630
  const [toggles, setToggles] = (0, react$1.useState)(/* @__PURE__ */ new Map());
65457
65631
  const [shown, setShown] = (0, react$1.useState)(60);
@@ -65525,7 +65699,10 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
65525
65699
  }),
65526
65700
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
65527
65701
  className: "mt-1 text-[10px] text-foreground-subtle",
65528
- children: t("uiLibrary.devicePicker.shownOf", "{{shown}} of {{total}} shown · {{selected}} selected", {
65702
+ children: selection === "single" ? t("uiLibrary.devicePicker.shownOfSingle", "{{shown}} of {{total}} shown", {
65703
+ shown: matches.length,
65704
+ total: devices.length
65705
+ }) : t("uiLibrary.devicePicker.shownOf", "{{shown}} of {{total}} shown · {{selected}} selected", {
65529
65706
  shown: matches.length,
65530
65707
  total: devices.length,
65531
65708
  selected: value.length
@@ -65547,7 +65724,9 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
65547
65724
  groupBy: shownBy,
65548
65725
  disabled,
65549
65726
  locked,
65550
- lockedNote
65727
+ lockedNote,
65728
+ selection,
65729
+ radioName
65551
65730
  }, row.key)), page.remaining > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65552
65731
  type: "button",
65553
65732
  className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
@@ -65577,8 +65756,9 @@ function secondLine(device, groupBy) {
65577
65756
  ].filter((part) => part !== null);
65578
65757
  return parts.length > 0 ? parts.join(" · ") : void 0;
65579
65758
  }
65580
- function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote }) {
65759
+ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote, selection, radioName }) {
65581
65760
  const t = useUiText();
65761
+ const single = selection === "single";
65582
65762
  if (row.kind === "header") {
65583
65763
  const { group, expanded } = row;
65584
65764
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -65596,7 +65776,7 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65596
65776
  className: "truncate",
65597
65777
  children: groupTitle(group)
65598
65778
  }),
65599
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
65779
+ !single && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
65600
65780
  className: "font-normal normal-case",
65601
65781
  children: [
65602
65782
  "(",
@@ -65608,14 +65788,14 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65608
65788
  })
65609
65789
  ]
65610
65790
  }),
65611
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65791
+ !single && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65612
65792
  type: "button",
65613
65793
  disabled,
65614
65794
  className: "text-[10px] text-primary hover:underline disabled:opacity-50",
65615
65795
  onClick: () => onChange(selectPickerGroup(value, group)),
65616
65796
  children: t("uiLibrary.devicePicker.all", "All")
65617
65797
  }),
65618
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65798
+ !single && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65619
65799
  type: "button",
65620
65800
  disabled,
65621
65801
  className: "text-[10px] text-foreground-subtle hover:underline disabled:opacity-50",
@@ -65632,10 +65812,11 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
65632
65812
  className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
65633
65813
  "data-testid": "device-picker-row",
65634
65814
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
65635
- type: "checkbox",
65815
+ type: single ? "radio" : "checkbox",
65816
+ ...single ? { name: radioName } : {},
65636
65817
  checked: row.selected || isLocked,
65637
65818
  disabled: disabled || isLocked,
65638
- onChange: () => onChange(togglePickerDevice(value, row.device.id)),
65819
+ onChange: () => onChange(single ? [row.device.id] : togglePickerDevice(value, row.device.id)),
65639
65820
  className: "accent-primary",
65640
65821
  "data-device-id": String(row.device.id)
65641
65822
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -68633,6 +68814,7 @@ exports.DialogHeader = DialogHeader;
68633
68814
  exports.DialogTitle = DialogTitle;
68634
68815
  exports.DialogTrigger = DialogTrigger;
68635
68816
  exports.DiscoveryPanel = DiscoveryPanel;
68817
+ exports.DisplayPaletteProvider = DisplayPaletteProvider;
68636
68818
  exports.Dog = Dog;
68637
68819
  exports.DoorClosed = DoorClosed;
68638
68820
  exports.DoorOpen = DoorOpen;
@@ -69534,6 +69716,7 @@ exports.useCameraStreamsGetRtspEntries = useCameraStreamsGetRtspEntries;
69534
69716
  exports.useCameraStreamsPickStream = useCameraStreamsPickStream;
69535
69717
  exports.useCapabilityPresence = useCapabilityPresence;
69536
69718
  exports.useCarbonMonoxideGetStatus = useCarbonMonoxideGetStatus;
69719
+ exports.useClassPainter = useClassPainter;
69537
69720
  exports.useClimateControlGetStatus = useClimateControlGetStatus;
69538
69721
  exports.useClimateControlSetFanMode = useClimateControlSetFanMode;
69539
69722
  exports.useClimateControlSetMode = useClimateControlSetMode;
@@ -69802,6 +69985,10 @@ exports.useDishwasherSetVarioSpeedPlus = useDishwasherSetVarioSpeedPlus;
69802
69985
  exports.useDishwasherStart = useDishwasherStart;
69803
69986
  exports.useDishwasherStop = useDishwasherStop;
69804
69987
  exports.useDisplayIconResolver = useDisplayIconResolver;
69988
+ exports.useDisplayPaletteGetPalette = useDisplayPaletteGetPalette;
69989
+ exports.useDisplayPaletteListPresets = useDisplayPaletteListPresets;
69990
+ exports.useDisplayPaletteSetPalette = useDisplayPaletteSetPalette;
69991
+ exports.useDisplayPaletteView = useDisplayPaletteView;
69805
69992
  exports.useDoorbellEvents = useDoorbellEvents;
69806
69993
  exports.useDoorbellGetStatus = useDoorbellGetStatus;
69807
69994
  exports.useEnumSensorGetStatus = useEnumSensorGetStatus;