@camstack/ui-library 1.2.349 → 1.2.350

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
@@ -1706,6 +1706,19 @@ var EllipsisVertical = require_createLucideIcon.createLucideIcon("ellipsis-verti
1706
1706
  * This source code is licensed under the ISC license.
1707
1707
  * See the LICENSE file in the root directory of this source tree.
1708
1708
  */
1709
+ var Eraser = require_createLucideIcon.createLucideIcon("eraser", [["path", {
1710
+ d: "M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21",
1711
+ key: "g5wo59"
1712
+ }], ["path", {
1713
+ d: "m5.082 11.09 8.828 8.828",
1714
+ key: "1wx5vj"
1715
+ }]]);
1716
+ /**
1717
+ * @license lucide-react v0.576.0 - ISC
1718
+ *
1719
+ * This source code is licensed under the ISC license.
1720
+ * See the LICENSE file in the root directory of this source tree.
1721
+ */
1709
1722
  var ExternalLink = require_createLucideIcon.createLucideIcon("external-link", [
1710
1723
  ["path", {
1711
1724
  d: "M15 3h6v6",
@@ -5400,6 +5413,11 @@ var DEVICE_TYPE_META = {
5400
5413
  labelKey: "uiLibrary.deviceMeta.type.petFeeder",
5401
5414
  labelFallback: "Pet feeder"
5402
5415
  },
5416
+ [_camstack_types.DeviceType.PetFountain]: {
5417
+ icon: Droplets,
5418
+ labelKey: "uiLibrary.deviceMeta.type.petFountain",
5419
+ labelFallback: "Pet fountain"
5420
+ },
5403
5421
  [_camstack_types.DeviceType.Dishwasher]: {
5404
5422
  icon: Waves,
5405
5423
  labelKey: "uiLibrary.deviceMeta.type.dishwasher",
@@ -5900,7 +5918,7 @@ function lucideExportName(key) {
5900
5918
  function loadIconGlyphs() {
5901
5919
  if (fullTable !== void 0) return Promise.resolve();
5902
5920
  if (failure$1 !== void 0) return Promise.reject(failure$1);
5903
- pending ??= Promise.resolve().then(() => require("./icons-CWmwnulO.cjs")).then((module) => {
5921
+ pending ??= Promise.resolve().then(() => require("./icons-BJpFQcLH.cjs")).then((module) => {
5904
5922
  fullTable = module;
5905
5923
  publish();
5906
5924
  }, (error) => {
@@ -11026,6 +11044,14 @@ var useDeviceExportListExposedDevices = trpc.deviceExport.listExposedDevices.use
11026
11044
  var useDeviceExportExposeDevice = trpc.deviceExport.exposeDevice.useMutation;
11027
11045
  /** Generated alias around `trpc.deviceExport.unexposeDevice.useMutation`. */
11028
11046
  var useDeviceExportUnexposeDevice = trpc.deviceExport.unexposeDevice.useMutation;
11047
+ /** Generated alias around `trpc.deviceExport.listSupportedEntityKinds.useQuery`. */
11048
+ var useDeviceExportListSupportedEntityKinds = trpc.deviceExport.listSupportedEntityKinds.useQuery;
11049
+ /** Generated alias around `trpc.deviceExport.listExposedEntities.useQuery`. */
11050
+ var useDeviceExportListExposedEntities = trpc.deviceExport.listExposedEntities.useQuery;
11051
+ /** Generated alias around `trpc.deviceExport.exposeEntity.useMutation`. */
11052
+ var useDeviceExportExposeEntity = trpc.deviceExport.exposeEntity.useMutation;
11053
+ /** Generated alias around `trpc.deviceExport.unexposeEntity.useMutation`. */
11054
+ var useDeviceExportUnexposeEntity = trpc.deviceExport.unexposeEntity.useMutation;
11029
11055
  /** Generated alias around `trpc.deviceExport.getDeviceSettingsContribution.useQuery`. */
11030
11056
  var useDeviceExportGetDeviceSettingsContribution = trpc.deviceExport.getDeviceSettingsContribution.useQuery;
11031
11057
  /** Generated alias around `trpc.deviceExport.getDeviceLiveContribution.useQuery`. */
@@ -11928,6 +11954,18 @@ var usePetFeederSetFeedSound = trpc.petFeeder.setFeedSound.useMutation;
11928
11954
  var usePetFeederSetVolume = trpc.petFeeder.setVolume.useMutation;
11929
11955
  /** Generated alias around `trpc.petFeeder.getStatus.useQuery`. */
11930
11956
  var usePetFeederGetStatus = trpc.petFeeder.getStatus.useQuery;
11957
+ /** Generated alias around `trpc.petFountain.setPump.useMutation`. */
11958
+ var usePetFountainSetPump = trpc.petFountain.setPump.useMutation;
11959
+ /** Generated alias around `trpc.petFountain.setPumpInterval.useMutation`. */
11960
+ var usePetFountainSetPumpInterval = trpc.petFountain.setPumpInterval.useMutation;
11961
+ /** Generated alias around `trpc.petFountain.setPumpOnMinutes.useMutation`. */
11962
+ var usePetFountainSetPumpOnMinutes = trpc.petFountain.setPumpOnMinutes.useMutation;
11963
+ /** Generated alias around `trpc.petFountain.setPumpOffMinutes.useMutation`. */
11964
+ var usePetFountainSetPumpOffMinutes = trpc.petFountain.setPumpOffMinutes.useMutation;
11965
+ /** Generated alias around `trpc.petFountain.resetDailyConsumption.useMutation`. */
11966
+ var usePetFountainResetDailyConsumption = trpc.petFountain.resetDailyConsumption.useMutation;
11967
+ /** Generated alias around `trpc.petFountain.getStatus.useQuery`. */
11968
+ var usePetFountainGetStatus = trpc.petFountain.getStatus.useQuery;
11931
11969
  /** Generated alias around `trpc.pipelineAnalytics.getActiveTracks.useQuery`. */
11932
11970
  var usePipelineAnalyticsGetActiveTracks = trpc.pipelineAnalytics.getActiveTracks.useQuery;
11933
11971
  /** Generated alias around `trpc.pipelineAnalytics.getTrack.useQuery`. */
@@ -48791,6 +48829,7 @@ var CONTROL_CAP_NAMES = [
48791
48829
  "media-player",
48792
48830
  "control",
48793
48831
  "pet-feeder",
48832
+ "pet-fountain",
48794
48833
  "dishwasher"
48795
48834
  ];
48796
48835
  //#endregion
@@ -53081,6 +53120,10 @@ var HOLD_DELAY_MS = 350;
53081
53120
  var HOLD_INTERVAL_MS = 70;
53082
53121
  var HOLD_ACCEL_AFTER = 8;
53083
53122
  var HOLD_FAST_STEP = 5;
53123
+ /** The slice stores the bowl as a mass in kilograms (D944); the card draws grams. */
53124
+ function bowlGrams(kilograms) {
53125
+ return kilograms === null ? null : Math.round(kilograms * 1e3);
53126
+ }
53084
53127
  function clampPortion(grams) {
53085
53128
  return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
53086
53129
  }
@@ -53132,7 +53175,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53132
53175
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UtensilsCrossed, {
53133
53176
  className: "h-3.5 w-3.5",
53134
53177
  "aria-hidden": true
53135
- }), display.bowlFood ?? "—"]
53178
+ }), bowlGrams(display.bowlFood) ?? "—"]
53136
53179
  }),
53137
53180
  onMains && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53138
53181
  className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle",
@@ -53178,7 +53221,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53178
53221
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53179
53222
  className: "flex items-center gap-4 rounded-lg border border-border/50 bg-surface-subtle/40 p-3",
53180
53223
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BowlGauge, {
53181
- grams: display.bowlFood,
53224
+ grams: bowlGrams(display.bowlFood),
53182
53225
  low: display.lowFood,
53183
53226
  eating: display.eating
53184
53227
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -53188,7 +53231,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53188
53231
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53189
53232
  className: "text-3xl font-bold leading-none tabular-nums",
53190
53233
  style: { color: display.lowFood ? KIBBLE_LOW : KIBBLE },
53191
- children: display.bowlFood ?? "—"
53234
+ children: bowlGrams(display.bowlFood) ?? "—"
53192
53235
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53193
53236
  className: "text-sm text-foreground-subtle",
53194
53237
  children: t("uiLibrary.petFeeder.inBowl", "in bowl")
@@ -53625,6 +53668,367 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
53625
53668
  });
53626
53669
  }
53627
53670
  //#endregion
53671
+ //#region src/composites/device-controls/pet-fountain-panel.tsx
53672
+ /**
53673
+ * Pet-fountain controls for a `DeviceType.PetFountain` device.
53674
+ *
53675
+ * Reads the `pet-fountain` cap status slice (water level and volume, empty,
53676
+ * pump and its duty cycle, the water drunk) and renders:
53677
+ * - PetFountainInlineControl (device-list row): the tank level and a pump
53678
+ * on/off toggle.
53679
+ * - PetFountainHeroCard (device-detail hero): the tank gauge with the level
53680
+ * and the volume first, what the pet drank today / in 24 h / this hour /
53681
+ * last time, the pump switch, its duty-cycle switch and the run / rest
53682
+ * minutes. The filter is a consumable: the page's Consumables section
53683
+ * draws it, never this card (D915).
53684
+ *
53685
+ * Every reading may be unknown (`null`) and is drawn as a dash, never as
53686
+ * zero. Stored volumes are litres and durations seconds (D944); the card
53687
+ * shows millilitres and minutes. Actions are gated on `available`.
53688
+ */
53689
+ var LITRES_TO_ML = 1e3;
53690
+ var SECONDS_PER_MINUTE = 60;
53691
+ var MAX_PUMP_MINUTES = 240;
53692
+ function millilitres(litres) {
53693
+ return litres === null ? "—" : `${Math.round(litres * LITRES_TO_ML)} mL`;
53694
+ }
53695
+ function minutesOf(seconds) {
53696
+ return seconds === null ? null : Math.round(seconds / SECONDS_PER_MINUTE);
53697
+ }
53698
+ function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
53699
+ const t = useUiText();
53700
+ const { cap, slice, available } = useDeviceCapability({
53701
+ trpc,
53702
+ deviceId,
53703
+ cap: (d) => d.petFountain,
53704
+ slice: (d) => d.state.petFountain
53705
+ });
53706
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
53707
+ if (cap === void 0 || display === void 0) return null;
53708
+ const busy = !available;
53709
+ const fault = display.error !== null && display.error !== "no_error" ? display.error : null;
53710
+ const pumpOn = display.pumpRunning === true;
53711
+ const level = display.waterLevel;
53712
+ const setPump = (on) => {
53713
+ applyOptimistic({ pumpRunning: on });
53714
+ cap.setPump({ on });
53715
+ };
53716
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53717
+ className: "inline-flex items-center gap-2",
53718
+ children: [
53719
+ (fault !== null || display.empty === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
53720
+ className: "h-3.5 w-3.5 text-danger",
53721
+ "aria-label": fault ?? t("uiLibrary.petFountain.empty", "Tank empty")
53722
+ }),
53723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53724
+ className: cn("inline-flex items-center gap-1 text-xs font-medium tabular-nums", display.empty === true ? "text-danger" : "text-foreground"),
53725
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, {
53726
+ className: "h-3.5 w-3.5",
53727
+ "aria-hidden": true
53728
+ }), level === null ? "—" : `${Math.round(level)}%`]
53729
+ }),
53730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53731
+ onClick: (e) => e.stopPropagation(),
53732
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
53733
+ checked: pumpOn,
53734
+ disabled: busy,
53735
+ "aria-label": t("uiLibrary.petFountain.pump", "Pump"),
53736
+ onCheckedChange: setPump
53737
+ })
53738
+ })
53739
+ ]
53740
+ });
53741
+ const intervalOn = display.pumpIntervalEnabled === true;
53742
+ const onMinutes = minutesOf(display.pumpOnTime);
53743
+ const offMinutes = minutesOf(display.pumpOffTime);
53744
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53745
+ className: "flex flex-col items-stretch gap-3 rounded-lg border border-border bg-surface p-4",
53746
+ children: [
53747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53748
+ className: "flex items-center gap-4 rounded-lg border border-border-subtle bg-background-elevated p-3",
53749
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankGauge, {
53750
+ level,
53751
+ empty: display.empty === true
53752
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53753
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
53754
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53755
+ className: "flex items-baseline gap-2",
53756
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53757
+ className: cn("text-3xl font-bold leading-none tabular-nums", display.empty === true ? "text-danger" : "text-info"),
53758
+ children: level === null ? "—" : `${Math.round(level)}%`
53759
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53760
+ className: "text-sm tabular-nums text-foreground-muted",
53761
+ children: millilitres(display.waterVolume)
53762
+ })]
53763
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusLine, {
53764
+ empty: display.empty,
53765
+ pumpRunning: display.pumpRunning
53766
+ })]
53767
+ })]
53768
+ }),
53769
+ fault !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53770
+ className: "flex items-start gap-2 rounded-lg border border-danger/40 bg-danger/10 px-3 py-2 text-xs font-medium text-danger",
53771
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
53772
+ className: "mt-px h-4 w-4 shrink-0",
53773
+ "aria-hidden": true
53774
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53775
+ className: "min-w-0 break-words",
53776
+ children: fault
53777
+ })]
53778
+ }),
53779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section$1, {
53780
+ label: t("uiLibrary.petFountain.consumption", "Water drunk"),
53781
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53782
+ className: "grid grid-cols-2 gap-2 sm:grid-cols-4",
53783
+ children: [
53784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53785
+ label: t("uiLibrary.petFountain.today", "Today"),
53786
+ value: display.todayDrink
53787
+ }),
53788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53789
+ label: t("uiLibrary.petFountain.last24h", "Last 24 h"),
53790
+ value: display.last24hDrink
53791
+ }),
53792
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53793
+ label: t("uiLibrary.petFountain.thisHour", "This hour"),
53794
+ value: display.currentHourDrink
53795
+ }),
53796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53797
+ label: t("uiLibrary.petFountain.lastDrink", "Last drink"),
53798
+ value: display.lastDrink
53799
+ })
53800
+ ]
53801
+ })
53802
+ }),
53803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section$1, {
53804
+ label: t("uiLibrary.petFountain.pumpSection", "Controls"),
53805
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53806
+ className: "flex flex-col gap-2",
53807
+ children: [
53808
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
53809
+ icon: pumpOn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" }),
53810
+ label: t("uiLibrary.petFountain.pump", "Pump"),
53811
+ checked: pumpOn,
53812
+ disabled: busy,
53813
+ onChange: setPump
53814
+ }),
53815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
53816
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-4 w-4" }),
53817
+ label: t("uiLibrary.petFountain.cycle", "On / off cycle"),
53818
+ checked: intervalOn,
53819
+ disabled: busy,
53820
+ onChange: (on) => {
53821
+ applyOptimistic({ pumpIntervalEnabled: on });
53822
+ cap.setPumpInterval({ on });
53823
+ }
53824
+ }),
53825
+ intervalOn && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53826
+ className: "flex flex-col gap-2",
53827
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinutesStepper, {
53828
+ label: t("uiLibrary.petFountain.runMinutes", "Runs for"),
53829
+ minutes: onMinutes,
53830
+ disabled: busy,
53831
+ onChange: (minutes) => {
53832
+ applyOptimistic({ pumpOnTime: minutes * SECONDS_PER_MINUTE });
53833
+ cap.setPumpOnMinutes({ minutes });
53834
+ }
53835
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinutesStepper, {
53836
+ label: t("uiLibrary.petFountain.restMinutes", "Rests for"),
53837
+ minutes: offMinutes,
53838
+ disabled: busy,
53839
+ onChange: (minutes) => {
53840
+ applyOptimistic({ pumpOffTime: minutes * SECONDS_PER_MINUTE });
53841
+ cap.setPumpOffMinutes({ minutes });
53842
+ }
53843
+ })]
53844
+ })
53845
+ ]
53846
+ })
53847
+ }),
53848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
53849
+ variant: "outline",
53850
+ size: "sm",
53851
+ disabled: busy,
53852
+ onClick: () => void cap.resetDailyConsumption({}),
53853
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eraser, {
53854
+ className: "h-4 w-4",
53855
+ "aria-hidden": true
53856
+ }), t("uiLibrary.petFountain.clearDaily", "Clear daily count")]
53857
+ })
53858
+ ]
53859
+ });
53860
+ }
53861
+ /** A tank outline whose water rises with the level; danger-coloured when empty. */
53862
+ function TankGauge({ level, empty }) {
53863
+ const t = useUiText();
53864
+ const top = 58 - Math.min(1, Math.max(0, (level ?? 0) / 100)) * 40;
53865
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
53866
+ width: 72,
53867
+ height: 72,
53868
+ viewBox: "0 0 72 72",
53869
+ role: "img",
53870
+ "aria-label": level === null ? t("uiLibrary.petFountain.levelUnknown", "Water level unknown") : t("uiLibrary.petFountain.levelPercent", "{{percent}}% water", { percent: Math.round(level) }),
53871
+ className: "shrink-0",
53872
+ children: [
53873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("clipPath", {
53874
+ id: "pfn-tank-clip",
53875
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53876
+ x: "18",
53877
+ y: "14",
53878
+ width: "36",
53879
+ height: "46",
53880
+ rx: "6"
53881
+ })
53882
+ }) }),
53883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
53884
+ clipPath: "url(#pfn-tank-clip)",
53885
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53886
+ x: "18",
53887
+ y: top,
53888
+ width: "36",
53889
+ height: "46",
53890
+ className: empty ? "fill-danger" : "fill-info",
53891
+ opacity: .75
53892
+ })
53893
+ }),
53894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53895
+ x: "18",
53896
+ y: "14",
53897
+ width: "36",
53898
+ height: "46",
53899
+ rx: "6",
53900
+ fill: "none",
53901
+ strokeWidth: "2.5",
53902
+ className: "stroke-border"
53903
+ }),
53904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53905
+ x: "28",
53906
+ y: "8",
53907
+ width: "16",
53908
+ height: "6",
53909
+ rx: "2",
53910
+ className: "fill-border"
53911
+ })
53912
+ ]
53913
+ });
53914
+ }
53915
+ function StatusLine({ empty, pumpRunning }) {
53916
+ const t = useUiText();
53917
+ if (empty === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53918
+ className: "inline-flex items-center gap-1 text-xs font-medium text-danger",
53919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, {
53920
+ className: "h-3.5 w-3.5",
53921
+ "aria-hidden": true
53922
+ }), t("uiLibrary.petFountain.empty", "Tank empty")]
53923
+ });
53924
+ if (pumpRunning === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53925
+ className: "inline-flex items-center gap-1 text-xs font-medium text-info",
53926
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplets, {
53927
+ className: "h-3.5 w-3.5",
53928
+ "aria-hidden": true
53929
+ }), t("uiLibrary.petFountain.pumpRunning", "Pump running")]
53930
+ });
53931
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53932
+ className: "inline-flex items-center gap-1 text-xs text-foreground-subtle",
53933
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, {
53934
+ className: "h-3.5 w-3.5",
53935
+ "aria-hidden": true
53936
+ }), pumpRunning === false ? t("uiLibrary.petFountain.pumpIdle", "Pump idle") : t("uiLibrary.petFountain.pumpUnknown", "Pump state unknown")]
53937
+ });
53938
+ }
53939
+ function Section$1({ label, children }) {
53940
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53941
+ className: "flex flex-col gap-1",
53942
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53943
+ className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
53944
+ children: label
53945
+ }), children]
53946
+ });
53947
+ }
53948
+ function Stat({ label, value }) {
53949
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53950
+ className: "flex flex-col rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
53951
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53952
+ className: "text-[11px] text-foreground-subtle",
53953
+ children: label
53954
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53955
+ className: "text-sm font-semibold tabular-nums text-foreground",
53956
+ children: millilitres(value)
53957
+ })]
53958
+ });
53959
+ }
53960
+ function SwitchRow({ icon, label, checked, disabled, onChange }) {
53961
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53962
+ className: "flex items-center justify-between gap-3 rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
53963
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53964
+ className: "inline-flex items-center gap-2 text-sm text-foreground",
53965
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53966
+ className: "text-foreground-subtle",
53967
+ children: icon
53968
+ }), label]
53969
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
53970
+ checked,
53971
+ disabled,
53972
+ "aria-label": label,
53973
+ onCheckedChange: onChange
53974
+ })]
53975
+ });
53976
+ }
53977
+ function MinutesStepper({ label, minutes, disabled, onChange }) {
53978
+ const t = useUiText();
53979
+ const current = minutes ?? 0;
53980
+ const step = (delta) => onChange(Math.min(MAX_PUMP_MINUTES, Math.max(0, current + delta)));
53981
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53982
+ className: "flex items-center justify-between gap-2 rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
53983
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53984
+ className: "text-sm text-foreground",
53985
+ children: label
53986
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53987
+ className: "flex items-center gap-2",
53988
+ children: [
53989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
53990
+ variant: "outline",
53991
+ size: "sm",
53992
+ "aria-label": t("uiLibrary.petFountain.decrease", "Decrease {{label}}", { label }),
53993
+ disabled: disabled || minutes === null || current <= 0,
53994
+ onClick: () => step(-1),
53995
+ children: "−"
53996
+ }),
53997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53998
+ className: "w-14 text-center text-sm font-semibold tabular-nums text-foreground",
53999
+ children: minutes === null ? "—" : t("uiLibrary.petFountain.minutes", "{{count}} min", { count: current })
54000
+ }),
54001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
54002
+ variant: "outline",
54003
+ size: "sm",
54004
+ "aria-label": t("uiLibrary.petFountain.increase", "Increase {{label}}", { label }),
54005
+ disabled: disabled || minutes === null || current >= MAX_PUMP_MINUTES,
54006
+ onClick: () => step(1),
54007
+ children: "+"
54008
+ })
54009
+ ]
54010
+ })]
54011
+ });
54012
+ }
54013
+ function PetFountainInlineControl({ trpc, deviceId, features, optimistic }) {
54014
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFountainPanel, {
54015
+ trpc,
54016
+ deviceId,
54017
+ features,
54018
+ layout: "compact",
54019
+ optimistic
54020
+ });
54021
+ }
54022
+ function PetFountainHeroCard({ trpc, deviceId, features, optimistic }) {
54023
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFountainPanel, {
54024
+ trpc,
54025
+ deviceId,
54026
+ features,
54027
+ layout: "full",
54028
+ optimistic
54029
+ });
54030
+ }
54031
+ //#endregion
53628
54032
  //#region src/composites/device-controls/dishwasher-control.tsx
53629
54033
  /**
53630
54034
  * Dishwasher controls for a `DeviceType.Dishwasher` device.
@@ -56747,6 +57151,10 @@ var CONTROL_KIND_COMPONENTS = {
56747
57151
  InlineControl: PetFeederInlineControl,
56748
57152
  HeroCard: PetFeederHeroCard
56749
57153
  },
57154
+ "pet-fountain": {
57155
+ InlineControl: PetFountainInlineControl,
57156
+ HeroCard: PetFountainHeroCard
57157
+ },
56750
57158
  dishwasher: {
56751
57159
  InlineControl: DishwasherInlineControl,
56752
57160
  HeroCard: DishwasherHeroCard
@@ -56828,6 +57236,7 @@ var CONTROL_FILLS = {
56828
57236
  [_camstack_types.DeviceType.Container]: false,
56829
57237
  [_camstack_types.DeviceType.Image]: false,
56830
57238
  [_camstack_types.DeviceType.PetFeeder]: false,
57239
+ [_camstack_types.DeviceType.PetFountain]: false,
56831
57240
  [_camstack_types.DeviceType.Dishwasher]: false,
56832
57241
  [_camstack_types.DeviceType.Washer]: false,
56833
57242
  [_camstack_types.DeviceType.Dryer]: false
@@ -60835,6 +61244,88 @@ function deselectShown(current, shown) {
60835
61244
  return new Set([...current].filter((id) => !drop.has(id)));
60836
61245
  }
60837
61246
  //#endregion
61247
+ //#region src/composites/export-entity-rows.ts
61248
+ /**
61249
+ * The action and notification-target rows of the device export table (D968).
61250
+ *
61251
+ * An action and a notifier are list entries, never devices (D954): they are
61252
+ * merged into the SAME table as extra rows, built only from the kinds the
61253
+ * exporter answered. A kinds list that was not answered is UNKNOWN, not
61254
+ * "supports nothing" (D315) — it yields devices only and no filter entry, and
61255
+ * says nothing about what the exporter cannot do.
61256
+ *
61257
+ * Pure functions, so the rules are pinned without a DOM.
61258
+ */
61259
+ /** The type-filter ids of the entity kinds, apart from every device type. */
61260
+ var ENTITY_FILTER_IDS = {
61261
+ action: "entity:action",
61262
+ notifier: "entity:notifier"
61263
+ };
61264
+ var KindsSchema = zod.z.array(_camstack_types.ExportEntityKindSchema);
61265
+ var NamedSchema = zod.z.array(zod.z.object({
61266
+ id: zod.z.string(),
61267
+ name: zod.z.string()
61268
+ }));
61269
+ var ExposedSchema = zod.z.array(_camstack_types.ExposedEntitySchema);
61270
+ /**
61271
+ * The non-device kinds the exporter answered, or null when it did not answer
61272
+ * (unknown — never an empty set).
61273
+ */
61274
+ function supportedEntityKinds(data) {
61275
+ const parsed = KindsSchema.safeParse(data);
61276
+ if (!parsed.success) return null;
61277
+ const kinds = /* @__PURE__ */ new Set();
61278
+ for (const kind of parsed.data) if (kind === "action" || kind === "notifier") kinds.add(kind);
61279
+ return kinds;
61280
+ }
61281
+ function buildEntityRows(input) {
61282
+ const { kinds } = input;
61283
+ if (kinds === null) return [];
61284
+ const exposed = ExposedSchema.safeParse(input.exposed);
61285
+ const exposedActions = /* @__PURE__ */ new Set();
61286
+ const exposedTargets = /* @__PURE__ */ new Set();
61287
+ if (exposed.success) {
61288
+ for (const row of exposed.data) if (row.entity.kind === "action") exposedActions.add(row.entity.actionId);
61289
+ else if (row.entity.kind === "notifier") exposedTargets.add(row.entity.targetId);
61290
+ }
61291
+ const rows = [];
61292
+ if (kinds.has("action")) {
61293
+ const actions = NamedSchema.safeParse(input.actions);
61294
+ for (const a of actions.success ? actions.data : []) rows.push({
61295
+ kind: "action",
61296
+ key: `action:${a.id}`,
61297
+ ref: {
61298
+ kind: "action",
61299
+ actionId: a.id
61300
+ },
61301
+ name: a.name,
61302
+ exposed: exposedActions.has(a.id)
61303
+ });
61304
+ }
61305
+ if (kinds.has("notifier")) {
61306
+ const targets = NamedSchema.safeParse(input.targets);
61307
+ for (const t of targets.success ? targets.data : []) rows.push({
61308
+ kind: "notifier",
61309
+ key: `notifier:${t.id}`,
61310
+ ref: {
61311
+ kind: "notifier",
61312
+ targetId: t.id
61313
+ },
61314
+ name: t.name,
61315
+ exposed: exposedTargets.has(t.id)
61316
+ });
61317
+ }
61318
+ return rows;
61319
+ }
61320
+ /** The type-filter ids to add beside the device types: one per SUPPORTED kind. */
61321
+ function entityFilterOptionIds(kinds) {
61322
+ if (kinds === null) return [];
61323
+ const ids = [];
61324
+ if (kinds.has("action")) ids.push(ENTITY_FILTER_IDS.action);
61325
+ if (kinds.has("notifier")) ids.push(ENTITY_FILTER_IDS.notifier);
61326
+ return ids;
61327
+ }
61328
+ //#endregion
60838
61329
  //#region src/composites/device-export-panel.tsx
60839
61330
  /**
60840
61331
  * DeviceExportPanel — generic, addon-agnostic surface for the
@@ -61054,19 +61545,28 @@ function ExportSelectCell({ device, checked, onToggle }) {
61054
61545
  * of these live in one table, and "Expose" alone names none of them.
61055
61546
  */
61056
61547
  function ExportRowToggle({ addonId, device, exposed, busy, onToggle }) {
61548
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExposeToggleButton, {
61549
+ addonId,
61550
+ label: useDeviceLabel(device, "compact"),
61551
+ exposed,
61552
+ busy,
61553
+ onToggle: (action) => onToggle(device, action)
61554
+ });
61555
+ }
61556
+ /** The Expose / Exposed button every row kind (device, action, notifier) shares. */
61557
+ function ExposeToggleButton({ addonId, label, exposed, busy, onToggle }) {
61057
61558
  const t = useUiText();
61058
- const deviceLabel = useDeviceLabel(device, "compact");
61059
61559
  const handleClick = (e) => {
61060
61560
  e.stopPropagation();
61061
61561
  if (busy) return;
61062
- onToggle(device, exposed ? "unexpose" : "expose");
61562
+ onToggle(exposed ? "unexpose" : "expose");
61063
61563
  };
61064
61564
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61065
61565
  size: "sm",
61066
61566
  variant: exposed ? "ghost" : "primary",
61067
61567
  disabled: busy,
61068
61568
  onClick: handleClick,
61069
- "aria-label": exposed ? t("uiLibrary.deviceExport.unexposeDevice", "Unexpose {{name}}", { name: deviceLabel }) : t("uiLibrary.deviceExport.exposeDevice", "Expose {{name}}", { name: deviceLabel }),
61569
+ "aria-label": exposed ? t("uiLibrary.deviceExport.unexposeDevice", "Unexpose {{name}}", { name: label }) : t("uiLibrary.deviceExport.exposeDevice", "Expose {{name}}", { name: label }),
61070
61570
  title: exposed ? t("uiLibrary.deviceExport.stopTitle", "Stop exposing to \"{{addon}}\"", { addon: addonId }) : t("uiLibrary.deviceExport.exposeTitle", "Expose to \"{{addon}}\"", { addon: addonId }),
61071
61571
  children: exposed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Unlink2, { className: "mr-1 h-3.5 w-3.5" }), t("uiLibrary.deviceExport.exposed", "Exposed")] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Share2, { className: "mr-1 h-3.5 w-3.5" }), t("uiLibrary.deviceExport.expose", "Expose")] })
61072
61572
  });
@@ -61147,11 +61647,34 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61147
61647
  addonId
61148
61648
  }, { retry: false });
61149
61649
  const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
61650
+ const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
61651
+ nodeId: "hub",
61652
+ addonId
61653
+ }, { retry: false });
61654
+ const entityKinds = (0, react$1.useMemo)(() => supportedEntityKinds(entityKindsQuery.data), [entityKindsQuery.data]);
61655
+ const exposedEntitiesQuery = useDeviceExportListExposedEntities({
61656
+ nodeId: "hub",
61657
+ addonId
61658
+ }, {
61659
+ refetchInterval: STATUS_POLL_INTERVAL_MS,
61660
+ retry: false,
61661
+ enabled: entityKinds !== null
61662
+ });
61663
+ const actionsQuery = useActionsList({}, {
61664
+ refetchInterval: FLEET_POLL_INTERVAL_MS,
61665
+ enabled: entityKinds?.has("action") === true
61666
+ });
61667
+ const targetsQuery = useNotificationOutputListTargets({}, {
61668
+ refetchInterval: FLEET_POLL_INTERVAL_MS,
61669
+ enabled: entityKinds?.has("notifier") === true
61670
+ });
61150
61671
  const invalidate = (0, react$1.useCallback)(() => {
61151
61672
  queryClient.invalidateQueries({ queryKey: [["deviceExport"]] });
61152
61673
  }, [queryClient]);
61153
61674
  const exposeMutation = useDeviceExportExposeDevice({ onSuccess: invalidate });
61154
61675
  const unexposeMutation = useDeviceExportUnexposeDevice({ onSuccess: invalidate });
61676
+ const exposeEntityMutation = useDeviceExportExposeEntity({ onSuccess: invalidate });
61677
+ const unexposeEntityMutation = useDeviceExportUnexposeEntity({ onSuccess: invalidate });
61155
61678
  const status = (0, react$1.useMemo)(() => parseStatus(statusQuery.data), [statusQuery.data]);
61156
61679
  const exposed = (0, react$1.useMemo)(() => {
61157
61680
  const parsed = ExposedDeviceArraySchema.safeParse(exposedQuery.data);
@@ -61181,10 +61704,37 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61181
61704
  }
61182
61705
  return rows;
61183
61706
  }, [devicesQuery.data, supportedKinds]);
61707
+ const entityRows = (0, react$1.useMemo)(() => buildEntityRows({
61708
+ kinds: entityKinds,
61709
+ actions: actionsQuery.data,
61710
+ targets: targetsQuery.data,
61711
+ exposed: exposedEntitiesQuery.data
61712
+ }), [
61713
+ entityKinds,
61714
+ actionsQuery.data,
61715
+ targetsQuery.data,
61716
+ exposedEntitiesQuery.data
61717
+ ]);
61718
+ const typeFilterOptions = (0, react$1.useMemo)(() => {
61719
+ const entityOptions = entityFilterOptionIds(entityKinds).map((id) => id === ENTITY_FILTER_IDS.action ? {
61720
+ id,
61721
+ label: t("uiLibrary.deviceExport.typeAction", "Action"),
61722
+ icon: Play
61723
+ } : {
61724
+ id,
61725
+ label: t("uiLibrary.deviceExport.typeNotifier", "Notifier"),
61726
+ icon: Bell
61727
+ });
61728
+ return [...supportedTypeOptions, ...entityOptions];
61729
+ }, [
61730
+ supportedTypeOptions,
61731
+ entityKinds,
61732
+ t
61733
+ ]);
61184
61734
  const linkState = status?.linkState ?? "unlinked";
61185
- const loading = statusQuery.isLoading || exposedQuery.isLoading || kindsQuery.isLoading || devicesQuery.isLoading;
61735
+ const loading = statusQuery.isLoading || exposedQuery.isLoading || kindsQuery.isLoading || devicesQuery.isLoading || entityKindsQuery.isLoading;
61186
61736
  const exposedCount = status?.exposedDeviceCount ?? exposedIds.size;
61187
- const busy = exposeMutation.isPending || unexposeMutation.isPending;
61737
+ const busy = exposeMutation.isPending || unexposeMutation.isPending || exposeEntityMutation.isPending || unexposeEntityMutation.isPending;
61188
61738
  const [selectedIds, setSelectedIds] = (0, react$1.useState)(() => /* @__PURE__ */ new Set());
61189
61739
  const [outcome, setOutcome] = (0, react$1.useState)(null);
61190
61740
  const [running, setRunning] = (0, react$1.useState)(false);
@@ -61251,10 +61801,71 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61251
61801
  labelFor,
61252
61802
  running
61253
61803
  ]);
61804
+ /**
61805
+ * Expose / unexpose ONE action or notification target. Same contract as the
61806
+ * device runner: the result is recorded either way and a refusal is named on
61807
+ * screen, with the entity (it has no device id to tag).
61808
+ */
61809
+ const runEntityExport = (0, react$1.useCallback)(async (action, row) => {
61810
+ if (running) return;
61811
+ setRunning(true);
61812
+ setOutcome(null);
61813
+ const mutation = action === "expose" ? exposeEntityMutation : unexposeEntityMutation;
61814
+ const succeeded = [];
61815
+ const failed = [];
61816
+ try {
61817
+ await mutation.mutateAsync({
61818
+ entity: row.ref,
61819
+ nodeId: "hub",
61820
+ addonId
61821
+ });
61822
+ succeeded.push(row.name);
61823
+ } catch (err) {
61824
+ const message = err instanceof Error ? err.message : String(err);
61825
+ console.error(`[device-export] ${action} ${row.kind} failed`, {
61826
+ tags: row.ref.kind === "action" ? { actionId: row.ref.actionId } : { targetId: row.ref.targetId },
61827
+ addonId,
61828
+ error: message
61829
+ });
61830
+ failed.push({
61831
+ label: row.name,
61832
+ message
61833
+ });
61834
+ }
61835
+ setOutcome({
61836
+ action,
61837
+ total: 1,
61838
+ succeeded,
61839
+ failed
61840
+ });
61841
+ setRunning(false);
61842
+ }, [
61843
+ addonId,
61844
+ exposeEntityMutation,
61845
+ unexposeEntityMutation,
61846
+ running
61847
+ ]);
61254
61848
  const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
61255
61849
  const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices), [candidateDevices]);
61256
61850
  const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices), [candidateDevices]);
61257
61851
  const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters), [candidateDevices, bulkFilters]);
61852
+ const shownRows = (0, react$1.useMemo)(() => {
61853
+ const deviceRows = shownDevices.map((device) => ({
61854
+ kind: "device",
61855
+ key: String(device.id),
61856
+ device
61857
+ }));
61858
+ const entities = bulkFilters.roles.length > 0 || bulkFilters.providers.length > 0 ? [] : entityRows.map((entity) => ({
61859
+ kind: "entity",
61860
+ key: entity.key,
61861
+ entity
61862
+ }));
61863
+ return [...deviceRows, ...entities];
61864
+ }, [
61865
+ shownDevices,
61866
+ entityRows,
61867
+ bulkFilters
61868
+ ]);
61258
61869
  const setRoleFilter = (0, react$1.useCallback)((roles) => setBulkFilters((f) => ({
61259
61870
  ...f,
61260
61871
  roles
@@ -61273,27 +61884,35 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61273
61884
  const onRowToggle = (0, react$1.useCallback)((device, action) => {
61274
61885
  runExport(action, [String(device.id)]);
61275
61886
  }, [runExport]);
61276
- const onRowClick = (0, react$1.useCallback)((device) => onOpenDevice?.(String(device.id)), [onOpenDevice]);
61277
- const getKey = (0, react$1.useCallback)((device) => String(device.id), []);
61278
- const getSearchText = (0, react$1.useCallback)((device) => `${device.name} ${device.stableId}`, []);
61279
- const getFilterType = (0, react$1.useCallback)((device) => device.type, []);
61887
+ const onRowClick = (0, react$1.useCallback)((row) => {
61888
+ if (row.kind === "device") onOpenDevice?.(String(row.device.id));
61889
+ }, [onOpenDevice]);
61890
+ const getKey = (0, react$1.useCallback)((row) => row.key, []);
61891
+ const getSearchText = (0, react$1.useCallback)((row) => row.kind === "device" ? `${row.device.name} ${row.device.stableId}` : row.entity.name, []);
61892
+ const getFilterType = (0, react$1.useCallback)((row) => row.kind === "device" ? row.device.type : ENTITY_FILTER_IDS[row.entity.kind], []);
61280
61893
  const columns = (0, react$1.useMemo)(() => [
61281
61894
  {
61282
61895
  key: "select",
61283
61896
  header: "",
61284
61897
  headerClassName: "w-8",
61285
- cell: (device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportSelectCell, {
61286
- device,
61287
- checked: selectedIds.has(String(device.id)),
61288
- onToggle: () => toggleSelection(String(device.id))
61289
- })
61898
+ cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportSelectCell, {
61899
+ device: row.device,
61900
+ checked: selectedIds.has(String(row.device.id)),
61901
+ onToggle: () => toggleSelection(String(row.device.id))
61902
+ }) : null
61290
61903
  },
61291
61904
  {
61292
61905
  key: "type",
61293
61906
  header: t("uiLibrary.deviceExport.colType", "Type"),
61294
61907
  headerClassName: "w-32",
61295
- cell: (device) => {
61296
- const meta = deviceTypeMetaOf(device.type, t);
61908
+ cell: (row) => {
61909
+ const meta = row.kind === "device" ? deviceTypeMetaOf(row.device.type, t) : row.entity.kind === "action" ? {
61910
+ label: t("uiLibrary.deviceExport.typeAction", "Action"),
61911
+ icon: Play
61912
+ } : {
61913
+ label: t("uiLibrary.deviceExport.typeNotifier", "Notifier"),
61914
+ icon: Bell
61915
+ };
61297
61916
  const TypeIcon = meta.icon;
61298
61917
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
61299
61918
  className: "inline-flex items-center gap-1.5 text-[11px] text-foreground-subtle",
@@ -61304,10 +61923,13 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61304
61923
  {
61305
61924
  key: "name",
61306
61925
  header: t("uiLibrary.deviceExport.colName", "Name"),
61307
- cell: (device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceName, {
61308
- device,
61926
+ cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceName, {
61927
+ device: row.device,
61309
61928
  form: "compact",
61310
61929
  className: "text-xs font-medium text-foreground"
61930
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
61931
+ className: "text-xs font-medium text-foreground",
61932
+ children: row.entity.name
61311
61933
  })
61312
61934
  },
61313
61935
  {
@@ -61315,12 +61937,18 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61315
61937
  header: t("uiLibrary.deviceExport.colExport", "Export"),
61316
61938
  headerClassName: "w-32 text-right",
61317
61939
  cellClassName: "text-right",
61318
- cell: (device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportRowToggle, {
61940
+ cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportRowToggle, {
61319
61941
  addonId,
61320
- device,
61321
- exposed: exposedIds.has(String(device.id)),
61942
+ device: row.device,
61943
+ exposed: exposedIds.has(String(row.device.id)),
61322
61944
  busy,
61323
61945
  onToggle: onRowToggle
61946
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExposeToggleButton, {
61947
+ addonId,
61948
+ label: row.entity.name,
61949
+ exposed: row.entity.exposed,
61950
+ busy,
61951
+ onToggle: (action) => void runEntityExport(action, row.entity)
61324
61952
  })
61325
61953
  }
61326
61954
  ], [
@@ -61328,67 +61956,71 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61328
61956
  busy,
61329
61957
  exposedIds,
61330
61958
  onRowToggle,
61959
+ runEntityExport,
61331
61960
  selectedIds,
61332
61961
  toggleSelection,
61333
61962
  t
61334
61963
  ]);
61335
- const renderToolbar = (0, react$1.useCallback)(({ filtered }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
61336
- className: "inline-flex flex-wrap items-center gap-1.5",
61337
- children: [
61338
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
61339
- label: t("uiLibrary.deviceExport.filterRole", "Role"),
61340
- options: roleOptions,
61341
- selected: bulkFilters.roles,
61342
- onChange: setRoleFilter
61343
- }),
61344
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
61345
- label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
61346
- options: providerOptions,
61347
- selected: bulkFilters.providers,
61348
- onChange: setProviderFilter
61349
- }),
61350
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61351
- size: "sm",
61352
- variant: "secondary",
61353
- disabled: filtered.length === 0,
61354
- onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
61355
- children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
61356
- }),
61357
- selectedIds.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
61358
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61359
- size: "sm",
61360
- variant: "ghost",
61361
- onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
61362
- children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
61363
- }),
61364
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
61365
- "data-testid": "export-selection-count",
61366
- className: "text-[11px] text-foreground-subtle",
61367
- children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
61964
+ const renderToolbar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
61965
+ const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
61966
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
61967
+ className: "inline-flex flex-wrap items-center gap-1.5",
61968
+ children: [
61969
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
61970
+ label: t("uiLibrary.deviceExport.filterRole", "Role"),
61971
+ options: roleOptions,
61972
+ selected: bulkFilters.roles,
61973
+ onChange: setRoleFilter
61368
61974
  }),
61369
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61370
- size: "sm",
61371
- variant: "primary",
61372
- disabled: busy,
61373
- onClick: exposeSelected,
61374
- children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
61975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
61976
+ label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
61977
+ options: providerOptions,
61978
+ selected: bulkFilters.providers,
61979
+ onChange: setProviderFilter
61375
61980
  }),
61376
61981
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61377
61982
  size: "sm",
61378
61983
  variant: "secondary",
61379
- disabled: busy,
61380
- onClick: unexposeSelected,
61381
- children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
61984
+ disabled: filtered.length === 0,
61985
+ onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
61986
+ children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
61382
61987
  }),
61383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61384
- size: "sm",
61385
- variant: "ghost",
61386
- onClick: clearSelection,
61387
- children: t("uiLibrary.deviceExport.clear", "Clear")
61388
- })
61389
- ] })
61390
- ]
61391
- }), [
61988
+ selectedIds.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
61989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61990
+ size: "sm",
61991
+ variant: "ghost",
61992
+ onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
61993
+ children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
61994
+ }),
61995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
61996
+ "data-testid": "export-selection-count",
61997
+ className: "text-[11px] text-foreground-subtle",
61998
+ children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
61999
+ }),
62000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
62001
+ size: "sm",
62002
+ variant: "primary",
62003
+ disabled: busy,
62004
+ onClick: exposeSelected,
62005
+ children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
62006
+ }),
62007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
62008
+ size: "sm",
62009
+ variant: "secondary",
62010
+ disabled: busy,
62011
+ onClick: unexposeSelected,
62012
+ children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
62013
+ }),
62014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
62015
+ size: "sm",
62016
+ variant: "ghost",
62017
+ onClick: clearSelection,
62018
+ children: t("uiLibrary.deviceExport.clear", "Clear")
62019
+ })
62020
+ ] })
62021
+ ]
62022
+ });
62023
+ }, [
61392
62024
  t,
61393
62025
  roleOptions,
61394
62026
  providerOptions,
@@ -61437,16 +62069,16 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61437
62069
  }),
61438
62070
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
61439
62071
  className: "p-2 sm:p-3",
61440
- children: loading && candidateDevices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62072
+ children: loading && candidateDevices.length === 0 && entityRows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
61441
62073
  className: "flex items-center gap-2 px-1 py-6 text-xs text-foreground-subtle",
61442
62074
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), t("uiLibrary.deviceExport.loading", "Loading exportable devices…")]
61443
- }) : hasSupportedKinds && candidateDevices.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceList, {
62075
+ }) : hasSupportedKinds && candidateDevices.length > 0 || entityRows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceList, {
61444
62076
  mode: "generic",
61445
- items: shownDevices,
62077
+ items: shownRows,
61446
62078
  getKey,
61447
62079
  getSearchText,
61448
62080
  getFilterType,
61449
- filterOptions: supportedTypeOptions,
62081
+ filterOptions: typeFilterOptions,
61450
62082
  columns,
61451
62083
  onRowClick: onOpenDevice ? onRowClick : void 0,
61452
62084
  toolbar: renderToolbar,
@@ -63590,7 +64222,8 @@ function SectionedPage({ icon, title, subtitle, actions, sections, defaultSectio
63590
64222
  tabKey: activeId,
63591
64223
  fill,
63592
64224
  children: fill ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63593
- className: "flex min-h-0 flex-1 flex-col",
64225
+ className: "flex min-h-0 flex-1 flex-col overflow-y-auto",
64226
+ "data-testid": "sectioned-page-body",
63594
64227
  children: active?.content
63595
64228
  }) : active?.content
63596
64229
  });
@@ -71958,6 +72591,7 @@ exports.EmptyState = EmptyState;
71958
72591
  exports.EndpointResultsCard = EndpointResultsCard;
71959
72592
  exports.EndpointTestCard = EndpointTestCard;
71960
72593
  exports.EntryJsonSchemaForm = EntryJsonSchemaForm;
72594
+ exports.Eraser = Eraser;
71961
72595
  exports.ErrorBox = ErrorBox;
71962
72596
  exports.EventKindGlyph = EventKindGlyph;
71963
72597
  exports.EventStream = EventStream;
@@ -72974,12 +73608,16 @@ exports.useDeviceDiscoveryRefreshDiscovery = useDeviceDiscoveryRefreshDiscovery;
72974
73608
  exports.useDeviceDiscoveryReleaseDevice = useDeviceDiscoveryReleaseDevice;
72975
73609
  exports.useDeviceExportApplyDeviceSettingsPatch = useDeviceExportApplyDeviceSettingsPatch;
72976
73610
  exports.useDeviceExportExposeDevice = useDeviceExportExposeDevice;
73611
+ exports.useDeviceExportExposeEntity = useDeviceExportExposeEntity;
72977
73612
  exports.useDeviceExportGetDeviceLiveContribution = useDeviceExportGetDeviceLiveContribution;
72978
73613
  exports.useDeviceExportGetDeviceSettingsContribution = useDeviceExportGetDeviceSettingsContribution;
72979
73614
  exports.useDeviceExportGetStatus = useDeviceExportGetStatus;
72980
73615
  exports.useDeviceExportListExposedDevices = useDeviceExportListExposedDevices;
73616
+ exports.useDeviceExportListExposedEntities = useDeviceExportListExposedEntities;
72981
73617
  exports.useDeviceExportListSupportedDeviceKinds = useDeviceExportListSupportedDeviceKinds;
73618
+ exports.useDeviceExportListSupportedEntityKinds = useDeviceExportListSupportedEntityKinds;
72982
73619
  exports.useDeviceExportUnexposeDevice = useDeviceExportUnexposeDevice;
73620
+ exports.useDeviceExportUnexposeEntity = useDeviceExportUnexposeEntity;
72983
73621
  exports.useDeviceExtensionListCandidates = useDeviceExtensionListCandidates;
72984
73622
  exports.useDeviceExtensionListSlots = useDeviceExtensionListSlots;
72985
73623
  exports.useDeviceExtensionListSources = useDeviceExtensionListSources;
@@ -73470,6 +74108,12 @@ exports.usePetFeederSetChildLock = usePetFeederSetChildLock;
73470
74108
  exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
73471
74109
  exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
73472
74110
  exports.usePetFeederSetVolume = usePetFeederSetVolume;
74111
+ exports.usePetFountainGetStatus = usePetFountainGetStatus;
74112
+ exports.usePetFountainResetDailyConsumption = usePetFountainResetDailyConsumption;
74113
+ exports.usePetFountainSetPump = usePetFountainSetPump;
74114
+ exports.usePetFountainSetPumpInterval = usePetFountainSetPumpInterval;
74115
+ exports.usePetFountainSetPumpOffMinutes = usePetFountainSetPumpOffMinutes;
74116
+ exports.usePetFountainSetPumpOnMinutes = usePetFountainSetPumpOnMinutes;
73473
74117
  exports.usePickerSiteZones = usePickerSiteZones;
73474
74118
  exports.usePipelineAnalyticsAddRetrainFrameFromRecording = usePipelineAnalyticsAddRetrainFrameFromRecording;
73475
74119
  exports.usePipelineAnalyticsAddRetrainFramesToCollection = usePipelineAnalyticsAddRetrainFramesToCollection;