@camstack/ui-library 1.2.349 → 1.2.351

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`. */
@@ -12576,6 +12614,10 @@ var useServerManagementApplyServerUpdate = trpc.serverManagement.applyServerUpda
12576
12614
  var useServerManagementRollbackServerUpdate = trpc.serverManagement.rollbackServerUpdate.useMutation;
12577
12615
  /** Generated alias around `trpc.serverManagement.restartServer.useMutation`. */
12578
12616
  var useServerManagementRestartServer = trpc.serverManagement.restartServer.useMutation;
12617
+ /** Generated alias around `trpc.serverManagement.getLaunchOverrides.useQuery`. */
12618
+ var useServerManagementGetLaunchOverrides = trpc.serverManagement.getLaunchOverrides.useQuery;
12619
+ /** Generated alias around `trpc.serverManagement.setLaunchOverrides.useMutation`. */
12620
+ var useServerManagementSetLaunchOverrides = trpc.serverManagement.setLaunchOverrides.useMutation;
12579
12621
  /** Generated alias around `trpc.settingsStore.get.useQuery`. */
12580
12622
  var useSettingsStoreGet = trpc.settingsStore.get.useQuery;
12581
12623
  /** Generated alias around `trpc.settingsStore.set.useMutation`. */
@@ -48791,6 +48833,7 @@ var CONTROL_CAP_NAMES = [
48791
48833
  "media-player",
48792
48834
  "control",
48793
48835
  "pet-feeder",
48836
+ "pet-fountain",
48794
48837
  "dishwasher"
48795
48838
  ];
48796
48839
  //#endregion
@@ -53081,6 +53124,10 @@ var HOLD_DELAY_MS = 350;
53081
53124
  var HOLD_INTERVAL_MS = 70;
53082
53125
  var HOLD_ACCEL_AFTER = 8;
53083
53126
  var HOLD_FAST_STEP = 5;
53127
+ /** The slice stores the bowl as a mass in kilograms (D944); the card draws grams. */
53128
+ function bowlGrams(kilograms) {
53129
+ return kilograms === null ? null : Math.round(kilograms * 1e3);
53130
+ }
53084
53131
  function clampPortion(grams) {
53085
53132
  return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
53086
53133
  }
@@ -53132,7 +53179,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53132
53179
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UtensilsCrossed, {
53133
53180
  className: "h-3.5 w-3.5",
53134
53181
  "aria-hidden": true
53135
- }), display.bowlFood ?? "—"]
53182
+ }), bowlGrams(display.bowlFood) ?? "—"]
53136
53183
  }),
53137
53184
  onMains && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53138
53185
  className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle",
@@ -53178,7 +53225,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53178
53225
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53179
53226
  className: "flex items-center gap-4 rounded-lg border border-border/50 bg-surface-subtle/40 p-3",
53180
53227
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BowlGauge, {
53181
- grams: display.bowlFood,
53228
+ grams: bowlGrams(display.bowlFood),
53182
53229
  low: display.lowFood,
53183
53230
  eating: display.eating
53184
53231
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -53188,7 +53235,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53188
53235
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53189
53236
  className: "text-3xl font-bold leading-none tabular-nums",
53190
53237
  style: { color: display.lowFood ? KIBBLE_LOW : KIBBLE },
53191
- children: display.bowlFood ?? "—"
53238
+ children: bowlGrams(display.bowlFood) ?? "—"
53192
53239
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53193
53240
  className: "text-sm text-foreground-subtle",
53194
53241
  children: t("uiLibrary.petFeeder.inBowl", "in bowl")
@@ -53625,6 +53672,367 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
53625
53672
  });
53626
53673
  }
53627
53674
  //#endregion
53675
+ //#region src/composites/device-controls/pet-fountain-panel.tsx
53676
+ /**
53677
+ * Pet-fountain controls for a `DeviceType.PetFountain` device.
53678
+ *
53679
+ * Reads the `pet-fountain` cap status slice (water level and volume, empty,
53680
+ * pump and its duty cycle, the water drunk) and renders:
53681
+ * - PetFountainInlineControl (device-list row): the tank level and a pump
53682
+ * on/off toggle.
53683
+ * - PetFountainHeroCard (device-detail hero): the tank gauge with the level
53684
+ * and the volume first, what the pet drank today / in 24 h / this hour /
53685
+ * last time, the pump switch, its duty-cycle switch and the run / rest
53686
+ * minutes. The filter is a consumable: the page's Consumables section
53687
+ * draws it, never this card (D915).
53688
+ *
53689
+ * Every reading may be unknown (`null`) and is drawn as a dash, never as
53690
+ * zero. Stored volumes are litres and durations seconds (D944); the card
53691
+ * shows millilitres and minutes. Actions are gated on `available`.
53692
+ */
53693
+ var LITRES_TO_ML = 1e3;
53694
+ var SECONDS_PER_MINUTE = 60;
53695
+ var MAX_PUMP_MINUTES = 240;
53696
+ function millilitres(litres) {
53697
+ return litres === null ? "—" : `${Math.round(litres * LITRES_TO_ML)} mL`;
53698
+ }
53699
+ function minutesOf(seconds) {
53700
+ return seconds === null ? null : Math.round(seconds / SECONDS_PER_MINUTE);
53701
+ }
53702
+ function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
53703
+ const t = useUiText();
53704
+ const { cap, slice, available } = useDeviceCapability({
53705
+ trpc,
53706
+ deviceId,
53707
+ cap: (d) => d.petFountain,
53708
+ slice: (d) => d.state.petFountain
53709
+ });
53710
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
53711
+ if (cap === void 0 || display === void 0) return null;
53712
+ const busy = !available;
53713
+ const fault = display.error !== null && display.error !== "no_error" ? display.error : null;
53714
+ const pumpOn = display.pumpRunning === true;
53715
+ const level = display.waterLevel;
53716
+ const setPump = (on) => {
53717
+ applyOptimistic({ pumpRunning: on });
53718
+ cap.setPump({ on });
53719
+ };
53720
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53721
+ className: "inline-flex items-center gap-2",
53722
+ children: [
53723
+ (fault !== null || display.empty === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
53724
+ className: "h-3.5 w-3.5 text-danger",
53725
+ "aria-label": fault ?? t("uiLibrary.petFountain.empty", "Tank empty")
53726
+ }),
53727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53728
+ className: cn("inline-flex items-center gap-1 text-xs font-medium tabular-nums", display.empty === true ? "text-danger" : "text-foreground"),
53729
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, {
53730
+ className: "h-3.5 w-3.5",
53731
+ "aria-hidden": true
53732
+ }), level === null ? "—" : `${Math.round(level)}%`]
53733
+ }),
53734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53735
+ onClick: (e) => e.stopPropagation(),
53736
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
53737
+ checked: pumpOn,
53738
+ disabled: busy,
53739
+ "aria-label": t("uiLibrary.petFountain.pump", "Pump"),
53740
+ onCheckedChange: setPump
53741
+ })
53742
+ })
53743
+ ]
53744
+ });
53745
+ const intervalOn = display.pumpIntervalEnabled === true;
53746
+ const onMinutes = minutesOf(display.pumpOnTime);
53747
+ const offMinutes = minutesOf(display.pumpOffTime);
53748
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53749
+ className: "flex flex-col items-stretch gap-3 rounded-lg border border-border bg-surface p-4",
53750
+ children: [
53751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53752
+ className: "flex items-center gap-4 rounded-lg border border-border-subtle bg-background-elevated p-3",
53753
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankGauge, {
53754
+ level,
53755
+ empty: display.empty === true
53756
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53757
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
53758
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53759
+ className: "flex items-baseline gap-2",
53760
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53761
+ className: cn("text-3xl font-bold leading-none tabular-nums", display.empty === true ? "text-danger" : "text-info"),
53762
+ children: level === null ? "—" : `${Math.round(level)}%`
53763
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53764
+ className: "text-sm tabular-nums text-foreground-muted",
53765
+ children: millilitres(display.waterVolume)
53766
+ })]
53767
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusLine, {
53768
+ empty: display.empty,
53769
+ pumpRunning: display.pumpRunning
53770
+ })]
53771
+ })]
53772
+ }),
53773
+ fault !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53774
+ 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",
53775
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
53776
+ className: "mt-px h-4 w-4 shrink-0",
53777
+ "aria-hidden": true
53778
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53779
+ className: "min-w-0 break-words",
53780
+ children: fault
53781
+ })]
53782
+ }),
53783
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section$1, {
53784
+ label: t("uiLibrary.petFountain.consumption", "Water drunk"),
53785
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53786
+ className: "grid grid-cols-2 gap-2 sm:grid-cols-4",
53787
+ children: [
53788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53789
+ label: t("uiLibrary.petFountain.today", "Today"),
53790
+ value: display.todayDrink
53791
+ }),
53792
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53793
+ label: t("uiLibrary.petFountain.last24h", "Last 24 h"),
53794
+ value: display.last24hDrink
53795
+ }),
53796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53797
+ label: t("uiLibrary.petFountain.thisHour", "This hour"),
53798
+ value: display.currentHourDrink
53799
+ }),
53800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
53801
+ label: t("uiLibrary.petFountain.lastDrink", "Last drink"),
53802
+ value: display.lastDrink
53803
+ })
53804
+ ]
53805
+ })
53806
+ }),
53807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section$1, {
53808
+ label: t("uiLibrary.petFountain.pumpSection", "Controls"),
53809
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53810
+ className: "flex flex-col gap-2",
53811
+ children: [
53812
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
53813
+ 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" }),
53814
+ label: t("uiLibrary.petFountain.pump", "Pump"),
53815
+ checked: pumpOn,
53816
+ disabled: busy,
53817
+ onChange: setPump
53818
+ }),
53819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
53820
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-4 w-4" }),
53821
+ label: t("uiLibrary.petFountain.cycle", "On / off cycle"),
53822
+ checked: intervalOn,
53823
+ disabled: busy,
53824
+ onChange: (on) => {
53825
+ applyOptimistic({ pumpIntervalEnabled: on });
53826
+ cap.setPumpInterval({ on });
53827
+ }
53828
+ }),
53829
+ intervalOn && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53830
+ className: "flex flex-col gap-2",
53831
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinutesStepper, {
53832
+ label: t("uiLibrary.petFountain.runMinutes", "Runs for"),
53833
+ minutes: onMinutes,
53834
+ disabled: busy,
53835
+ onChange: (minutes) => {
53836
+ applyOptimistic({ pumpOnTime: minutes * SECONDS_PER_MINUTE });
53837
+ cap.setPumpOnMinutes({ minutes });
53838
+ }
53839
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinutesStepper, {
53840
+ label: t("uiLibrary.petFountain.restMinutes", "Rests for"),
53841
+ minutes: offMinutes,
53842
+ disabled: busy,
53843
+ onChange: (minutes) => {
53844
+ applyOptimistic({ pumpOffTime: minutes * SECONDS_PER_MINUTE });
53845
+ cap.setPumpOffMinutes({ minutes });
53846
+ }
53847
+ })]
53848
+ })
53849
+ ]
53850
+ })
53851
+ }),
53852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
53853
+ variant: "outline",
53854
+ size: "sm",
53855
+ disabled: busy,
53856
+ onClick: () => void cap.resetDailyConsumption({}),
53857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eraser, {
53858
+ className: "h-4 w-4",
53859
+ "aria-hidden": true
53860
+ }), t("uiLibrary.petFountain.clearDaily", "Clear daily count")]
53861
+ })
53862
+ ]
53863
+ });
53864
+ }
53865
+ /** A tank outline whose water rises with the level; danger-coloured when empty. */
53866
+ function TankGauge({ level, empty }) {
53867
+ const t = useUiText();
53868
+ const top = 58 - Math.min(1, Math.max(0, (level ?? 0) / 100)) * 40;
53869
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
53870
+ width: 72,
53871
+ height: 72,
53872
+ viewBox: "0 0 72 72",
53873
+ role: "img",
53874
+ "aria-label": level === null ? t("uiLibrary.petFountain.levelUnknown", "Water level unknown") : t("uiLibrary.petFountain.levelPercent", "{{percent}}% water", { percent: Math.round(level) }),
53875
+ className: "shrink-0",
53876
+ children: [
53877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("clipPath", {
53878
+ id: "pfn-tank-clip",
53879
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53880
+ x: "18",
53881
+ y: "14",
53882
+ width: "36",
53883
+ height: "46",
53884
+ rx: "6"
53885
+ })
53886
+ }) }),
53887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
53888
+ clipPath: "url(#pfn-tank-clip)",
53889
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53890
+ x: "18",
53891
+ y: top,
53892
+ width: "36",
53893
+ height: "46",
53894
+ className: empty ? "fill-danger" : "fill-info",
53895
+ opacity: .75
53896
+ })
53897
+ }),
53898
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53899
+ x: "18",
53900
+ y: "14",
53901
+ width: "36",
53902
+ height: "46",
53903
+ rx: "6",
53904
+ fill: "none",
53905
+ strokeWidth: "2.5",
53906
+ className: "stroke-border"
53907
+ }),
53908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
53909
+ x: "28",
53910
+ y: "8",
53911
+ width: "16",
53912
+ height: "6",
53913
+ rx: "2",
53914
+ className: "fill-border"
53915
+ })
53916
+ ]
53917
+ });
53918
+ }
53919
+ function StatusLine({ empty, pumpRunning }) {
53920
+ const t = useUiText();
53921
+ if (empty === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53922
+ className: "inline-flex items-center gap-1 text-xs font-medium text-danger",
53923
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, {
53924
+ className: "h-3.5 w-3.5",
53925
+ "aria-hidden": true
53926
+ }), t("uiLibrary.petFountain.empty", "Tank empty")]
53927
+ });
53928
+ if (pumpRunning === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53929
+ className: "inline-flex items-center gap-1 text-xs font-medium text-info",
53930
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplets, {
53931
+ className: "h-3.5 w-3.5",
53932
+ "aria-hidden": true
53933
+ }), t("uiLibrary.petFountain.pumpRunning", "Pump running")]
53934
+ });
53935
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53936
+ className: "inline-flex items-center gap-1 text-xs text-foreground-subtle",
53937
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, {
53938
+ className: "h-3.5 w-3.5",
53939
+ "aria-hidden": true
53940
+ }), pumpRunning === false ? t("uiLibrary.petFountain.pumpIdle", "Pump idle") : t("uiLibrary.petFountain.pumpUnknown", "Pump state unknown")]
53941
+ });
53942
+ }
53943
+ function Section$1({ label, children }) {
53944
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53945
+ className: "flex flex-col gap-1",
53946
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53947
+ className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
53948
+ children: label
53949
+ }), children]
53950
+ });
53951
+ }
53952
+ function Stat({ label, value }) {
53953
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53954
+ className: "flex flex-col rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
53955
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53956
+ className: "text-[11px] text-foreground-subtle",
53957
+ children: label
53958
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53959
+ className: "text-sm font-semibold tabular-nums text-foreground",
53960
+ children: millilitres(value)
53961
+ })]
53962
+ });
53963
+ }
53964
+ function SwitchRow({ icon, label, checked, disabled, onChange }) {
53965
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53966
+ className: "flex items-center justify-between gap-3 rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
53967
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53968
+ className: "inline-flex items-center gap-2 text-sm text-foreground",
53969
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53970
+ className: "text-foreground-subtle",
53971
+ children: icon
53972
+ }), label]
53973
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
53974
+ checked,
53975
+ disabled,
53976
+ "aria-label": label,
53977
+ onCheckedChange: onChange
53978
+ })]
53979
+ });
53980
+ }
53981
+ function MinutesStepper({ label, minutes, disabled, onChange }) {
53982
+ const t = useUiText();
53983
+ const current = minutes ?? 0;
53984
+ const step = (delta) => onChange(Math.min(MAX_PUMP_MINUTES, Math.max(0, current + delta)));
53985
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53986
+ className: "flex items-center justify-between gap-2 rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
53987
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53988
+ className: "text-sm text-foreground",
53989
+ children: label
53990
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53991
+ className: "flex items-center gap-2",
53992
+ children: [
53993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
53994
+ variant: "outline",
53995
+ size: "sm",
53996
+ "aria-label": t("uiLibrary.petFountain.decrease", "Decrease {{label}}", { label }),
53997
+ disabled: disabled || minutes === null || current <= 0,
53998
+ onClick: () => step(-1),
53999
+ children: "−"
54000
+ }),
54001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54002
+ className: "w-14 text-center text-sm font-semibold tabular-nums text-foreground",
54003
+ children: minutes === null ? "—" : t("uiLibrary.petFountain.minutes", "{{count}} min", { count: current })
54004
+ }),
54005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
54006
+ variant: "outline",
54007
+ size: "sm",
54008
+ "aria-label": t("uiLibrary.petFountain.increase", "Increase {{label}}", { label }),
54009
+ disabled: disabled || minutes === null || current >= MAX_PUMP_MINUTES,
54010
+ onClick: () => step(1),
54011
+ children: "+"
54012
+ })
54013
+ ]
54014
+ })]
54015
+ });
54016
+ }
54017
+ function PetFountainInlineControl({ trpc, deviceId, features, optimistic }) {
54018
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFountainPanel, {
54019
+ trpc,
54020
+ deviceId,
54021
+ features,
54022
+ layout: "compact",
54023
+ optimistic
54024
+ });
54025
+ }
54026
+ function PetFountainHeroCard({ trpc, deviceId, features, optimistic }) {
54027
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFountainPanel, {
54028
+ trpc,
54029
+ deviceId,
54030
+ features,
54031
+ layout: "full",
54032
+ optimistic
54033
+ });
54034
+ }
54035
+ //#endregion
53628
54036
  //#region src/composites/device-controls/dishwasher-control.tsx
53629
54037
  /**
53630
54038
  * Dishwasher controls for a `DeviceType.Dishwasher` device.
@@ -56747,6 +57155,10 @@ var CONTROL_KIND_COMPONENTS = {
56747
57155
  InlineControl: PetFeederInlineControl,
56748
57156
  HeroCard: PetFeederHeroCard
56749
57157
  },
57158
+ "pet-fountain": {
57159
+ InlineControl: PetFountainInlineControl,
57160
+ HeroCard: PetFountainHeroCard
57161
+ },
56750
57162
  dishwasher: {
56751
57163
  InlineControl: DishwasherInlineControl,
56752
57164
  HeroCard: DishwasherHeroCard
@@ -56828,6 +57240,7 @@ var CONTROL_FILLS = {
56828
57240
  [_camstack_types.DeviceType.Container]: false,
56829
57241
  [_camstack_types.DeviceType.Image]: false,
56830
57242
  [_camstack_types.DeviceType.PetFeeder]: false,
57243
+ [_camstack_types.DeviceType.PetFountain]: false,
56831
57244
  [_camstack_types.DeviceType.Dishwasher]: false,
56832
57245
  [_camstack_types.DeviceType.Washer]: false,
56833
57246
  [_camstack_types.DeviceType.Dryer]: false
@@ -60835,6 +61248,88 @@ function deselectShown(current, shown) {
60835
61248
  return new Set([...current].filter((id) => !drop.has(id)));
60836
61249
  }
60837
61250
  //#endregion
61251
+ //#region src/composites/export-entity-rows.ts
61252
+ /**
61253
+ * The action and notification-target rows of the device export table (D968).
61254
+ *
61255
+ * An action and a notifier are list entries, never devices (D954): they are
61256
+ * merged into the SAME table as extra rows, built only from the kinds the
61257
+ * exporter answered. A kinds list that was not answered is UNKNOWN, not
61258
+ * "supports nothing" (D315) — it yields devices only and no filter entry, and
61259
+ * says nothing about what the exporter cannot do.
61260
+ *
61261
+ * Pure functions, so the rules are pinned without a DOM.
61262
+ */
61263
+ /** The type-filter ids of the entity kinds, apart from every device type. */
61264
+ var ENTITY_FILTER_IDS = {
61265
+ action: "entity:action",
61266
+ notifier: "entity:notifier"
61267
+ };
61268
+ var KindsSchema = zod.z.array(_camstack_types.ExportEntityKindSchema);
61269
+ var NamedSchema = zod.z.array(zod.z.object({
61270
+ id: zod.z.string(),
61271
+ name: zod.z.string()
61272
+ }));
61273
+ var ExposedSchema = zod.z.array(_camstack_types.ExposedEntitySchema);
61274
+ /**
61275
+ * The non-device kinds the exporter answered, or null when it did not answer
61276
+ * (unknown — never an empty set).
61277
+ */
61278
+ function supportedEntityKinds(data) {
61279
+ const parsed = KindsSchema.safeParse(data);
61280
+ if (!parsed.success) return null;
61281
+ const kinds = /* @__PURE__ */ new Set();
61282
+ for (const kind of parsed.data) if (kind === "action" || kind === "notifier") kinds.add(kind);
61283
+ return kinds;
61284
+ }
61285
+ function buildEntityRows(input) {
61286
+ const { kinds } = input;
61287
+ if (kinds === null) return [];
61288
+ const exposed = ExposedSchema.safeParse(input.exposed);
61289
+ const exposedActions = /* @__PURE__ */ new Set();
61290
+ const exposedTargets = /* @__PURE__ */ new Set();
61291
+ if (exposed.success) {
61292
+ for (const row of exposed.data) if (row.entity.kind === "action") exposedActions.add(row.entity.actionId);
61293
+ else if (row.entity.kind === "notifier") exposedTargets.add(row.entity.targetId);
61294
+ }
61295
+ const rows = [];
61296
+ if (kinds.has("action")) {
61297
+ const actions = NamedSchema.safeParse(input.actions);
61298
+ for (const a of actions.success ? actions.data : []) rows.push({
61299
+ kind: "action",
61300
+ key: `action:${a.id}`,
61301
+ ref: {
61302
+ kind: "action",
61303
+ actionId: a.id
61304
+ },
61305
+ name: a.name,
61306
+ exposed: exposedActions.has(a.id)
61307
+ });
61308
+ }
61309
+ if (kinds.has("notifier")) {
61310
+ const targets = NamedSchema.safeParse(input.targets);
61311
+ for (const t of targets.success ? targets.data : []) rows.push({
61312
+ kind: "notifier",
61313
+ key: `notifier:${t.id}`,
61314
+ ref: {
61315
+ kind: "notifier",
61316
+ targetId: t.id
61317
+ },
61318
+ name: t.name,
61319
+ exposed: exposedTargets.has(t.id)
61320
+ });
61321
+ }
61322
+ return rows;
61323
+ }
61324
+ /** The type-filter ids to add beside the device types: one per SUPPORTED kind. */
61325
+ function entityFilterOptionIds(kinds) {
61326
+ if (kinds === null) return [];
61327
+ const ids = [];
61328
+ if (kinds.has("action")) ids.push(ENTITY_FILTER_IDS.action);
61329
+ if (kinds.has("notifier")) ids.push(ENTITY_FILTER_IDS.notifier);
61330
+ return ids;
61331
+ }
61332
+ //#endregion
60838
61333
  //#region src/composites/device-export-panel.tsx
60839
61334
  /**
60840
61335
  * DeviceExportPanel — generic, addon-agnostic surface for the
@@ -61054,19 +61549,28 @@ function ExportSelectCell({ device, checked, onToggle }) {
61054
61549
  * of these live in one table, and "Expose" alone names none of them.
61055
61550
  */
61056
61551
  function ExportRowToggle({ addonId, device, exposed, busy, onToggle }) {
61552
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExposeToggleButton, {
61553
+ addonId,
61554
+ label: useDeviceLabel(device, "compact"),
61555
+ exposed,
61556
+ busy,
61557
+ onToggle: (action) => onToggle(device, action)
61558
+ });
61559
+ }
61560
+ /** The Expose / Exposed button every row kind (device, action, notifier) shares. */
61561
+ function ExposeToggleButton({ addonId, label, exposed, busy, onToggle }) {
61057
61562
  const t = useUiText();
61058
- const deviceLabel = useDeviceLabel(device, "compact");
61059
61563
  const handleClick = (e) => {
61060
61564
  e.stopPropagation();
61061
61565
  if (busy) return;
61062
- onToggle(device, exposed ? "unexpose" : "expose");
61566
+ onToggle(exposed ? "unexpose" : "expose");
61063
61567
  };
61064
61568
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61065
61569
  size: "sm",
61066
61570
  variant: exposed ? "ghost" : "primary",
61067
61571
  disabled: busy,
61068
61572
  onClick: handleClick,
61069
- "aria-label": exposed ? t("uiLibrary.deviceExport.unexposeDevice", "Unexpose {{name}}", { name: deviceLabel }) : t("uiLibrary.deviceExport.exposeDevice", "Expose {{name}}", { name: deviceLabel }),
61573
+ "aria-label": exposed ? t("uiLibrary.deviceExport.unexposeDevice", "Unexpose {{name}}", { name: label }) : t("uiLibrary.deviceExport.exposeDevice", "Expose {{name}}", { name: label }),
61070
61574
  title: exposed ? t("uiLibrary.deviceExport.stopTitle", "Stop exposing to \"{{addon}}\"", { addon: addonId }) : t("uiLibrary.deviceExport.exposeTitle", "Expose to \"{{addon}}\"", { addon: addonId }),
61071
61575
  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
61576
  });
@@ -61147,11 +61651,34 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61147
61651
  addonId
61148
61652
  }, { retry: false });
61149
61653
  const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
61654
+ const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
61655
+ nodeId: "hub",
61656
+ addonId
61657
+ }, { retry: false });
61658
+ const entityKinds = (0, react$1.useMemo)(() => supportedEntityKinds(entityKindsQuery.data), [entityKindsQuery.data]);
61659
+ const exposedEntitiesQuery = useDeviceExportListExposedEntities({
61660
+ nodeId: "hub",
61661
+ addonId
61662
+ }, {
61663
+ refetchInterval: STATUS_POLL_INTERVAL_MS,
61664
+ retry: false,
61665
+ enabled: entityKinds !== null
61666
+ });
61667
+ const actionsQuery = useActionsList({}, {
61668
+ refetchInterval: FLEET_POLL_INTERVAL_MS,
61669
+ enabled: entityKinds?.has("action") === true
61670
+ });
61671
+ const targetsQuery = useNotificationOutputListTargets({}, {
61672
+ refetchInterval: FLEET_POLL_INTERVAL_MS,
61673
+ enabled: entityKinds?.has("notifier") === true
61674
+ });
61150
61675
  const invalidate = (0, react$1.useCallback)(() => {
61151
61676
  queryClient.invalidateQueries({ queryKey: [["deviceExport"]] });
61152
61677
  }, [queryClient]);
61153
61678
  const exposeMutation = useDeviceExportExposeDevice({ onSuccess: invalidate });
61154
61679
  const unexposeMutation = useDeviceExportUnexposeDevice({ onSuccess: invalidate });
61680
+ const exposeEntityMutation = useDeviceExportExposeEntity({ onSuccess: invalidate });
61681
+ const unexposeEntityMutation = useDeviceExportUnexposeEntity({ onSuccess: invalidate });
61155
61682
  const status = (0, react$1.useMemo)(() => parseStatus(statusQuery.data), [statusQuery.data]);
61156
61683
  const exposed = (0, react$1.useMemo)(() => {
61157
61684
  const parsed = ExposedDeviceArraySchema.safeParse(exposedQuery.data);
@@ -61181,10 +61708,37 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61181
61708
  }
61182
61709
  return rows;
61183
61710
  }, [devicesQuery.data, supportedKinds]);
61711
+ const entityRows = (0, react$1.useMemo)(() => buildEntityRows({
61712
+ kinds: entityKinds,
61713
+ actions: actionsQuery.data,
61714
+ targets: targetsQuery.data,
61715
+ exposed: exposedEntitiesQuery.data
61716
+ }), [
61717
+ entityKinds,
61718
+ actionsQuery.data,
61719
+ targetsQuery.data,
61720
+ exposedEntitiesQuery.data
61721
+ ]);
61722
+ const typeFilterOptions = (0, react$1.useMemo)(() => {
61723
+ const entityOptions = entityFilterOptionIds(entityKinds).map((id) => id === ENTITY_FILTER_IDS.action ? {
61724
+ id,
61725
+ label: t("uiLibrary.deviceExport.typeAction", "Action"),
61726
+ icon: Play
61727
+ } : {
61728
+ id,
61729
+ label: t("uiLibrary.deviceExport.typeNotifier", "Notifier"),
61730
+ icon: Bell
61731
+ });
61732
+ return [...supportedTypeOptions, ...entityOptions];
61733
+ }, [
61734
+ supportedTypeOptions,
61735
+ entityKinds,
61736
+ t
61737
+ ]);
61184
61738
  const linkState = status?.linkState ?? "unlinked";
61185
- const loading = statusQuery.isLoading || exposedQuery.isLoading || kindsQuery.isLoading || devicesQuery.isLoading;
61739
+ const loading = statusQuery.isLoading || exposedQuery.isLoading || kindsQuery.isLoading || devicesQuery.isLoading || entityKindsQuery.isLoading;
61186
61740
  const exposedCount = status?.exposedDeviceCount ?? exposedIds.size;
61187
- const busy = exposeMutation.isPending || unexposeMutation.isPending;
61741
+ const busy = exposeMutation.isPending || unexposeMutation.isPending || exposeEntityMutation.isPending || unexposeEntityMutation.isPending;
61188
61742
  const [selectedIds, setSelectedIds] = (0, react$1.useState)(() => /* @__PURE__ */ new Set());
61189
61743
  const [outcome, setOutcome] = (0, react$1.useState)(null);
61190
61744
  const [running, setRunning] = (0, react$1.useState)(false);
@@ -61251,10 +61805,71 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61251
61805
  labelFor,
61252
61806
  running
61253
61807
  ]);
61808
+ /**
61809
+ * Expose / unexpose ONE action or notification target. Same contract as the
61810
+ * device runner: the result is recorded either way and a refusal is named on
61811
+ * screen, with the entity (it has no device id to tag).
61812
+ */
61813
+ const runEntityExport = (0, react$1.useCallback)(async (action, row) => {
61814
+ if (running) return;
61815
+ setRunning(true);
61816
+ setOutcome(null);
61817
+ const mutation = action === "expose" ? exposeEntityMutation : unexposeEntityMutation;
61818
+ const succeeded = [];
61819
+ const failed = [];
61820
+ try {
61821
+ await mutation.mutateAsync({
61822
+ entity: row.ref,
61823
+ nodeId: "hub",
61824
+ addonId
61825
+ });
61826
+ succeeded.push(row.name);
61827
+ } catch (err) {
61828
+ const message = err instanceof Error ? err.message : String(err);
61829
+ console.error(`[device-export] ${action} ${row.kind} failed`, {
61830
+ tags: row.ref.kind === "action" ? { actionId: row.ref.actionId } : { targetId: row.ref.targetId },
61831
+ addonId,
61832
+ error: message
61833
+ });
61834
+ failed.push({
61835
+ label: row.name,
61836
+ message
61837
+ });
61838
+ }
61839
+ setOutcome({
61840
+ action,
61841
+ total: 1,
61842
+ succeeded,
61843
+ failed
61844
+ });
61845
+ setRunning(false);
61846
+ }, [
61847
+ addonId,
61848
+ exposeEntityMutation,
61849
+ unexposeEntityMutation,
61850
+ running
61851
+ ]);
61254
61852
  const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
61255
61853
  const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices), [candidateDevices]);
61256
61854
  const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices), [candidateDevices]);
61257
61855
  const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters), [candidateDevices, bulkFilters]);
61856
+ const shownRows = (0, react$1.useMemo)(() => {
61857
+ const deviceRows = shownDevices.map((device) => ({
61858
+ kind: "device",
61859
+ key: String(device.id),
61860
+ device
61861
+ }));
61862
+ const entities = bulkFilters.roles.length > 0 || bulkFilters.providers.length > 0 ? [] : entityRows.map((entity) => ({
61863
+ kind: "entity",
61864
+ key: entity.key,
61865
+ entity
61866
+ }));
61867
+ return [...deviceRows, ...entities];
61868
+ }, [
61869
+ shownDevices,
61870
+ entityRows,
61871
+ bulkFilters
61872
+ ]);
61258
61873
  const setRoleFilter = (0, react$1.useCallback)((roles) => setBulkFilters((f) => ({
61259
61874
  ...f,
61260
61875
  roles
@@ -61273,27 +61888,35 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61273
61888
  const onRowToggle = (0, react$1.useCallback)((device, action) => {
61274
61889
  runExport(action, [String(device.id)]);
61275
61890
  }, [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, []);
61891
+ const onRowClick = (0, react$1.useCallback)((row) => {
61892
+ if (row.kind === "device") onOpenDevice?.(String(row.device.id));
61893
+ }, [onOpenDevice]);
61894
+ const getKey = (0, react$1.useCallback)((row) => row.key, []);
61895
+ const getSearchText = (0, react$1.useCallback)((row) => row.kind === "device" ? `${row.device.name} ${row.device.stableId}` : row.entity.name, []);
61896
+ const getFilterType = (0, react$1.useCallback)((row) => row.kind === "device" ? row.device.type : ENTITY_FILTER_IDS[row.entity.kind], []);
61280
61897
  const columns = (0, react$1.useMemo)(() => [
61281
61898
  {
61282
61899
  key: "select",
61283
61900
  header: "",
61284
61901
  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
- })
61902
+ cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportSelectCell, {
61903
+ device: row.device,
61904
+ checked: selectedIds.has(String(row.device.id)),
61905
+ onToggle: () => toggleSelection(String(row.device.id))
61906
+ }) : null
61290
61907
  },
61291
61908
  {
61292
61909
  key: "type",
61293
61910
  header: t("uiLibrary.deviceExport.colType", "Type"),
61294
61911
  headerClassName: "w-32",
61295
- cell: (device) => {
61296
- const meta = deviceTypeMetaOf(device.type, t);
61912
+ cell: (row) => {
61913
+ const meta = row.kind === "device" ? deviceTypeMetaOf(row.device.type, t) : row.entity.kind === "action" ? {
61914
+ label: t("uiLibrary.deviceExport.typeAction", "Action"),
61915
+ icon: Play
61916
+ } : {
61917
+ label: t("uiLibrary.deviceExport.typeNotifier", "Notifier"),
61918
+ icon: Bell
61919
+ };
61297
61920
  const TypeIcon = meta.icon;
61298
61921
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
61299
61922
  className: "inline-flex items-center gap-1.5 text-[11px] text-foreground-subtle",
@@ -61304,10 +61927,13 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61304
61927
  {
61305
61928
  key: "name",
61306
61929
  header: t("uiLibrary.deviceExport.colName", "Name"),
61307
- cell: (device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceName, {
61308
- device,
61930
+ cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceName, {
61931
+ device: row.device,
61309
61932
  form: "compact",
61310
61933
  className: "text-xs font-medium text-foreground"
61934
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
61935
+ className: "text-xs font-medium text-foreground",
61936
+ children: row.entity.name
61311
61937
  })
61312
61938
  },
61313
61939
  {
@@ -61315,12 +61941,18 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61315
61941
  header: t("uiLibrary.deviceExport.colExport", "Export"),
61316
61942
  headerClassName: "w-32 text-right",
61317
61943
  cellClassName: "text-right",
61318
- cell: (device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportRowToggle, {
61944
+ cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportRowToggle, {
61319
61945
  addonId,
61320
- device,
61321
- exposed: exposedIds.has(String(device.id)),
61946
+ device: row.device,
61947
+ exposed: exposedIds.has(String(row.device.id)),
61322
61948
  busy,
61323
61949
  onToggle: onRowToggle
61950
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExposeToggleButton, {
61951
+ addonId,
61952
+ label: row.entity.name,
61953
+ exposed: row.entity.exposed,
61954
+ busy,
61955
+ onToggle: (action) => void runEntityExport(action, row.entity)
61324
61956
  })
61325
61957
  }
61326
61958
  ], [
@@ -61328,67 +61960,71 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61328
61960
  busy,
61329
61961
  exposedIds,
61330
61962
  onRowToggle,
61963
+ runEntityExport,
61331
61964
  selectedIds,
61332
61965
  toggleSelection,
61333
61966
  t
61334
61967
  ]);
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 })
61968
+ const renderToolbar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
61969
+ const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
61970
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
61971
+ className: "inline-flex flex-wrap items-center gap-1.5",
61972
+ children: [
61973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
61974
+ label: t("uiLibrary.deviceExport.filterRole", "Role"),
61975
+ options: roleOptions,
61976
+ selected: bulkFilters.roles,
61977
+ onChange: setRoleFilter
61368
61978
  }),
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 })
61979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
61980
+ label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
61981
+ options: providerOptions,
61982
+ selected: bulkFilters.providers,
61983
+ onChange: setProviderFilter
61375
61984
  }),
61376
61985
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61377
61986
  size: "sm",
61378
61987
  variant: "secondary",
61379
- disabled: busy,
61380
- onClick: unexposeSelected,
61381
- children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
61988
+ disabled: filtered.length === 0,
61989
+ onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
61990
+ children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
61382
61991
  }),
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
- }), [
61992
+ selectedIds.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
61993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
61994
+ size: "sm",
61995
+ variant: "ghost",
61996
+ onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
61997
+ children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
61998
+ }),
61999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
62000
+ "data-testid": "export-selection-count",
62001
+ className: "text-[11px] text-foreground-subtle",
62002
+ children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
62003
+ }),
62004
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
62005
+ size: "sm",
62006
+ variant: "primary",
62007
+ disabled: busy,
62008
+ onClick: exposeSelected,
62009
+ children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
62010
+ }),
62011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
62012
+ size: "sm",
62013
+ variant: "secondary",
62014
+ disabled: busy,
62015
+ onClick: unexposeSelected,
62016
+ children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
62017
+ }),
62018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
62019
+ size: "sm",
62020
+ variant: "ghost",
62021
+ onClick: clearSelection,
62022
+ children: t("uiLibrary.deviceExport.clear", "Clear")
62023
+ })
62024
+ ] })
62025
+ ]
62026
+ });
62027
+ }, [
61392
62028
  t,
61393
62029
  roleOptions,
61394
62030
  providerOptions,
@@ -61437,16 +62073,16 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
61437
62073
  }),
61438
62074
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
61439
62075
  className: "p-2 sm:p-3",
61440
- children: loading && candidateDevices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62076
+ children: loading && candidateDevices.length === 0 && entityRows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
61441
62077
  className: "flex items-center gap-2 px-1 py-6 text-xs text-foreground-subtle",
61442
62078
  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, {
62079
+ }) : hasSupportedKinds && candidateDevices.length > 0 || entityRows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceList, {
61444
62080
  mode: "generic",
61445
- items: shownDevices,
62081
+ items: shownRows,
61446
62082
  getKey,
61447
62083
  getSearchText,
61448
62084
  getFilterType,
61449
- filterOptions: supportedTypeOptions,
62085
+ filterOptions: typeFilterOptions,
61450
62086
  columns,
61451
62087
  onRowClick: onOpenDevice ? onRowClick : void 0,
61452
62088
  toolbar: renderToolbar,
@@ -63590,7 +64226,8 @@ function SectionedPage({ icon, title, subtitle, actions, sections, defaultSectio
63590
64226
  tabKey: activeId,
63591
64227
  fill,
63592
64228
  children: fill ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63593
- className: "flex min-h-0 flex-1 flex-col",
64229
+ className: "flex min-h-0 flex-1 flex-col overflow-y-auto",
64230
+ "data-testid": "sectioned-page-body",
63594
64231
  children: active?.content
63595
64232
  }) : active?.content
63596
64233
  });
@@ -71958,6 +72595,7 @@ exports.EmptyState = EmptyState;
71958
72595
  exports.EndpointResultsCard = EndpointResultsCard;
71959
72596
  exports.EndpointTestCard = EndpointTestCard;
71960
72597
  exports.EntryJsonSchemaForm = EntryJsonSchemaForm;
72598
+ exports.Eraser = Eraser;
71961
72599
  exports.ErrorBox = ErrorBox;
71962
72600
  exports.EventKindGlyph = EventKindGlyph;
71963
72601
  exports.EventStream = EventStream;
@@ -72974,12 +73612,16 @@ exports.useDeviceDiscoveryRefreshDiscovery = useDeviceDiscoveryRefreshDiscovery;
72974
73612
  exports.useDeviceDiscoveryReleaseDevice = useDeviceDiscoveryReleaseDevice;
72975
73613
  exports.useDeviceExportApplyDeviceSettingsPatch = useDeviceExportApplyDeviceSettingsPatch;
72976
73614
  exports.useDeviceExportExposeDevice = useDeviceExportExposeDevice;
73615
+ exports.useDeviceExportExposeEntity = useDeviceExportExposeEntity;
72977
73616
  exports.useDeviceExportGetDeviceLiveContribution = useDeviceExportGetDeviceLiveContribution;
72978
73617
  exports.useDeviceExportGetDeviceSettingsContribution = useDeviceExportGetDeviceSettingsContribution;
72979
73618
  exports.useDeviceExportGetStatus = useDeviceExportGetStatus;
72980
73619
  exports.useDeviceExportListExposedDevices = useDeviceExportListExposedDevices;
73620
+ exports.useDeviceExportListExposedEntities = useDeviceExportListExposedEntities;
72981
73621
  exports.useDeviceExportListSupportedDeviceKinds = useDeviceExportListSupportedDeviceKinds;
73622
+ exports.useDeviceExportListSupportedEntityKinds = useDeviceExportListSupportedEntityKinds;
72982
73623
  exports.useDeviceExportUnexposeDevice = useDeviceExportUnexposeDevice;
73624
+ exports.useDeviceExportUnexposeEntity = useDeviceExportUnexposeEntity;
72983
73625
  exports.useDeviceExtensionListCandidates = useDeviceExtensionListCandidates;
72984
73626
  exports.useDeviceExtensionListSlots = useDeviceExtensionListSlots;
72985
73627
  exports.useDeviceExtensionListSources = useDeviceExtensionListSources;
@@ -73470,6 +74112,12 @@ exports.usePetFeederSetChildLock = usePetFeederSetChildLock;
73470
74112
  exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
73471
74113
  exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
73472
74114
  exports.usePetFeederSetVolume = usePetFeederSetVolume;
74115
+ exports.usePetFountainGetStatus = usePetFountainGetStatus;
74116
+ exports.usePetFountainResetDailyConsumption = usePetFountainResetDailyConsumption;
74117
+ exports.usePetFountainSetPump = usePetFountainSetPump;
74118
+ exports.usePetFountainSetPumpInterval = usePetFountainSetPumpInterval;
74119
+ exports.usePetFountainSetPumpOffMinutes = usePetFountainSetPumpOffMinutes;
74120
+ exports.usePetFountainSetPumpOnMinutes = usePetFountainSetPumpOnMinutes;
73473
74121
  exports.usePickerSiteZones = usePickerSiteZones;
73474
74122
  exports.usePipelineAnalyticsAddRetrainFrameFromRecording = usePipelineAnalyticsAddRetrainFrameFromRecording;
73475
74123
  exports.usePipelineAnalyticsAddRetrainFramesToCollection = usePipelineAnalyticsAddRetrainFramesToCollection;
@@ -73802,9 +74450,11 @@ exports.useScrubController = useScrubController;
73802
74450
  exports.useSearchFieldHotkey = useSearchFieldHotkey;
73803
74451
  exports.useServerManagementApplyServerUpdate = useServerManagementApplyServerUpdate;
73804
74452
  exports.useServerManagementCheckServerUpdate = useServerManagementCheckServerUpdate;
74453
+ exports.useServerManagementGetLaunchOverrides = useServerManagementGetLaunchOverrides;
73805
74454
  exports.useServerManagementGetServerPackageStatus = useServerManagementGetServerPackageStatus;
73806
74455
  exports.useServerManagementRestartServer = useServerManagementRestartServer;
73807
74456
  exports.useServerManagementRollbackServerUpdate = useServerManagementRollbackServerUpdate;
74457
+ exports.useServerManagementSetLaunchOverrides = useServerManagementSetLaunchOverrides;
73808
74458
  exports.useSettingsStoreAggregate = useSettingsStoreAggregate;
73809
74459
  exports.useSettingsStoreCount = useSettingsStoreCount;
73810
74460
  exports.useSettingsStoreDeclareCollection = useSettingsStoreDeclareCollection;