@camstack/ui-library 1.2.316 → 1.2.317

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
@@ -1899,26 +1899,6 @@ var Flashlight = require_createLucideIcon.createLucideIcon("flashlight", [
1899
1899
  * This source code is licensed under the ISC license.
1900
1900
  * See the LICENSE file in the root directory of this source tree.
1901
1901
  */
1902
- var FlaskConical = require_createLucideIcon.createLucideIcon("flask-conical", [
1903
- ["path", {
1904
- d: "M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2",
1905
- key: "18mbvz"
1906
- }],
1907
- ["path", {
1908
- d: "M6.453 15h11.094",
1909
- key: "3shlmq"
1910
- }],
1911
- ["path", {
1912
- d: "M8.5 2h7",
1913
- key: "csnxdl"
1914
- }]
1915
- ]);
1916
- /**
1917
- * @license lucide-react v0.576.0 - ISC
1918
- *
1919
- * This source code is licensed under the ISC license.
1920
- * See the LICENSE file in the root directory of this source tree.
1921
- */
1922
1902
  var Footprints = require_createLucideIcon.createLucideIcon("footprints", [
1923
1903
  ["path", {
1924
1904
  d: "M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z",
@@ -5870,7 +5850,7 @@ function lucideExportName(key) {
5870
5850
  function loadIconGlyphs() {
5871
5851
  if (fullTable !== void 0) return Promise.resolve();
5872
5852
  if (failure$1 !== void 0) return Promise.reject(failure$1);
5873
- pending ??= Promise.resolve().then(() => require("./icons-BTux33dH.cjs")).then((module) => {
5853
+ pending ??= Promise.resolve().then(() => require("./icons-pqV1UOV_.cjs")).then((module) => {
5874
5854
  fullTable = module;
5875
5855
  publish();
5876
5856
  }, (error) => {
@@ -10801,6 +10781,10 @@ var useDeviceHistoryGetDeviceEnabled = trpc.deviceHistory.getDeviceEnabled.useQu
10801
10781
  var useDeviceHistorySetDeviceEnabled = trpc.deviceHistory.setDeviceEnabled.useMutation;
10802
10782
  /** Generated alias around `trpc.deviceHistory.getStatus.useQuery`. */
10803
10783
  var useDeviceHistoryGetStatus = trpc.deviceHistory.getStatus.useQuery;
10784
+ /** Generated alias around `trpc.deviceHistory.listOrphanSeries.useQuery`. */
10785
+ var useDeviceHistoryListOrphanSeries = trpc.deviceHistory.listOrphanSeries.useQuery;
10786
+ /** Generated alias around `trpc.deviceHistory.deleteOrphanSeries.useMutation`. */
10787
+ var useDeviceHistoryDeleteOrphanSeries = trpc.deviceHistory.deleteOrphanSeries.useMutation;
10804
10788
  /** Generated alias around `trpc.deviceManager.allocateDeviceId.useMutation`. */
10805
10789
  var useDeviceManagerAllocateDeviceId = trpc.deviceManager.allocateDeviceId.useMutation;
10806
10790
  /** Generated alias around `trpc.deviceManager.migrateDevice.useMutation`. */
@@ -19149,20 +19133,30 @@ function useDeviceAutotrack(deviceId) {
19149
19133
  }
19150
19134
  //#endregion
19151
19135
  //#region src/composites/widget-panel.tsx
19152
- function WidgetPanel({ title, icon, headerRight, collapsible = false, defaultOpen = true, bodyClassName = "p-3", children }) {
19153
- const [open, setOpen] = (0, react$1.useState)(defaultOpen);
19136
+ function WidgetPanel({ title, icon, headerRight, collapsible = false, defaultOpen = true, open: openProp, onOpenChange, bodyClassName = "p-3", children }) {
19137
+ const [openState, setOpenState] = (0, react$1.useState)(defaultOpen);
19138
+ const open = openProp ?? openState;
19139
+ const toggle = () => {
19140
+ const next = !open;
19141
+ if (openProp === void 0) setOpenState(next);
19142
+ onOpenChange?.(next);
19143
+ };
19154
19144
  const showBody = !collapsible || open;
19155
19145
  const headerInner = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
19156
19146
  className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-foreground",
19157
19147
  children: [icon, title]
19158
- }), collapsible ? open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3.5 w-3.5 text-foreground-subtle" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3.5 w-3.5 text-foreground-subtle" }) : headerRight] });
19148
+ }), collapsible ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
19149
+ className: "flex min-w-0 items-center gap-2",
19150
+ children: [headerRight, open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" })]
19151
+ }) : headerRight] });
19159
19152
  const headerClass = `px-4 py-2 flex items-center justify-between ${showBody ? "border-b border-border" : ""}`;
19160
19153
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19161
19154
  className: "rounded-lg border border-border bg-surface overflow-hidden",
19162
19155
  children: [collapsible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
19163
19156
  type: "button",
19164
- onClick: () => setOpen((v) => !v),
19165
- className: `w-full text-left ${headerClass}`,
19157
+ "aria-expanded": open,
19158
+ onClick: toggle,
19159
+ className: `w-full gap-2 text-left ${headerClass}`,
19166
19160
  children: headerInner
19167
19161
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19168
19162
  className: headerClass,
@@ -20523,6 +20517,60 @@ function useDeviceCapability(options) {
20523
20517
  };
20524
20518
  }
20525
20519
  //#endregion
20520
+ //#region src/hooks/use-persisted-disclosure.ts
20521
+ /**
20522
+ * usePersistedDisclosure — an open / closed choice the viewer made, remembered
20523
+ * per viewer under the caller's key (one key per section and device).
20524
+ *
20525
+ * `null` means the viewer never chose. The caller then shows its own default,
20526
+ * which may follow the data (the Consumables section opens itself while an
20527
+ * item needs attention) — the default is never written, so it can change with
20528
+ * the data until the viewer picks.
20529
+ *
20530
+ * Storage is a convenience, never a requirement: every read and write is
20531
+ * guarded, and a blocked / private-mode storage means the choice lasts until
20532
+ * the page is left. A stored value this hook did not write is ignored.
20533
+ */
20534
+ var OPEN = "open";
20535
+ var CLOSED = "closed";
20536
+ function readStored$1(key) {
20537
+ if (key === null || typeof window === "undefined") return null;
20538
+ try {
20539
+ const raw = window.localStorage.getItem(key);
20540
+ if (raw === OPEN) return true;
20541
+ if (raw === CLOSED) return false;
20542
+ return null;
20543
+ } catch {
20544
+ return null;
20545
+ }
20546
+ }
20547
+ function writeStored$1(key, open) {
20548
+ if (key === null || typeof window === "undefined") return;
20549
+ try {
20550
+ window.localStorage.setItem(key, open ? OPEN : CLOSED);
20551
+ } catch {}
20552
+ }
20553
+ /** @returns `[choice, setChoice]` — `choice` is `null` until the viewer picks. */
20554
+ function usePersistedDisclosure(storageKey) {
20555
+ const [choice, setChoice] = (0, react$1.useState)(() => ({
20556
+ key: storageKey,
20557
+ open: readStored$1(storageKey)
20558
+ }));
20559
+ const current = choice.key === storageKey ? choice : {
20560
+ key: storageKey,
20561
+ open: readStored$1(storageKey)
20562
+ };
20563
+ if (current !== choice) setChoice(current);
20564
+ const update = (0, react$1.useCallback)((open) => {
20565
+ setChoice({
20566
+ key: storageKey,
20567
+ open
20568
+ });
20569
+ writeStored$1(storageKey, open);
20570
+ }, [storageKey]);
20571
+ return [current.open, update];
20572
+ }
20573
+ //#endregion
20526
20574
  //#region src/composites/consumable-level-badge.tsx
20527
20575
  var LOOKS$1 = {
20528
20576
  ok: {
@@ -20543,6 +20591,12 @@ var LOOKS$1 = {
20543
20591
  labelKey: "uiLibrary.consumableLevel.empty",
20544
20592
  labelFallback: "Empty"
20545
20593
  },
20594
+ full: {
20595
+ variant: "warning",
20596
+ barClass: "bg-warning",
20597
+ labelKey: "uiLibrary.consumableLevel.full",
20598
+ labelFallback: "Full"
20599
+ },
20546
20600
  unknown: {
20547
20601
  variant: "neutral",
20548
20602
  barClass: "bg-foreground-subtle",
@@ -20566,39 +20620,57 @@ function consumableLevelLabel(level, t) {
20566
20620
  const look = lookOf(level);
20567
20621
  return t(look.labelKey, look.labelFallback);
20568
20622
  }
20569
- /** A remaining-life % at or below this is `low`. */
20623
+ /** A remaining-life (or supply fill) % at or below this is `low`. */
20570
20624
  var CONSUMABLE_LOW_PERCENT = 15;
20571
- /**
20572
- * A `consumables` cap item as a level: a known % (0 = empty, at or below
20573
- * {@link CONSUMABLE_LOW_PERCENT} = low), never better than `low` when the
20574
- * provider also says `replace`; without a % the discrete status (`replace` =
20575
- * low); else unknown.
20576
- */
20577
- function consumableItemLevel(item) {
20625
+ /** A waste item: full is bad, low or empty is what it should be. */
20626
+ function wasteLevel(item) {
20627
+ if (item.status === "full" || item.status === "replace") return "full";
20628
+ if (item.level !== null) return item.level >= 85 ? "full" : "ok";
20629
+ return item.status === null ? null : "ok";
20630
+ }
20631
+ /** A wear part or a supply: low / empty is bad, a full supply is fine. */
20632
+ function drawnLevel(item) {
20578
20633
  if (item.level !== null) {
20579
- if (item.level <= 0) return "empty";
20580
- if (item.level <= 15 || item.status === "replace") return "low";
20634
+ if (item.level <= 0 || item.status === "empty") return "empty";
20635
+ if (item.level <= 15 || item.status === "replace" || item.status === "low") return "low";
20581
20636
  return "ok";
20582
20637
  }
20583
20638
  switch (item.status) {
20584
- case "replace": return "low";
20585
- case "ok": return "ok";
20639
+ case "replace":
20640
+ case "low": return "low";
20641
+ case "empty": return "empty";
20642
+ case "ok":
20643
+ case "full": return "ok";
20586
20644
  case null: return null;
20587
20645
  }
20588
20646
  }
20589
20647
  /**
20648
+ * A `consumables` cap item as a level, by its kind (D915):
20649
+ * - a `waste` item (a dust bin) is `full` when the provider says so or its
20650
+ * fill is at or above {@link CONSUMABLE_FULL_PERCENT}, else ok;
20651
+ * - a `wear` part or a `supply` reads a known % (0 = empty, at or below
20652
+ * {@link CONSUMABLE_LOW_PERCENT} = low), never better than `low` when the
20653
+ * provider says `replace` or `low`; without a % its discrete status.
20654
+ * Nothing known is unknown (`null`).
20655
+ */
20656
+ function consumableItemLevel(item) {
20657
+ return (0, _camstack_types.consumableKindOf)(item) === "waste" ? wasteLevel(item) : drawnLevel(item);
20658
+ }
20659
+ /** The supply vocabulary a level FIELD of a cap is typed by: `ok | low | empty`. */
20660
+ var SUPPLY_LEVELS = _camstack_types.ConsumableLevelSchema.exclude(["full"]).options;
20661
+ /**
20590
20662
  * Whether a cap state field IS a consumable level: an enum whose members are
20591
- * exactly the consumable level enum. Derived from the cap's schema, so a new
20592
- * cap that uses the enum is recognised without naming it anywhere.
20663
+ * exactly the supply levels (`ok | low | empty`). Derived from the schema, so a
20664
+ * new cap that uses the vocabulary is recognised without naming it anywhere.
20593
20665
  */
20594
20666
  function isConsumableLevelField(field) {
20595
20667
  if (field.kind !== "enum" || field.enumValues === void 0) return false;
20596
- const members = _camstack_types.DishwasherConsumableLevelSchema.options;
20668
+ const members = SUPPLY_LEVELS;
20597
20669
  return field.enumValues.length === members.length && members.every((m) => field.enumValues?.includes(m));
20598
20670
  }
20599
20671
  /** A value read from a slice as a level; anything that is not a member is unknown. */
20600
20672
  function parseConsumableLevel(value) {
20601
- const parsed = _camstack_types.DishwasherConsumableLevelSchema.safeParse(value);
20673
+ const parsed = _camstack_types.ConsumableLevelSchema.safeParse(value);
20602
20674
  return parsed.success ? parsed.data : null;
20603
20675
  }
20604
20676
  function ConsumableLevelBadge({ level, name, label, size = "md", className }) {
@@ -20613,26 +20685,50 @@ function ConsumableLevelBadge({ level, name, label, size = "md", className }) {
20613
20685
  });
20614
20686
  }
20615
20687
  //#endregion
20616
- //#region src/composites/cap-settings/ConsumablesPanel.tsx
20617
- /**
20618
- * ConsumablesPanel — maintenance-items surface for the generic
20619
- * `consumables` capability. A cap-settings component (ui-library),
20620
- * mounted as a cap-contributed top-level device-detail tab via the
20621
- * cap-UI contribution mechanism (the `consumables` cap declares
20622
- * `ui: { kind:'widget', widgetId:'host/consumables-panel', tab:'consumables', topTab:true, ... }`).
20623
- *
20624
- * Reads the cap's pushed runtime-state slice via `useDeviceCapability`
20625
- * and renders one row per item: a fill bar + percentage when `level` is
20626
- * known, else a `ConsumableLevelBadge` (OK / Replace, or Unknown when
20627
- * neither is known) — the bar's colour and the badge's tone come from the
20628
- * same `consumableItemLevel` mapping. When a provider also reports `remaining` (a count in
20629
- * days/hours/cycles), its value is shown alongside — `null` renders a
20630
- * muted em-dash, never a fake `0` (D393); absent `remaining` renders
20631
- * nothing, exactly as before this field existed. A per-item "Replaced"
20632
- * button calls `cap.reset` when the item is `resettable`. The cap is
20633
- * inert until a provider sets items — an empty list renders a muted
20634
- * placeholder.
20635
- */
20688
+ //#region src/primitives/skeleton.tsx
20689
+ var Skeleton = (0, react$1.forwardRef)(({ className, ...props }, ref) => {
20690
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20691
+ ref,
20692
+ className: cn("animate-pulse bg-surface-hover rounded-md h-4 w-full", className),
20693
+ ...props
20694
+ });
20695
+ });
20696
+ Skeleton.displayName = "Skeleton";
20697
+ //#endregion
20698
+ //#region src/primitives/button.tsx
20699
+ var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
20700
+ variants: {
20701
+ variant: {
20702
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90",
20703
+ secondary: "bg-surface text-foreground hover:bg-surface-hover",
20704
+ ghost: "hover:bg-surface-hover text-foreground-muted",
20705
+ danger: "bg-danger text-white hover:bg-danger/90",
20706
+ outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
20707
+ },
20708
+ size: {
20709
+ sm: "h-7 px-2.5 text-xs gap-1.5",
20710
+ md: "h-8 px-3 text-sm gap-2",
20711
+ lg: "h-9 px-4 text-sm gap-2"
20712
+ }
20713
+ },
20714
+ defaultVariants: {
20715
+ variant: "primary",
20716
+ size: "sm"
20717
+ }
20718
+ });
20719
+ var Button = (0, react$1.forwardRef)(({ className, variant, size, ...props }, ref) => {
20720
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
20721
+ ref,
20722
+ className: cn(buttonVariants({
20723
+ variant,
20724
+ size
20725
+ }), className),
20726
+ ...props
20727
+ });
20728
+ });
20729
+ Button.displayName = "Button";
20730
+ //#endregion
20731
+ //#region src/composites/cap-settings/consumable-rows.tsx
20636
20732
  /** `value` null = unknown (D393) — renders an em-dash, never a fake `0`. */
20637
20733
  function formatRemaining(t, r) {
20638
20734
  if (r.value === null) return "—";
@@ -20664,55 +20760,278 @@ function formatResetAt(ms) {
20664
20760
  return "";
20665
20761
  }
20666
20762
  }
20667
- function ConsumableRow({ item, busy, canReset, onReset }) {
20763
+ function needsAttention(level) {
20764
+ return level === "low" || level === "empty" || level === "full";
20765
+ }
20766
+ function clampPercent(value) {
20767
+ return Math.max(0, Math.min(100, value));
20768
+ }
20769
+ function RowFrame({ item, warn, children }) {
20770
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20771
+ "data-form": (0, _camstack_types.consumableFormOf)(item),
20772
+ className: `flex gap-2.5 rounded-md border p-3 ${warn ? "border-warning/40 bg-warning/5" : "border-border-subtle"}`,
20773
+ children
20774
+ });
20775
+ }
20776
+ function RowLabel({ item, warn }) {
20777
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20778
+ className: `truncate text-xs font-semibold ${warn ? "text-warning" : "text-foreground"}`,
20779
+ children: item.label
20780
+ });
20781
+ }
20782
+ /** A wear part: bar + %, or its verdict; remaining time; the reset. */
20783
+ function PartRow({ item, busy, canReset, onReset }) {
20668
20784
  const t = useUiText();
20669
20785
  const level = consumableItemLevel(item);
20670
- const warn = level === "low" || level === "empty";
20671
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20672
- className: `flex flex-col gap-1.5 rounded-md border p-3 ${warn ? "border-warning/40 bg-warning/5" : "border-border/40 bg-surface-elevated/30"}`,
20673
- children: [
20674
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20675
- className: "flex items-center justify-between gap-2",
20676
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20677
- className: `text-xs font-semibold ${warn ? "text-warning" : "text-foreground"}`,
20678
- children: item.label
20679
- }), item.resettable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
20786
+ const warn = needsAttention(level);
20787
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowFrame, {
20788
+ item,
20789
+ warn,
20790
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20791
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
20792
+ children: [
20793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowLabel, {
20794
+ item,
20795
+ warn
20796
+ }),
20797
+ item.level !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20798
+ className: "flex items-center gap-2",
20799
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20800
+ className: "h-1.5 flex-1 overflow-hidden rounded-full bg-surface-hover",
20801
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20802
+ className: `h-full rounded-full ${consumableLevelBarClass(level)} transition-all`,
20803
+ style: { width: `${clampPercent(item.level)}%` }
20804
+ })
20805
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
20806
+ className: "text-[10px] font-medium tabular-nums text-foreground-subtle",
20807
+ children: [Math.round(item.level), "%"]
20808
+ })]
20809
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
20810
+ level,
20811
+ size: "sm",
20812
+ className: "w-fit",
20813
+ label: item.status === "replace" ? t("uiLibrary.consumablesPanel.replace", "Replace") : void 0
20814
+ }),
20815
+ item.remaining && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20816
+ className: "text-[10px] font-medium tabular-nums text-foreground-subtle",
20817
+ children: formatRemaining(t, item.remaining)
20818
+ }),
20819
+ item.lastResetAt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20820
+ className: "text-[10px] text-foreground-subtle",
20821
+ children: t("uiLibrary.consumablesPanel.replacedOn", "Replaced {{date}}", { date: formatResetAt(item.lastResetAt) })
20822
+ }),
20823
+ item.resettable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
20680
20824
  type: "button",
20825
+ variant: "outline",
20826
+ size: "sm",
20827
+ className: "w-fit",
20681
20828
  onClick: () => onReset(item.key),
20682
20829
  disabled: busy || !canReset,
20683
- className: "rounded-md border border-border bg-surface px-2 py-0.5 text-[10px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
20684
20830
  children: t("uiLibrary.consumablesPanel.replaced", "Replaced")
20685
- })]
20686
- }),
20687
- item.level !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20831
+ })
20832
+ ]
20833
+ })
20834
+ });
20835
+ }
20836
+ /** A tank or a bin: a vertical gauge filled from the bottom, then the % or the verdict. */
20837
+ function GaugeRow({ item }) {
20838
+ const level = consumableItemLevel(item);
20839
+ const warn = needsAttention(level);
20840
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RowFrame, {
20841
+ item,
20842
+ warn,
20843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20844
+ className: "relative h-10 w-3 shrink-0 overflow-hidden rounded-sm border border-border bg-surface-hover",
20845
+ "data-testid": "consumable-gauge",
20846
+ children: item.level !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20847
+ className: `absolute inset-x-0 bottom-0 ${consumableLevelBarClass(level)} transition-all`,
20848
+ style: { height: `${clampPercent(item.level)}%` }
20849
+ })
20850
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20851
+ className: "flex min-w-0 flex-1 flex-col justify-center gap-1",
20852
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowLabel, {
20853
+ item,
20854
+ warn
20855
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20688
20856
  className: "flex items-center gap-2",
20689
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20690
- className: "h-1.5 flex-1 overflow-hidden rounded-full bg-surface-elevated/60",
20691
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20692
- className: `h-full rounded-full ${consumableLevelBarClass(level)} transition-all`,
20693
- style: { width: `${Math.max(0, Math.min(100, item.level))}%` }
20694
- })
20695
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
20857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
20858
+ level,
20859
+ size: "sm",
20860
+ className: "w-fit"
20861
+ }), item.level !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
20696
20862
  className: "text-[10px] font-medium tabular-nums text-foreground-subtle",
20697
20863
  children: [Math.round(item.level), "%"]
20698
20864
  })]
20699
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
20700
- level,
20701
- size: "sm",
20702
- className: "w-fit",
20703
- label: item.status === "replace" ? t("uiLibrary.consumablesPanel.replace", "Replace") : void 0
20704
- }),
20705
- item.remaining && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20706
- className: "text-[10px] font-medium tabular-nums text-foreground-subtle",
20707
- children: formatRemaining(t, item.remaining)
20708
- }),
20709
- item.lastResetAt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
20710
- className: "text-[10px] text-foreground-subtle",
20711
- children: t("uiLibrary.consumablesPanel.replacedOn", "Replaced {{date}}", { date: formatResetAt(item.lastResetAt) })
20712
- })
20713
- ]
20865
+ })]
20866
+ })]
20867
+ });
20868
+ }
20869
+ /** How many of the three steps a product's level fills. */
20870
+ var PRODUCT_STEPS = {
20871
+ full: 3,
20872
+ ok: 3,
20873
+ low: 1,
20874
+ empty: 0,
20875
+ unknown: 0
20876
+ };
20877
+ var STEP_INDEXES = [
20878
+ 0,
20879
+ 1,
20880
+ 2
20881
+ ];
20882
+ /** The refill wording a product's level reads as. */
20883
+ function productLabel(level, t) {
20884
+ if (level === "low") return t("uiLibrary.consumablesPanel.refillSoon", "Refill soon");
20885
+ if (level === "empty") return t("uiLibrary.consumablesPanel.refillNow", "Refill now");
20886
+ }
20887
+ /** A product bought and refilled: package icon, three steps, "refill". */
20888
+ function ProductRow({ item }) {
20889
+ const t = useUiText();
20890
+ const level = consumableItemLevel(item);
20891
+ const warn = needsAttention(level);
20892
+ const filled = PRODUCT_STEPS[level ?? "unknown"];
20893
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RowFrame, {
20894
+ item,
20895
+ warn,
20896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Package, { className: `mt-0.5 h-4 w-4 shrink-0 ${warn ? "text-warning" : "text-foreground-subtle"}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20897
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
20898
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowLabel, {
20899
+ item,
20900
+ warn
20901
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20902
+ className: "flex flex-wrap items-center gap-2",
20903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20904
+ className: "flex gap-0.5",
20905
+ "data-testid": "consumable-steps",
20906
+ "data-filled": filled,
20907
+ children: STEP_INDEXES.map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-3 rounded-full ${i < filled ? consumableLevelBarClass(level) : "bg-surface-hover"}` }, i))
20908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
20909
+ level,
20910
+ size: "sm",
20911
+ className: "whitespace-nowrap",
20912
+ label: productLabel(level, t)
20913
+ })]
20914
+ })]
20915
+ })]
20714
20916
  });
20715
20917
  }
20918
+ /** The row for an item, chosen by its form. */
20919
+ function ConsumableRow(props) {
20920
+ switch ((0, _camstack_types.consumableFormOf)(props.item)) {
20921
+ case "part": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PartRow, { ...props });
20922
+ case "tank":
20923
+ case "bin": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRow, { item: props.item });
20924
+ case "product": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProductRow, { item: props.item });
20925
+ }
20926
+ }
20927
+ //#endregion
20928
+ //#region src/composites/cap-settings/consumables-summary.ts
20929
+ var SEVERITY = {
20930
+ empty: 3,
20931
+ full: 2,
20932
+ low: 1
20933
+ };
20934
+ function isAttention(level) {
20935
+ return level === "low" || level === "empty" || level === "full";
20936
+ }
20937
+ function summarizeConsumables(items) {
20938
+ if (items.length === 0) return {
20939
+ kind: "none",
20940
+ needsAttention: false
20941
+ };
20942
+ const attention = [];
20943
+ let ok = 0;
20944
+ let unknown = 0;
20945
+ for (const item of items) {
20946
+ const level = consumableItemLevel(item);
20947
+ if (isAttention(level)) attention.push({
20948
+ label: item.label,
20949
+ level,
20950
+ replace: item.status === "replace"
20951
+ });
20952
+ else if (level === "ok") ok += 1;
20953
+ else unknown += 1;
20954
+ }
20955
+ const worst = [...attention].sort((a, b) => SEVERITY[b.level] - SEVERITY[a.level])[0];
20956
+ if (worst !== void 0) return {
20957
+ kind: "attention",
20958
+ needsAttention: true,
20959
+ count: attention.length,
20960
+ worst
20961
+ };
20962
+ return unknown === 0 ? {
20963
+ kind: "ok",
20964
+ needsAttention: false,
20965
+ count: ok
20966
+ } : {
20967
+ kind: "partial",
20968
+ needsAttention: false,
20969
+ ok,
20970
+ unknown
20971
+ };
20972
+ }
20973
+ //#endregion
20974
+ //#region src/composites/cap-settings/ConsumablesPanel.tsx
20975
+ /**
20976
+ * ConsumablesPanel — the ONE consumables surface of every device (D915): wear
20977
+ * parts, tanks (a vacuum's clean and dirty water), products (detergent, a
20978
+ * dishwasher's salt and rinse aid) and bins (a dust bin), for whichever device
20979
+ * is bound to the `consumables` capability. A cap-settings component
20980
+ * (ui-library), shown directly under the device page's hero card through the
20981
+ * cap-UI contribution mechanism (the cap declares
20982
+ * `ui: { kind:'widget', widgetId:'host/consumables-panel', placement:'hero', ... }`)
20983
+ * — never drawn by a device type's hero card itself.
20984
+ *
20985
+ * Reads the cap's pushed runtime-state slice via `useDeviceCapability` and
20986
+ * renders one row per item, drawn by its form (`consumable-rows.tsx`). A
20987
+ * slice that has not landed is a loading line, never "none reported" (D315).
20988
+ *
20989
+ * The section COLLAPSES smartly: it starts open while any item needs
20990
+ * attention (low, empty, a full bin or waste tank, a part to replace) and
20991
+ * closed while every item is ok or unknown; closed, its header carries a
20992
+ * one-line summary (`consumables-summary.ts`). The operator's own toggle wins
20993
+ * and is remembered per device and per viewer.
20994
+ */
20995
+ /** Where one viewer's open / closed choice for one device's section is kept. */
20996
+ function consumablesOpenStorageKey(deviceId) {
20997
+ return `camstack.device.${String(deviceId)}.consumables.open`;
20998
+ }
20999
+ /** The one line a collapsed section says. */
21000
+ function SummaryLine({ summary, t }) {
21001
+ switch (summary.kind) {
21002
+ case "none": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
21003
+ variant: "neutral",
21004
+ size: "sm",
21005
+ children: t("uiLibrary.consumablesPanel.noneReported", "None reported")
21006
+ });
21007
+ case "ok": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
21008
+ variant: "success",
21009
+ size: "sm",
21010
+ children: t("uiLibrary.consumablesPanel.allOk", "All OK")
21011
+ });
21012
+ case "partial": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
21013
+ variant: "neutral",
21014
+ size: "sm",
21015
+ children: t("uiLibrary.consumablesPanel.okAndUnknown", "{{ok}} OK · {{unknown}} unknown", {
21016
+ ok: summary.ok,
21017
+ unknown: summary.unknown
21018
+ })
21019
+ });
21020
+ case "attention": return summary.count === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
21021
+ level: summary.worst.level,
21022
+ name: summary.worst.label,
21023
+ size: "sm",
21024
+ label: summary.worst.replace ? t("uiLibrary.consumablesPanel.replace", "Replace") : void 0
21025
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
21026
+ variant: consumableLevelVariant(summary.worst.level),
21027
+ size: "sm",
21028
+ children: t("uiLibrary.consumablesPanel.needAttention", {
21029
+ one: "{{count}} needs attention",
21030
+ other: "{{count}} need attention"
21031
+ }, { count: summary.count })
21032
+ });
21033
+ }
21034
+ }
20716
21035
  function ConsumablesPanel({ deviceId }) {
20717
21036
  const t = useUiText();
20718
21037
  const system = useSystem();
@@ -20724,6 +21043,7 @@ function ConsumablesPanel({ deviceId }) {
20724
21043
  slice: (d) => d.state.consumables
20725
21044
  });
20726
21045
  const [busyKey, setBusyKey] = (0, react$1.useState)(null);
21046
+ const [choice, setChoice] = usePersistedDisclosure(ready ? consumablesOpenStorageKey(deviceId) : null);
20727
21047
  const onReset = (0, react$1.useCallback)(async (key) => {
20728
21048
  if (!cap) return;
20729
21049
  setBusyKey(key);
@@ -20735,17 +21055,30 @@ function ConsumablesPanel({ deviceId }) {
20735
21055
  setBusyKey(null);
20736
21056
  }
20737
21057
  }, [cap]);
20738
- const items = slice?.items ?? [];
21058
+ const summary = slice === void 0 ? null : summarizeConsumables(slice.items);
21059
+ const open = choice ?? summary?.needsAttention ?? false;
21060
+ const loading = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, {
21061
+ className: open ? "h-10 w-full" : "h-4 w-16",
21062
+ "data-testid": "consumables-loading",
21063
+ "aria-label": t("uiLibrary.consumablesPanel.loading", "Reading consumables…")
21064
+ });
20739
21065
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetPanel, {
20740
21066
  title: t("uiLibrary.consumablesPanel.title", "Consumables"),
20741
21067
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
20742
21068
  bodyClassName: "p-3 overflow-y-auto",
20743
- children: items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
21069
+ collapsible: true,
21070
+ open,
21071
+ onOpenChange: setChoice,
21072
+ headerRight: open ? void 0 : summary === null ? loading : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SummaryLine, {
21073
+ summary,
21074
+ t
21075
+ }),
21076
+ children: slice === void 0 ? loading : slice.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
20744
21077
  className: "text-[11px] text-foreground-subtle italic leading-relaxed",
20745
21078
  children: t("uiLibrary.consumablesPanel.empty", "No consumables reported.")
20746
21079
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20747
- className: "flex flex-col gap-2",
20748
- children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableRow, {
21080
+ className: "grid grid-cols-1 gap-2 sm:grid-cols-2",
21081
+ children: slice.items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableRow, {
20749
21082
  item,
20750
21083
  busy: busyKey === item.key,
20751
21084
  canReset: available,
@@ -20785,49 +21118,6 @@ function useCapabilityPresence(capabilityName) {
20785
21118
  return classifyCapability(query.data);
20786
21119
  }
20787
21120
  //#endregion
20788
- //#region src/primitives/button.tsx
20789
- var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
20790
- variants: {
20791
- variant: {
20792
- primary: "bg-primary text-primary-foreground hover:bg-primary/90",
20793
- secondary: "bg-surface text-foreground hover:bg-surface-hover",
20794
- ghost: "hover:bg-surface-hover text-foreground-muted",
20795
- danger: "bg-danger text-white hover:bg-danger/90",
20796
- outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
20797
- },
20798
- size: {
20799
- sm: "h-7 px-2.5 text-xs gap-1.5",
20800
- md: "h-8 px-3 text-sm gap-2",
20801
- lg: "h-9 px-4 text-sm gap-2"
20802
- }
20803
- },
20804
- defaultVariants: {
20805
- variant: "primary",
20806
- size: "sm"
20807
- }
20808
- });
20809
- var Button = (0, react$1.forwardRef)(({ className, variant, size, ...props }, ref) => {
20810
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
20811
- ref,
20812
- className: cn(buttonVariants({
20813
- variant,
20814
- size
20815
- }), className),
20816
- ...props
20817
- });
20818
- });
20819
- Button.displayName = "Button";
20820
- //#endregion
20821
- //#region src/primitives/skeleton.tsx
20822
- var Skeleton = (0, react$1.forwardRef)(({ className, ...props }, ref) => {
20823
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
20824
- ref,
20825
- className: cn("animate-pulse bg-surface-hover rounded-md h-4 w-full", className),
20826
- ...props
20827
- });
20828
- });
20829
- Skeleton.displayName = "Skeleton";
20830
- //#endregion
20831
21121
  //#region src/composites/cap-settings/recording-timeline-model.ts
20832
21122
  /** Tie-break precedence for a bucket's dominant kind (first wins on equal count). */
20833
21123
  var KIND_PRECEDENCE = [
@@ -47102,147 +47392,6 @@ function BatteryBadge$2({ level }) {
47102
47392
  });
47103
47393
  }
47104
47394
  /**
47105
- * Pure helper — partitions a tank kind into "supply" (low is bad) vs
47106
- * "waste" (full is bad) via an exhaustive switch. A future TankKind that
47107
- * forgets to classify itself fails the build (the `never` default arm),
47108
- * rather than silently defaulting to the waste branch.
47109
- */
47110
- function tankIsSupply(kind) {
47111
- switch (kind) {
47112
- case "cleanWater": return true;
47113
- case "detergent": return true;
47114
- case "dirtyWater": return false;
47115
- case "dustBin": return false;
47116
- default: return kind;
47117
- }
47118
- }
47119
- /**
47120
- * Pure helper — classifies a tank as `ok` or `warning`.
47121
- *
47122
- * Clean-water + detergent are "supply" tanks: low is bad. Dirty-water +
47123
- * dust-bin are "waste" tanks: full is bad. Prefers the numeric `level`
47124
- * (thresholds 15 / 85), falling back to the discrete `status` flag. Both
47125
- * unknown → `ok`. Exhaustive over TankKind.
47126
- */
47127
- function tankAlert(kind, tank) {
47128
- const isSupply = tankIsSupply(kind);
47129
- if (tank.level !== null) {
47130
- if (isSupply) return tank.level <= 15 ? "warning" : "ok";
47131
- return tank.level >= 85 ? "warning" : "ok";
47132
- }
47133
- if (tank.status !== null) {
47134
- if (isSupply) return tank.status === "low" ? "warning" : "ok";
47135
- return tank.status === "full" ? "warning" : "ok";
47136
- }
47137
- return "ok";
47138
- }
47139
- function tankMeta(t, kind) {
47140
- switch (kind) {
47141
- case "cleanWater": return {
47142
- label: t("uiLibrary.vacuum.cleanWater", "Clean water"),
47143
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, { className: "h-3.5 w-3.5" })
47144
- };
47145
- case "dirtyWater": return {
47146
- label: t("uiLibrary.vacuum.dirtyWater", "Dirty water"),
47147
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplets, { className: "h-3.5 w-3.5" })
47148
- };
47149
- case "detergent": return {
47150
- label: t("uiLibrary.vacuum.detergent", "Detergent"),
47151
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FlaskConical, { className: "h-3.5 w-3.5" })
47152
- };
47153
- case "dustBin": return {
47154
- label: t("uiLibrary.vacuum.dustBin", "Dust bin"),
47155
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
47156
- };
47157
- default: return kind;
47158
- }
47159
- }
47160
- /**
47161
- * TankIndicator — one tank row: label + glyph, then a fill bar (numeric
47162
- * level) or a status pill (discrete), tinted by `tankAlert`. Renders
47163
- * nothing only when the tank is absent (null). A PRESENT tank whose
47164
- * reading is unknown (both fields null) still renders a neutral row with
47165
- * an em-dash placeholder, so the user always sees which tanks the robot
47166
- * has (Dreame reports presence but no fill telemetry).
47167
- */
47168
- function TankIndicator({ kind, tank }) {
47169
- const t = useUiText();
47170
- if (tank === null) return null;
47171
- const meta = tankMeta(t, kind);
47172
- const warning = tankAlert(kind, tank) === "warning";
47173
- if (tank.level !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47174
- className: "flex items-center gap-2 text-xs",
47175
- children: [
47176
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47177
- className: cn("inline-flex items-center gap-1 w-24 shrink-0", warning ? "text-warning" : "text-foreground-subtle"),
47178
- children: [meta.icon, meta.label]
47179
- }),
47180
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47181
- className: "relative h-1.5 flex-1 overflow-hidden rounded-full bg-surface-hover",
47182
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47183
- className: cn("absolute inset-y-0 left-0 rounded-full transition-all", warning ? "bg-warning" : "bg-primary"),
47184
- style: { width: `${tank.level}%` }
47185
- })
47186
- }),
47187
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47188
- className: cn("tabular-nums w-9 text-right", warning ? "text-warning" : "text-foreground-subtle"),
47189
- children: [tank.level, "%"]
47190
- })
47191
- ]
47192
- });
47193
- if (tank.status !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47194
- className: "flex items-center gap-2 text-xs",
47195
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47196
- className: cn("inline-flex items-center gap-1 w-24 shrink-0", warning ? "text-warning" : "text-foreground-subtle"),
47197
- children: [meta.icon, meta.label]
47198
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
47199
- level: warning ? "low" : "ok",
47200
- label: tankStatusLabel(t, tank.status),
47201
- size: "sm"
47202
- })]
47203
- });
47204
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47205
- className: "flex items-center gap-2 text-xs",
47206
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47207
- className: "inline-flex items-center gap-1 w-24 shrink-0 text-foreground-subtle",
47208
- children: [meta.icon, meta.label]
47209
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
47210
- level: null,
47211
- size: "sm"
47212
- })]
47213
- });
47214
- }
47215
- /** A tank's discrete reading in words; the tone is `tankAlert`'s (a full waste tank warns, a full supply tank does not). */
47216
- function tankStatusLabel(t, status) {
47217
- switch (status) {
47218
- case "ok": return consumableLevelLabel("ok", t);
47219
- case "low": return consumableLevelLabel("low", t);
47220
- case "full": return t("uiLibrary.vacuum.tankFull", "Full");
47221
- }
47222
- }
47223
- /**
47224
- * TanksRow — compact 2-column grid of the present tanks for the vacuum
47225
- * hero. Absent tanks (null) are skipped; every PRESENT tank is shown, even
47226
- * when its reading is unknown (`{level:null,status:null}`) — it renders a
47227
- * neutral em-dash row. The row renders iff the robot has at least one tank.
47228
- */
47229
- function TanksRow({ cleanWater, dirtyWater, detergent, dustBin }) {
47230
- const tanks = [
47231
- ["cleanWater", cleanWater],
47232
- ["dirtyWater", dirtyWater],
47233
- ["detergent", detergent],
47234
- ["dustBin", dustBin]
47235
- ];
47236
- if (!tanks.some(([, t]) => t !== null)) return null;
47237
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47238
- className: "grid grid-cols-1 gap-x-6 gap-y-2 rounded-md border border-border bg-surface-hover/40 p-3 sm:grid-cols-2",
47239
- children: tanks.map(([kind, tank]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankIndicator, {
47240
- kind,
47241
- tank
47242
- }, kind))
47243
- });
47244
- }
47245
- /**
47246
47395
  * VacuumInlineControl — compact, state-aware vacuum controls for a
47247
47396
  * device-list row. Start / Pause / Return-to-base + battery badge.
47248
47397
  */
@@ -47324,114 +47473,105 @@ function VacuumHeroCard({ trpc, deviceId }) {
47324
47473
  };
47325
47474
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47326
47475
  className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
47327
- children: [
47328
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47329
- className: "flex items-center gap-6",
47330
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumGraphic, {
47331
- state,
47332
- batteryLevel: slice?.batteryLevel ?? null
47333
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47334
- className: "flex flex-col gap-3 flex-1",
47335
- children: [
47336
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47337
- className: "flex items-center justify-between",
47338
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47339
- className: cn("text-3xl font-semibold", meta.tone),
47340
- children: meta.label
47341
- }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$2, { level: slice.batteryLevel })]
47342
- }),
47343
- slice?.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47344
- className: "flex items-center gap-2",
47345
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47346
- className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
47347
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47348
- className: "h-full bg-primary transition-[width] duration-500",
47349
- style: { width: `${Math.max(0, Math.min(100, slice.progressPercent))}%` }
47350
- })
47351
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47352
- className: "tabular-nums text-xs text-foreground-subtle",
47353
- children: [Math.round(slice.progressPercent), "%"]
47354
- })]
47355
- }),
47356
- slice?.errorCode != null && slice.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47357
- className: "inline-flex items-center gap-1 text-xs text-destructive",
47358
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: slice.errorLabel ?? t("uiLibrary.vacuum.errorCode", "Error {{code}}", { code: slice.errorCode }) })]
47359
- }),
47360
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47361
- className: "flex items-center gap-2",
47362
- children: [
47363
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47364
- label: t("uiLibrary.vacuum.start", "Start"),
47365
- busy,
47366
- active: state === "cleaning",
47367
- onClick: () => press(cap.start),
47368
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-5 w-5" })
47369
- }),
47370
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47371
- label: t("uiLibrary.vacuum.pause", "Pause"),
47372
- busy,
47373
- active: state === "paused",
47374
- onClick: () => press(cap.pause),
47375
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-5 w-5" })
47376
- }),
47377
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47378
- label: t("uiLibrary.vacuum.stop", "Stop"),
47379
- busy,
47380
- active: false,
47381
- onClick: () => press(cap.stop),
47382
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-4 w-4" })
47383
- }),
47384
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47385
- label: t("uiLibrary.vacuum.returnToBase", "Return to base"),
47386
- busy,
47387
- active: state === "returning" || state === "docked",
47388
- onClick: () => press(cap.returnToBase),
47389
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: "h-5 w-5" })
47390
- }),
47391
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47392
- label: t("uiLibrary.vacuum.locate", "Locate"),
47393
- busy,
47394
- active: false,
47395
- onClick: () => press(cap.locate),
47396
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Megaphone, { className: "h-5 w-5" })
47397
- })
47398
- ]
47399
- })
47400
- ]
47401
- })]
47402
- }),
47403
- fanSpeeds.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
47404
- className: "flex items-center gap-2 text-xs text-foreground-subtle",
47405
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47406
- className: "w-16 shrink-0",
47407
- children: t("uiLibrary.vacuum.fanSpeed", "Fan speed")
47408
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
47409
- "aria-label": t("uiLibrary.vacuum.fanSpeed", "Fan speed"),
47410
- value: slice?.fanSpeed ?? "",
47411
- disabled: busy,
47412
- onChange: (e) => {
47413
- onFanSpeed(e.target.value);
47414
- },
47415
- onClick: (e) => {
47416
- e.stopPropagation();
47417
- },
47418
- className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
47419
- children: [slice?.fanSpeed == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
47420
- value: "",
47421
- children: "—"
47422
- }), fanSpeeds.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
47423
- value: s,
47424
- children: s
47425
- }, s))]
47426
- })]
47427
- }),
47428
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TanksRow, {
47429
- cleanWater: slice?.cleanWater ?? null,
47430
- dirtyWater: slice?.dirtyWater ?? null,
47431
- detergent: slice?.detergent ?? null,
47432
- dustBin: slice?.dustBin ?? null
47433
- })
47434
- ]
47476
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47477
+ className: "flex items-center gap-6",
47478
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumGraphic, {
47479
+ state,
47480
+ batteryLevel: slice?.batteryLevel ?? null
47481
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47482
+ className: "flex flex-col gap-3 flex-1",
47483
+ children: [
47484
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47485
+ className: "flex items-center justify-between",
47486
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47487
+ className: cn("text-3xl font-semibold", meta.tone),
47488
+ children: meta.label
47489
+ }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$2, { level: slice.batteryLevel })]
47490
+ }),
47491
+ slice?.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47492
+ className: "flex items-center gap-2",
47493
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47494
+ className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
47495
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47496
+ className: "h-full bg-primary transition-[width] duration-500",
47497
+ style: { width: `${Math.max(0, Math.min(100, slice.progressPercent))}%` }
47498
+ })
47499
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47500
+ className: "tabular-nums text-xs text-foreground-subtle",
47501
+ children: [Math.round(slice.progressPercent), "%"]
47502
+ })]
47503
+ }),
47504
+ slice?.errorCode != null && slice.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47505
+ className: "inline-flex items-center gap-1 text-xs text-destructive",
47506
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: slice.errorLabel ?? t("uiLibrary.vacuum.errorCode", "Error {{code}}", { code: slice.errorCode }) })]
47507
+ }),
47508
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47509
+ className: "flex items-center gap-2",
47510
+ children: [
47511
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47512
+ label: t("uiLibrary.vacuum.start", "Start"),
47513
+ busy,
47514
+ active: state === "cleaning",
47515
+ onClick: () => press(cap.start),
47516
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-5 w-5" })
47517
+ }),
47518
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47519
+ label: t("uiLibrary.vacuum.pause", "Pause"),
47520
+ busy,
47521
+ active: state === "paused",
47522
+ onClick: () => press(cap.pause),
47523
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-5 w-5" })
47524
+ }),
47525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47526
+ label: t("uiLibrary.vacuum.stop", "Stop"),
47527
+ busy,
47528
+ active: false,
47529
+ onClick: () => press(cap.stop),
47530
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-4 w-4" })
47531
+ }),
47532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47533
+ label: t("uiLibrary.vacuum.returnToBase", "Return to base"),
47534
+ busy,
47535
+ active: state === "returning" || state === "docked",
47536
+ onClick: () => press(cap.returnToBase),
47537
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: "h-5 w-5" })
47538
+ }),
47539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VacuumHeroButton, {
47540
+ label: t("uiLibrary.vacuum.locate", "Locate"),
47541
+ busy,
47542
+ active: false,
47543
+ onClick: () => press(cap.locate),
47544
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Megaphone, { className: "h-5 w-5" })
47545
+ })
47546
+ ]
47547
+ })
47548
+ ]
47549
+ })]
47550
+ }), fanSpeeds.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
47551
+ className: "flex items-center gap-2 text-xs text-foreground-subtle",
47552
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
47553
+ className: "w-16 shrink-0",
47554
+ children: t("uiLibrary.vacuum.fanSpeed", "Fan speed")
47555
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
47556
+ "aria-label": t("uiLibrary.vacuum.fanSpeed", "Fan speed"),
47557
+ value: slice?.fanSpeed ?? "",
47558
+ disabled: busy,
47559
+ onChange: (e) => {
47560
+ onFanSpeed(e.target.value);
47561
+ },
47562
+ onClick: (e) => {
47563
+ e.stopPropagation();
47564
+ },
47565
+ className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
47566
+ children: [slice?.fanSpeed == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
47567
+ value: "",
47568
+ children: "—"
47569
+ }), fanSpeeds.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
47570
+ value: s,
47571
+ children: s
47572
+ }, s))]
47573
+ })]
47574
+ })]
47435
47575
  });
47436
47576
  }
47437
47577
  //#endregion
@@ -50359,6 +50499,9 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
50359
50499
  * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
50360
50500
  * mirroring vacuum-control / pet-feeder patterns.
50361
50501
  *
50502
+ * Salt and rinse aid are not here: they are items of the device's `consumables`
50503
+ * cap, shown by the page's Consumables section like every device's (D915).
50504
+ *
50362
50505
  * UNKNOWN IS NOT OFF. The slice carries `null` for everything the appliance has not
50363
50506
  * reported, and this view renders that as "—" / a disabled control, never as an unchecked
50364
50507
  * box or a zero. It also does not echo a command before the appliance confirms it: the
@@ -50492,60 +50635,44 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
50492
50635
  if (busy) return;
50493
50636
  fn();
50494
50637
  };
50495
- if (layout === "compact") {
50496
- const saltWarn = display.saltLevel === "low" || display.saltLevel === "empty";
50497
- const rinseWarn = display.rinseAidLevel === "low" || display.rinseAidLevel === "empty";
50498
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50499
- className: "inline-flex items-center gap-1.5 max-w-full",
50500
- children: [
50501
- display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50502
- className: "inline-flex text-warning shrink-0",
50503
- title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
50504
- "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
50505
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
50506
- }),
50507
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50508
- className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
50509
- children: meta.label
50510
- }),
50511
- display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50512
- className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
50513
- title: display.program,
50514
- children: featureLabel(display.program)
50515
- }),
50516
- display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50517
- className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
50518
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remainingSeconds, t)]
50519
- }),
50520
- saltWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
50521
- level: display.saltLevel,
50522
- name: t("uiLibrary.dishwasher.salt", "Salt"),
50523
- size: "sm",
50524
- className: "shrink-0"
50525
- }),
50526
- rinseWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
50527
- level: display.rinseAidLevel,
50528
- name: t("uiLibrary.dishwasher.rinseAid", "Rinse aid"),
50529
- size: "sm",
50530
- className: "shrink-0"
50531
- }),
50532
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
50533
- label: t("uiLibrary.dishwasher.start", "Start"),
50534
- busy: busy || !startable,
50535
- active: running,
50536
- onClick: () => press(() => cap.start({})),
50537
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
50538
- }),
50539
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
50540
- label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
50541
- busy: busy || !(running || paused),
50542
- active: paused,
50543
- onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
50544
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
50545
- })
50546
- ]
50547
- });
50548
- }
50638
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50639
+ className: "inline-flex items-center gap-1.5 max-w-full",
50640
+ children: [
50641
+ display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50642
+ className: "inline-flex text-warning shrink-0",
50643
+ title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
50644
+ "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
50645
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
50646
+ }),
50647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50648
+ className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
50649
+ children: meta.label
50650
+ }),
50651
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50652
+ className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
50653
+ title: display.program,
50654
+ children: featureLabel(display.program)
50655
+ }),
50656
+ display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50657
+ className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
50658
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remainingSeconds, t)]
50659
+ }),
50660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
50661
+ label: t("uiLibrary.dishwasher.start", "Start"),
50662
+ busy: busy || !startable,
50663
+ active: running,
50664
+ onClick: () => press(() => cap.start({})),
50665
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
50666
+ }),
50667
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
50668
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
50669
+ busy: busy || !(running || paused),
50670
+ active: paused,
50671
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
50672
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
50673
+ })
50674
+ ]
50675
+ });
50549
50676
  const programs = display.availablePrograms;
50550
50677
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50551
50678
  className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
@@ -50596,25 +50723,14 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
50596
50723
  }),
50597
50724
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50598
50725
  className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
50599
- children: [
50600
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
50601
- name: t("uiLibrary.dishwasher.salt", "Salt"),
50602
- level: display.saltLevel
50603
- }),
50604
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableLevelBadge, {
50605
- name: t("uiLibrary.dishwasher.rinseAid", "Rinse aid"),
50606
- level: display.rinseAidLevel
50607
- }),
50608
- display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50609
- className: "inline-flex items-center gap-1 text-xs text-warning",
50610
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
50611
- }),
50612
- display.activeEvents.map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50613
- className: "text-xs text-warning",
50614
- title: event,
50615
- children: featureLabel(event)
50616
- }, event))
50617
- ]
50726
+ children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50727
+ className: "inline-flex items-center gap-1 text-xs text-warning",
50728
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
50729
+ }), display.activeEvents.map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50730
+ className: "text-xs text-warning",
50731
+ title: event,
50732
+ children: featureLabel(event)
50733
+ }, event))]
50618
50734
  }),
50619
50735
  display.operationState === "Error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50620
50736
  className: "inline-flex items-center gap-1 text-xs text-danger",
@@ -68253,7 +68369,6 @@ exports.FilterBar = FilterBar;
68253
68369
  exports.Flame = Flame;
68254
68370
  exports.Flashlight = Flashlight;
68255
68371
  exports.FlashlightOff = FlashlightOff;
68256
- exports.FlaskConical = FlaskConical;
68257
68372
  exports.FloatingEventStream = FloatingEventStream;
68258
68373
  exports.FloatingLogStream = FloatingLogStream;
68259
68374
  exports.FloatingPanel = FloatingPanel;
@@ -68945,7 +69060,6 @@ exports.summarizeHubCandidates = summarizeHubCandidates;
68945
69060
  exports.swatchFillClass = swatchFillClass;
68946
69061
  exports.swatchLineClass = swatchLineClass;
68947
69062
  exports.tabForSelector = tabForSelector;
68948
- exports.tankAlert = tankAlert;
68949
69063
  exports.themeToCss = require_theme_to_root_css.themeToCss;
68950
69064
  exports.themeToRootCss = require_theme_to_root_css.themeToRootCss;
68951
69065
  exports.timeInState = timeInState;
@@ -69211,9 +69325,11 @@ exports.useDeviceExtensionListSlots = useDeviceExtensionListSlots;
69211
69325
  exports.useDeviceExtensionListSources = useDeviceExtensionListSources;
69212
69326
  exports.useDeviceExtensionSetSlot = useDeviceExtensionSetSlot;
69213
69327
  exports.useDeviceHistoryDeclareSeries = useDeviceHistoryDeclareSeries;
69328
+ exports.useDeviceHistoryDeleteOrphanSeries = useDeviceHistoryDeleteOrphanSeries;
69214
69329
  exports.useDeviceHistoryGetDeviceEnabled = useDeviceHistoryGetDeviceEnabled;
69215
69330
  exports.useDeviceHistoryGetStatus = useDeviceHistoryGetStatus;
69216
69331
  exports.useDeviceHistoryListNodeSeries = useDeviceHistoryListNodeSeries;
69332
+ exports.useDeviceHistoryListOrphanSeries = useDeviceHistoryListOrphanSeries;
69217
69333
  exports.useDeviceHistoryListSeries = useDeviceHistoryListSeries;
69218
69334
  exports.useDeviceHistoryQuery = useDeviceHistoryQuery;
69219
69335
  exports.useDeviceHistoryQueryMany = useDeviceHistoryQueryMany;
@@ -69666,6 +69782,7 @@ exports.useOsdManagerSetSlotCameraEnabled = useOsdManagerSetSlotCameraEnabled;
69666
69782
  exports.useOsdManagerUpsertTemplate = useOsdManagerUpsertTemplate;
69667
69783
  exports.useOsdSetOverlay = useOsdSetOverlay;
69668
69784
  exports.usePTZ = usePTZ;
69785
+ exports.usePersistedDisclosure = usePersistedDisclosure;
69669
69786
  exports.usePersistedPageSize = usePersistedPageSize;
69670
69787
  exports.usePetFeederCallPet = usePetFeederCallPet;
69671
69788
  exports.usePetFeederCancelFeed = usePetFeederCancelFeed;