@camstack/ui-library 1.1.18 → 1.1.19

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
@@ -8492,6 +8492,16 @@ var Blinds = createLucideIcon("blinds", [
8492
8492
  * This source code is licensed under the ISC license.
8493
8493
  * See the LICENSE file in the root directory of this source tree.
8494
8494
  */
8495
+ var Bone = createLucideIcon("bone", [["path", {
8496
+ d: "M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z",
8497
+ key: "w610uw"
8498
+ }]]);
8499
+ /**
8500
+ * @license lucide-react v0.576.0 - ISC
8501
+ *
8502
+ * This source code is licensed under the ISC license.
8503
+ * See the LICENSE file in the root directory of this source tree.
8504
+ */
8495
8505
  var Bot = createLucideIcon("bot", [
8496
8506
  ["path", {
8497
8507
  d: "M12 8V4H8",
@@ -10459,6 +10469,36 @@ var Pause = createLucideIcon("pause", [["rect", {
10459
10469
  * This source code is licensed under the ISC license.
10460
10470
  * See the LICENSE file in the root directory of this source tree.
10461
10471
  */
10472
+ var PawPrint = createLucideIcon("paw-print", [
10473
+ ["circle", {
10474
+ cx: "11",
10475
+ cy: "4",
10476
+ r: "2",
10477
+ key: "vol9p0"
10478
+ }],
10479
+ ["circle", {
10480
+ cx: "18",
10481
+ cy: "8",
10482
+ r: "2",
10483
+ key: "17gozi"
10484
+ }],
10485
+ ["circle", {
10486
+ cx: "20",
10487
+ cy: "16",
10488
+ r: "2",
10489
+ key: "1v9bxh"
10490
+ }],
10491
+ ["path", {
10492
+ d: "M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z",
10493
+ key: "1ydw1z"
10494
+ }]
10495
+ ]);
10496
+ /**
10497
+ * @license lucide-react v0.576.0 - ISC
10498
+ *
10499
+ * This source code is licensed under the ISC license.
10500
+ * See the LICENSE file in the root directory of this source tree.
10501
+ */
10462
10502
  var Pencil = createLucideIcon("pencil", [["path", {
10463
10503
  d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
10464
10504
  key: "1a8usu"
@@ -11658,6 +11698,30 @@ var Upload = createLucideIcon("upload", [
11658
11698
  * This source code is licensed under the ISC license.
11659
11699
  * See the LICENSE file in the root directory of this source tree.
11660
11700
  */
11701
+ var UtensilsCrossed = createLucideIcon("utensils-crossed", [
11702
+ ["path", {
11703
+ d: "m16 2-2.3 2.3a3 3 0 0 0 0 4.2l1.8 1.8a3 3 0 0 0 4.2 0L22 8",
11704
+ key: "n7qcjb"
11705
+ }],
11706
+ ["path", {
11707
+ d: "M15 15 3.3 3.3a4.2 4.2 0 0 0 0 6l7.3 7.3c.7.7 2 .7 2.8 0L15 15Zm0 0 7 7",
11708
+ key: "d0u48b"
11709
+ }],
11710
+ ["path", {
11711
+ d: "m2.1 21.8 6.4-6.3",
11712
+ key: "yn04lh"
11713
+ }],
11714
+ ["path", {
11715
+ d: "m19 5-7 7",
11716
+ key: "194lzd"
11717
+ }]
11718
+ ]);
11719
+ /**
11720
+ * @license lucide-react v0.576.0 - ISC
11721
+ *
11722
+ * This source code is licensed under the ISC license.
11723
+ * See the LICENSE file in the root directory of this source tree.
11724
+ */
11661
11725
  var Vibrate = createLucideIcon("vibrate", [
11662
11726
  ["path", {
11663
11727
  d: "m2 8 2 2-2 2 2 2-2 2",
@@ -17439,6 +17503,28 @@ var useNumericSensorGetStatus = trpc.numericSensor.getStatus.useQuery;
17439
17503
  var useOsdSetOverlay = trpc.osd.setOverlay.useMutation;
17440
17504
  /** Generated alias around `trpc.osd.getStatus.useQuery`. */
17441
17505
  var useOsdGetStatus = trpc.osd.getStatus.useQuery;
17506
+ /** Generated alias around `trpc.petFeeder.feed.useMutation`. */
17507
+ var usePetFeederFeed = trpc.petFeeder.feed.useMutation;
17508
+ /** Generated alias around `trpc.petFeeder.cancelFeed.useMutation`. */
17509
+ var usePetFeederCancelFeed = trpc.petFeeder.cancelFeed.useMutation;
17510
+ /** Generated alias around `trpc.petFeeder.resetDesiccant.useMutation`. */
17511
+ var usePetFeederResetDesiccant = trpc.petFeeder.resetDesiccant.useMutation;
17512
+ /** Generated alias around `trpc.petFeeder.markFoodReplenished.useMutation`. */
17513
+ var usePetFeederMarkFoodReplenished = trpc.petFeeder.markFoodReplenished.useMutation;
17514
+ /** Generated alias around `trpc.petFeeder.callPet.useMutation`. */
17515
+ var usePetFeederCallPet = trpc.petFeeder.callPet.useMutation;
17516
+ /** Generated alias around `trpc.petFeeder.playSound.useMutation`. */
17517
+ var usePetFeederPlaySound = trpc.petFeeder.playSound.useMutation;
17518
+ /** Generated alias around `trpc.petFeeder.setChildLock.useMutation`. */
17519
+ var usePetFeederSetChildLock = trpc.petFeeder.setChildLock.useMutation;
17520
+ /** Generated alias around `trpc.petFeeder.setIndicatorLight.useMutation`. */
17521
+ var usePetFeederSetIndicatorLight = trpc.petFeeder.setIndicatorLight.useMutation;
17522
+ /** Generated alias around `trpc.petFeeder.setFeedSound.useMutation`. */
17523
+ var usePetFeederSetFeedSound = trpc.petFeeder.setFeedSound.useMutation;
17524
+ /** Generated alias around `trpc.petFeeder.setVolume.useMutation`. */
17525
+ var usePetFeederSetVolume = trpc.petFeeder.setVolume.useMutation;
17526
+ /** Generated alias around `trpc.petFeeder.getStatus.useQuery`. */
17527
+ var usePetFeederGetStatus = trpc.petFeeder.getStatus.useQuery;
17442
17528
  /** Generated alias around `trpc.pipelineAnalytics.getActiveTracks.useQuery`. */
17443
17529
  var usePipelineAnalyticsGetActiveTracks = trpc.pipelineAnalytics.getActiveTracks.useQuery;
17444
17530
  /** Generated alias around `trpc.pipelineAnalytics.getTrack.useQuery`. */
@@ -21381,7 +21467,7 @@ function renderScene(glyph, uid) {
21381
21467
  default: return null;
21382
21468
  }
21383
21469
  }
21384
- function Readout({ icon, label, value }) {
21470
+ function Readout$1({ icon, label, value }) {
21385
21471
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21386
21472
  className: "flex items-center gap-1.5 text-xs text-foreground-subtle",
21387
21473
  title: label,
@@ -21436,12 +21522,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
21436
21522
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21437
21523
  className: "flex flex-col gap-1.5",
21438
21524
  children: [
21439
- slice.humidity !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
21525
+ slice.humidity !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
21440
21526
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplets, { className: "h-3.5 w-3.5" }),
21441
21527
  label: "Humidity",
21442
21528
  value: `${Math.round(slice.humidity)}%`
21443
21529
  }),
21444
- slice.windSpeed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
21530
+ slice.windSpeed !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
21445
21531
  icon: slice.windBearing !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Navigation, {
21446
21532
  className: "h-3.5 w-3.5",
21447
21533
  style: { transform: `rotate(${slice.windBearing}deg)` }
@@ -21449,7 +21535,7 @@ function WeatherHeroCard({ trpc, deviceId }) {
21449
21535
  label: "Wind",
21450
21536
  value: `${Math.round(slice.windSpeed)} ${slice.windSpeedUnit ?? ""}`.trim()
21451
21537
  }),
21452
- slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
21538
+ slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
21453
21539
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5" }),
21454
21540
  label: "Pressure",
21455
21541
  value: `${Math.round(slice.pressure)} ${slice.pressureUnit ?? ""}`.trim()
@@ -21662,6 +21748,522 @@ function ImageHeroCard({ trpc, deviceId, deviceName }) {
21662
21748
  });
21663
21749
  }
21664
21750
  //#endregion
21751
+ //#region src/composites/device-controls/pet-feeder-panel.tsx
21752
+ /**
21753
+ * Pet-feeder controls for a `DeviceType.PetFeeder` device.
21754
+ *
21755
+ * Reads the `pet-feeder` cap status slice (foodLevel / food1 / food2 /
21756
+ * lowFood / batteryPower / desiccantLeftDays / feeding / status / error /
21757
+ * errorCode / isDualHopper + the four persisted settings) and renders:
21758
+ * - PetFeederInlineControl (device-list row): a compact food-level +
21759
+ * battery readout with a one-tap Feed button (default portion).
21760
+ * - PetFeederHeroCard (device-detail hero): the food-bowl fill gauge
21761
+ * signature, battery + desiccant readouts, a manual-feed portion
21762
+ * stepper honouring the 4-200 g hardware range (per-hopper on
21763
+ * dual-hopper models), a maintenance action row (Call pet / Reset
21764
+ * desiccant / Mark refilled) and the child-lock / indicator-light /
21765
+ * feed-sound / volume settings.
21766
+ *
21767
+ * Mirrors the AC card's `useDeviceCapability` + inferred-cap pattern so
21768
+ * the row and hero never diverge. Actions are gated on `available`.
21769
+ */
21770
+ /** Default one-tap manual-feed portion (grams) — the low end of the
21771
+ * hardware range, a safe snack-sized dispense. */
21772
+ var DEFAULT_PORTION = 20;
21773
+ var VOLUME_MAX = 9;
21774
+ /** Amber kibble accent for the bowl fill; destructive red when low. */
21775
+ var KIBBLE = "#f59e0b";
21776
+ var KIBBLE_LOW = "#ef4444";
21777
+ function clampPortion(grams) {
21778
+ return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
21779
+ }
21780
+ var KEYFRAMES$1 = `
21781
+ @keyframes pf-eat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(2px)} }
21782
+ @keyframes pf-pulse { 0%,100%{opacity:.55} 50%{opacity:1} }
21783
+ @media (prefers-reduced-motion: reduce) {
21784
+ .pf-eat, .pf-pulse { animation: none !important; }
21785
+ }
21786
+ `;
21787
+ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
21788
+ const { cap, slice, available } = useDeviceCapability({
21789
+ trpc,
21790
+ deviceId,
21791
+ cap: (d) => d.petFeeder,
21792
+ slice: (d) => d.state.petFeeder
21793
+ });
21794
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
21795
+ const [portion, setPortion] = (0, react$1.useState)(DEFAULT_PORTION);
21796
+ const [hopper1, setHopper1] = (0, react$1.useState)(DEFAULT_PORTION);
21797
+ const [hopper2, setHopper2] = (0, react$1.useState)(DEFAULT_PORTION);
21798
+ if (cap === void 0) return null;
21799
+ if (display === void 0) return null;
21800
+ const busy = !available;
21801
+ const faultMsg = display.error && display.error !== "no_error" ? display.error : null;
21802
+ const offline = display.status === "offline";
21803
+ const onBatteries = display.status === "on_batteries";
21804
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21805
+ className: "inline-flex items-center gap-2",
21806
+ children: [
21807
+ (faultMsg !== null || offline) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21808
+ className: "inline-flex items-center text-destructive",
21809
+ title: faultMsg ?? "Offline",
21810
+ "aria-label": faultMsg ?? "Offline",
21811
+ children: offline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WifiOff, {
21812
+ className: "h-3.5 w-3.5",
21813
+ "aria-hidden": true
21814
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
21815
+ className: "h-3.5 w-3.5",
21816
+ "aria-hidden": true
21817
+ })
21818
+ }),
21819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21820
+ className: cn("inline-flex items-center gap-1 text-xs tabular-nums font-medium", display.lowFood ? "text-destructive" : "text-foreground"),
21821
+ children: [
21822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UtensilsCrossed, {
21823
+ className: "h-3.5 w-3.5",
21824
+ "aria-hidden": true
21825
+ }),
21826
+ display.foodLevel ?? "—",
21827
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21828
+ className: "text-foreground-subtle",
21829
+ children: "g"
21830
+ })
21831
+ ]
21832
+ }),
21833
+ display.batteryPower !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21834
+ className: "inline-flex items-center gap-0.5 text-xs tabular-nums text-foreground-subtle",
21835
+ children: [
21836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryMedium, {
21837
+ className: "h-3.5 w-3.5",
21838
+ "aria-hidden": true
21839
+ }),
21840
+ display.batteryPower,
21841
+ "%"
21842
+ ]
21843
+ }),
21844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
21845
+ type: "button",
21846
+ "aria-label": "Feed now",
21847
+ disabled: busy || display.feeding,
21848
+ onClick: (e) => {
21849
+ e.stopPropagation();
21850
+ if (busy || display.feeding) return;
21851
+ applyOptimistic({ feeding: true });
21852
+ cap.feed({ grams: DEFAULT_PORTION });
21853
+ },
21854
+ className: cn("inline-flex items-center gap-1 rounded-md border border-border bg-surface px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-surface-hover", (busy || display.feeding) && "opacity-40 cursor-not-allowed"),
21855
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PawPrint, {
21856
+ className: "h-3.5 w-3.5",
21857
+ "aria-hidden": true
21858
+ }), "Feed"]
21859
+ })
21860
+ ]
21861
+ });
21862
+ const feedSingle = (grams) => {
21863
+ if (busy || display.feeding) return;
21864
+ applyOptimistic({ feeding: true });
21865
+ cap.feed({ grams: clampPortion(grams) });
21866
+ };
21867
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21868
+ className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
21869
+ children: [
21870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$1 }),
21871
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21872
+ className: "flex items-center gap-4 rounded-lg border border-border/50 bg-surface-subtle/40 p-3",
21873
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BowlGauge, {
21874
+ grams: display.foodLevel,
21875
+ low: display.lowFood,
21876
+ eating: display.feeding
21877
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21878
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
21879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21880
+ className: "flex items-baseline gap-1",
21881
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21882
+ className: "text-3xl font-bold leading-none tabular-nums",
21883
+ style: { color: display.lowFood ? KIBBLE_LOW : KIBBLE },
21884
+ children: display.foodLevel ?? "—"
21885
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21886
+ className: "text-sm text-foreground-subtle",
21887
+ children: "g in bowl"
21888
+ })]
21889
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21890
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-foreground-subtle",
21891
+ children: [
21892
+ display.batteryPower !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
21893
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryMedium, { className: "h-3.5 w-3.5" }),
21894
+ label: `${display.batteryPower}%`
21895
+ }),
21896
+ display.desiccantLeftDays !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
21897
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
21898
+ label: `${display.desiccantLeftDays}d desiccant`
21899
+ }),
21900
+ onBatteries && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21901
+ className: "inline-flex items-center gap-1 font-medium text-amber-500",
21902
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryWarning, {
21903
+ className: "h-3.5 w-3.5",
21904
+ "aria-hidden": true
21905
+ }), "On battery"]
21906
+ }),
21907
+ offline ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21908
+ className: "inline-flex items-center gap-1 font-medium text-destructive",
21909
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WifiOff, {
21910
+ className: "h-3.5 w-3.5",
21911
+ "aria-hidden": true
21912
+ }), "Offline"]
21913
+ }) : display.feeding ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21914
+ className: "pf-pulse inline-flex items-center gap-1 font-medium text-amber-500",
21915
+ style: { animation: "pf-pulse 1.2s ease-in-out infinite" },
21916
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PawPrint, {
21917
+ className: "h-3.5 w-3.5",
21918
+ "aria-hidden": true
21919
+ }), "Feeding…"]
21920
+ }) : display.lowFood ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21921
+ className: "inline-flex items-center gap-1 font-medium text-destructive",
21922
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ban, {
21923
+ className: "h-3.5 w-3.5",
21924
+ "aria-hidden": true
21925
+ }), "Low food"]
21926
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
21927
+ className: "inline-flex items-center gap-1 font-medium text-success",
21928
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CircleCheck, {
21929
+ className: "h-3.5 w-3.5",
21930
+ "aria-hidden": true
21931
+ }), "Ready"]
21932
+ })
21933
+ ]
21934
+ })]
21935
+ })]
21936
+ }),
21937
+ faultMsg !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21938
+ className: "flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs font-medium text-destructive",
21939
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
21940
+ className: "mt-px h-4 w-4 shrink-0",
21941
+ "aria-hidden": true
21942
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
21943
+ className: "min-w-0 break-words",
21944
+ children: faultMsg
21945
+ })]
21946
+ }),
21947
+ display.isDualHopper ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21948
+ className: "grid grid-cols-2 gap-2",
21949
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HopperFeed, {
21950
+ label: "Hopper 1",
21951
+ grams: hopper1,
21952
+ busy: busy || display.feeding,
21953
+ onGrams: setHopper1,
21954
+ onFeed: () => {
21955
+ if (busy || display.feeding) return;
21956
+ applyOptimistic({ feeding: true });
21957
+ cap.feed({ hopper1: clampPortion(hopper1) });
21958
+ }
21959
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HopperFeed, {
21960
+ label: "Hopper 2",
21961
+ grams: hopper2,
21962
+ busy: busy || display.feeding,
21963
+ onGrams: setHopper2,
21964
+ onFeed: () => {
21965
+ if (busy || display.feeding) return;
21966
+ applyOptimistic({ feeding: true });
21967
+ cap.feed({ hopper2: clampPortion(hopper2) });
21968
+ }
21969
+ })]
21970
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HopperFeed, {
21971
+ label: "Manual feed",
21972
+ grams: portion,
21973
+ busy: busy || display.feeding,
21974
+ onGrams: setPortion,
21975
+ onFeed: () => feedSingle(portion)
21976
+ }),
21977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21978
+ className: "grid grid-cols-3 gap-2",
21979
+ children: [
21980
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionButton, {
21981
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PawPrint, { className: "h-4 w-4" }),
21982
+ label: "Call pet",
21983
+ busy,
21984
+ onClick: () => void cap.callPet({})
21985
+ }),
21986
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionButton, {
21987
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshCw, { className: "h-4 w-4" }),
21988
+ label: "Reset desiccant",
21989
+ busy,
21990
+ onClick: () => void cap.resetDesiccant({})
21991
+ }),
21992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionButton, {
21993
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UtensilsCrossed, { className: "h-4 w-4" }),
21994
+ label: "Mark refilled",
21995
+ busy,
21996
+ onClick: () => void cap.markFoodReplenished({})
21997
+ })
21998
+ ]
21999
+ }),
22000
+ display.feeding && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
22001
+ type: "button",
22002
+ disabled: busy,
22003
+ onClick: () => {
22004
+ applyOptimistic({ feeding: false });
22005
+ cap.cancelFeed({});
22006
+ },
22007
+ className: cn("flex items-center justify-center gap-2 rounded-lg border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/20", busy && "opacity-40 cursor-not-allowed"),
22008
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ban, {
22009
+ className: "h-4 w-4",
22010
+ "aria-hidden": true
22011
+ }), "Cancel feed"]
22012
+ }),
22013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field$1, {
22014
+ label: "Settings",
22015
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22016
+ className: "grid grid-cols-3 gap-2",
22017
+ children: [
22018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingToggle, {
22019
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Lock, { className: "h-4 w-4" }),
22020
+ label: "Child lock",
22021
+ on: display.childLock,
22022
+ busy,
22023
+ onToggle: (next) => {
22024
+ applyOptimistic({ childLock: next });
22025
+ cap.setChildLock({ on: next });
22026
+ }
22027
+ }),
22028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingToggle, {
22029
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Lightbulb, { className: "h-4 w-4" }),
22030
+ label: "Light",
22031
+ on: display.indicatorLight,
22032
+ busy,
22033
+ onToggle: (next) => {
22034
+ applyOptimistic({ indicatorLight: next });
22035
+ cap.setIndicatorLight({ on: next });
22036
+ }
22037
+ }),
22038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingToggle, {
22039
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bell, { className: "h-4 w-4" }),
22040
+ label: "Feed sound",
22041
+ on: display.feedSound,
22042
+ busy,
22043
+ onToggle: (next) => {
22044
+ applyOptimistic({ feedSound: next });
22045
+ cap.setFeedSound({ on: next });
22046
+ }
22047
+ })
22048
+ ]
22049
+ })
22050
+ }),
22051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field$1, {
22052
+ label: "Volume",
22053
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22054
+ className: "flex items-center gap-3",
22055
+ children: [
22056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Volume2, {
22057
+ className: "h-4 w-4 shrink-0 text-foreground-subtle",
22058
+ "aria-hidden": true
22059
+ }),
22060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
22061
+ label: "Decrease volume",
22062
+ symbol: "−",
22063
+ disabled: busy || display.volume <= 0,
22064
+ onClick: () => {
22065
+ const next = Math.max(0, display.volume - 1);
22066
+ applyOptimistic({ volume: next });
22067
+ cap.setVolume({ level: next });
22068
+ }
22069
+ }),
22070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22071
+ className: "w-6 text-center text-lg font-semibold tabular-nums text-foreground",
22072
+ children: display.volume
22073
+ }),
22074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
22075
+ label: "Increase volume",
22076
+ symbol: "+",
22077
+ disabled: busy || display.volume >= VOLUME_MAX,
22078
+ onClick: () => {
22079
+ const next = Math.min(VOLUME_MAX, display.volume + 1);
22080
+ applyOptimistic({ volume: next });
22081
+ cap.setVolume({ level: next });
22082
+ }
22083
+ })
22084
+ ]
22085
+ })
22086
+ })
22087
+ ]
22088
+ });
22089
+ }
22090
+ /** Labelled control group — a small uppercase header above its control. */
22091
+ function Field$1({ label, children }) {
22092
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22093
+ className: "flex flex-col gap-1",
22094
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22095
+ className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
22096
+ children: label
22097
+ }), children]
22098
+ });
22099
+ }
22100
+ function Readout({ icon, label }) {
22101
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
22102
+ className: "inline-flex items-center gap-1 tabular-nums",
22103
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22104
+ className: "text-foreground-subtle",
22105
+ children: icon
22106
+ }), label]
22107
+ });
22108
+ }
22109
+ /**
22110
+ * Signature element — a food bowl whose kibble fills proportionally to the
22111
+ * bowl reading (0–200 g). Amber when stocked, red when the low-food flag is
22112
+ * set; the kibble mound bobs while the pet is eating.
22113
+ */
22114
+ function BowlGauge({ grams, low, eating }) {
22115
+ const frac = Math.min(1, Math.max(0, (grams ?? 0) / _camstack_types.PET_FEEDER_MANUAL_FEED_MAX));
22116
+ const fill = low ? KIBBLE_LOW : KIBBLE;
22117
+ const surfaceY = 52 - frac * 24;
22118
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
22119
+ width: 72,
22120
+ height: 72,
22121
+ viewBox: "0 0 72 72",
22122
+ role: "img",
22123
+ "aria-label": grams === null ? "Food level unknown" : `${grams} grams in bowl`,
22124
+ className: "shrink-0",
22125
+ children: [
22126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("clipPath", {
22127
+ id: "pf-bowl-clip",
22128
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 26 H58 L52 54 A6 6 0 0 1 46 58 H26 A6 6 0 0 1 20 54 Z" })
22129
+ }) }),
22130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
22131
+ clipPath: "url(#pf-bowl-clip)",
22132
+ className: eating ? "pf-eat" : void 0,
22133
+ style: eating ? { animation: "pf-eat 1.1s ease-in-out infinite" } : void 0,
22134
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
22135
+ x: "14",
22136
+ y: surfaceY,
22137
+ width: "44",
22138
+ height: "34",
22139
+ fill,
22140
+ opacity: .9
22141
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ellipse", {
22142
+ cx: "36",
22143
+ cy: surfaceY,
22144
+ rx: "22",
22145
+ ry: "4",
22146
+ fill
22147
+ })]
22148
+ }),
22149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
22150
+ d: "M14 26 H58 L52 54 A6 6 0 0 1 46 58 H26 A6 6 0 0 1 20 54 Z",
22151
+ fill: "none",
22152
+ stroke: "currentColor",
22153
+ strokeWidth: "2.5",
22154
+ className: "text-border"
22155
+ }),
22156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
22157
+ x: "11",
22158
+ y: "22",
22159
+ width: "50",
22160
+ height: "6",
22161
+ rx: "3",
22162
+ fill: "currentColor",
22163
+ className: "text-border"
22164
+ })
22165
+ ]
22166
+ });
22167
+ }
22168
+ /** Manual-feed portion stepper + Feed button, honouring the 4-200 g range. */
22169
+ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
22170
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22171
+ className: "flex flex-col gap-1",
22172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22173
+ className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
22174
+ children: label
22175
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22176
+ className: "flex items-center gap-2",
22177
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22178
+ className: "flex items-center gap-1 rounded-lg border border-border/60 bg-surface-subtle px-1 py-1",
22179
+ children: [
22180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
22181
+ label: `Decrease ${label} portion`,
22182
+ symbol: "−",
22183
+ disabled: busy || grams <= _camstack_types.PET_FEEDER_MANUAL_FEED_MIN,
22184
+ onClick: () => onGrams(clampPortion(grams - 1))
22185
+ }),
22186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
22187
+ className: "w-12 text-center text-sm font-semibold tabular-nums text-foreground",
22188
+ children: [grams, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
22189
+ className: "text-xs text-foreground-subtle",
22190
+ children: "g"
22191
+ })]
22192
+ }),
22193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
22194
+ label: `Increase ${label} portion`,
22195
+ symbol: "+",
22196
+ disabled: busy || grams >= _camstack_types.PET_FEEDER_MANUAL_FEED_MAX,
22197
+ onClick: () => onGrams(clampPortion(grams + 1))
22198
+ })
22199
+ ]
22200
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
22201
+ type: "button",
22202
+ disabled: busy,
22203
+ onClick: onFeed,
22204
+ className: cn("flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-amber-400 px-2 py-2 text-sm font-semibold text-amber-950 transition-colors hover:bg-amber-300 active:scale-95", busy && "opacity-40 cursor-not-allowed"),
22205
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PawPrint, {
22206
+ className: "h-4 w-4",
22207
+ "aria-hidden": true
22208
+ }), "Feed"]
22209
+ })]
22210
+ })]
22211
+ });
22212
+ }
22213
+ /** Circular ± button. */
22214
+ function RoundStep$1({ label, symbol, disabled, onClick }) {
22215
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
22216
+ type: "button",
22217
+ "aria-label": label,
22218
+ disabled,
22219
+ onClick,
22220
+ className: cn("flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border bg-surface text-base leading-none text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground active:scale-95", disabled && "opacity-40 cursor-not-allowed"),
22221
+ children: symbol
22222
+ });
22223
+ }
22224
+ /** Neutral maintenance action button (icon over label). */
22225
+ function ActionButton({ icon, label, busy, onClick }) {
22226
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
22227
+ type: "button",
22228
+ disabled: busy,
22229
+ onClick,
22230
+ className: cn("flex flex-col items-center justify-center gap-1 rounded-lg border border-border/60 bg-surface-subtle px-1 py-2 text-[11px] font-medium text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground", busy && "opacity-40 cursor-not-allowed"),
22231
+ children: [icon, label]
22232
+ });
22233
+ }
22234
+ /** Setting toggle card — reflects the persisted setting; tap to flip. */
22235
+ function SettingToggle({ icon, label, on, busy, onToggle }) {
22236
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
22237
+ type: "button",
22238
+ "aria-label": label,
22239
+ "aria-pressed": on,
22240
+ disabled: busy,
22241
+ onClick: () => {
22242
+ if (!busy) onToggle(!on);
22243
+ },
22244
+ className: cn("flex flex-col items-center justify-center gap-1 rounded-lg border px-1 py-2 text-[11px] font-medium transition-colors", on ? "border-transparent bg-indigo-400 text-indigo-950 shadow-sm" : "border-border/60 bg-surface-subtle text-foreground-subtle hover:bg-surface-hover hover:text-foreground", busy && "opacity-40 cursor-not-allowed"),
22245
+ children: [icon, label]
22246
+ });
22247
+ }
22248
+ function PetFeederInlineControl({ trpc, deviceId, features, optimistic }) {
22249
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFeederPanel, {
22250
+ trpc,
22251
+ deviceId,
22252
+ features,
22253
+ layout: "compact",
22254
+ optimistic
22255
+ });
22256
+ }
22257
+ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
22258
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFeederPanel, {
22259
+ trpc,
22260
+ deviceId,
22261
+ features,
22262
+ layout: "full",
22263
+ optimistic
22264
+ });
22265
+ }
22266
+ //#endregion
21665
22267
  //#region src/composites/device-controls/switch-panel.tsx
21666
22268
  function SwitchPanel({ trpc, deviceId, optimistic }) {
21667
22269
  const { cap, slice, available } = useDeviceCapability({
@@ -24847,6 +25449,10 @@ var IMAGE_CONTROL = {
24847
25449
  InlineControl: ImageInlineControl,
24848
25450
  HeroCard: ImageHeroCard
24849
25451
  };
25452
+ var PET_FEEDER_CONTROL = {
25453
+ InlineControl: PetFeederInlineControl,
25454
+ HeroCard: PetFeederHeroCard
25455
+ };
24850
25456
  /**
24851
25457
  * Exhaustive record over DeviceType. Omitting a member causes a
24852
25458
  * TypeScript build error (same pattern as DEVICE_TYPE_ACTIONS).
@@ -24884,7 +25490,8 @@ var DEVICE_TYPE_CONTROL = {
24884
25490
  [_camstack_types.DeviceType.Vacuum]: VACUUM_CONTROL,
24885
25491
  [_camstack_types.DeviceType.LawnMower]: LAWN_MOWER_CONTROL,
24886
25492
  [_camstack_types.DeviceType.Container]: DUMMY_CONTROL,
24887
- [_camstack_types.DeviceType.Image]: IMAGE_CONTROL
25493
+ [_camstack_types.DeviceType.Image]: IMAGE_CONTROL,
25494
+ [_camstack_types.DeviceType.PetFeeder]: PET_FEEDER_CONTROL
24888
25495
  };
24889
25496
  /**
24890
25497
  * Per-type FILL flag.
@@ -24935,7 +25542,8 @@ var CONTROL_FILLS = {
24935
25542
  [_camstack_types.DeviceType.Generic]: false,
24936
25543
  [_camstack_types.DeviceType.Weather]: false,
24937
25544
  [_camstack_types.DeviceType.Container]: false,
24938
- [_camstack_types.DeviceType.Image]: false
25545
+ [_camstack_types.DeviceType.Image]: false,
25546
+ [_camstack_types.DeviceType.PetFeeder]: false
24939
25547
  };
24940
25548
  var DEVICE_TYPE_VALUES$1 = new Set(Object.values(_camstack_types.DeviceType));
24941
25549
  /**
@@ -25184,6 +25792,10 @@ var DEVICE_TYPE_META = {
25184
25792
  [_camstack_types.DeviceType.Image]: {
25185
25793
  icon: Image,
25186
25794
  label: "Image"
25795
+ },
25796
+ [_camstack_types.DeviceType.PetFeeder]: {
25797
+ icon: Bone,
25798
+ label: "Pet feeder"
25187
25799
  }
25188
25800
  };
25189
25801
  var DEVICE_ROLE_META = {
@@ -29011,7 +29623,8 @@ var CONTROL_CAP_NAMES = [
29011
29623
  "fan-control",
29012
29624
  "climate-control",
29013
29625
  "media-player",
29014
- "control"
29626
+ "control",
29627
+ "pet-feeder"
29015
29628
  ];
29016
29629
  //#endregion
29017
29630
  //#region src/composites/device-controls/popover-row-action.tsx
@@ -45028,6 +45641,17 @@ exports.useOptionalWidgetRegistry = useOptionalWidgetRegistry;
45028
45641
  exports.useOsdGetStatus = useOsdGetStatus;
45029
45642
  exports.useOsdSetOverlay = useOsdSetOverlay;
45030
45643
  exports.usePTZ = usePTZ;
45644
+ exports.usePetFeederCallPet = usePetFeederCallPet;
45645
+ exports.usePetFeederCancelFeed = usePetFeederCancelFeed;
45646
+ exports.usePetFeederFeed = usePetFeederFeed;
45647
+ exports.usePetFeederGetStatus = usePetFeederGetStatus;
45648
+ exports.usePetFeederMarkFoodReplenished = usePetFeederMarkFoodReplenished;
45649
+ exports.usePetFeederPlaySound = usePetFeederPlaySound;
45650
+ exports.usePetFeederResetDesiccant = usePetFeederResetDesiccant;
45651
+ exports.usePetFeederSetChildLock = usePetFeederSetChildLock;
45652
+ exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
45653
+ exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
45654
+ exports.usePetFeederSetVolume = usePetFeederSetVolume;
45031
45655
  exports.usePipelineAnalyticsApplyDeviceSettingsPatch = usePipelineAnalyticsApplyDeviceSettingsPatch;
45032
45656
  exports.usePipelineAnalyticsClearTracks = usePipelineAnalyticsClearTracks;
45033
45657
  exports.usePipelineAnalyticsGetActiveTracks = usePipelineAnalyticsGetActiveTracks;