@camstack/ui-library 1.2.367 → 1.2.369

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
@@ -4284,6 +4284,34 @@ var Thermometer = require_createLucideIcon.createLucideIcon("thermometer", [["pa
4284
4284
  * This source code is licensed under the ISC license.
4285
4285
  * See the LICENSE file in the root directory of this source tree.
4286
4286
  */
4287
+ var TimerOff = require_createLucideIcon.createLucideIcon("timer-off", [
4288
+ ["path", {
4289
+ d: "M10 2h4",
4290
+ key: "n1abiw"
4291
+ }],
4292
+ ["path", {
4293
+ d: "M4.6 11a8 8 0 0 0 1.7 8.7 8 8 0 0 0 8.7 1.7",
4294
+ key: "10he05"
4295
+ }],
4296
+ ["path", {
4297
+ d: "M7.4 7.4a8 8 0 0 1 10.3 1 8 8 0 0 1 .9 10.2",
4298
+ key: "15f7sh"
4299
+ }],
4300
+ ["path", {
4301
+ d: "m2 2 20 20",
4302
+ key: "1ooewy"
4303
+ }],
4304
+ ["path", {
4305
+ d: "M12 12v-2",
4306
+ key: "fwoke6"
4307
+ }]
4308
+ ]);
4309
+ /**
4310
+ * @license lucide-react v0.576.0 - ISC
4311
+ *
4312
+ * This source code is licensed under the ISC license.
4313
+ * See the LICENSE file in the root directory of this source tree.
4314
+ */
4287
4315
  var Timer = require_createLucideIcon.createLucideIcon("timer", [
4288
4316
  ["line", {
4289
4317
  x1: "10",
@@ -5456,6 +5484,11 @@ var DEVICE_TYPE_META = {
5456
5484
  labelKey: "uiLibrary.deviceMeta.type.petFountain",
5457
5485
  labelFallback: "Pet fountain"
5458
5486
  },
5487
+ [_camstack_types.DeviceType.Heater]: {
5488
+ icon: Flame,
5489
+ labelKey: "uiLibrary.deviceMeta.type.heater",
5490
+ labelFallback: "Heater"
5491
+ },
5459
5492
  [_camstack_types.DeviceType.Dishwasher]: {
5460
5493
  icon: Waves,
5461
5494
  labelKey: "uiLibrary.deviceMeta.type.dishwasher",
@@ -5961,7 +5994,7 @@ function lucideExportName(key) {
5961
5994
  function loadIconGlyphs() {
5962
5995
  if (fullTable !== void 0) return Promise.resolve();
5963
5996
  if (failure$1 !== void 0) return Promise.reject(failure$1);
5964
- pending ??= Promise.resolve().then(() => require("./icons-jbAtRNh8.cjs")).then((module) => {
5997
+ pending ??= Promise.resolve().then(() => require("./icons-DXwPEKdJ.cjs")).then((module) => {
5965
5998
  fullTable = module;
5966
5999
  publish();
5967
6000
  }, (error) => {
@@ -10694,12 +10727,6 @@ var useAddonsGetLogs = /*#__PURE__*/ alias(() => trpc.addons.getLogs.useQuery);
10694
10727
  var useAddonsListPackages = /*#__PURE__*/ alias(() => trpc.addons.listPackages.useQuery);
10695
10728
  /** Generated alias around `trpc.addons.installPackage.useMutation`. */
10696
10729
  var useAddonsInstallPackage = /*#__PURE__*/ alias(() => trpc.addons.installPackage.useMutation);
10697
- /** Generated alias around `trpc.addons.installFromWorkspace.useMutation`. */
10698
- var useAddonsInstallFromWorkspace = /*#__PURE__*/ alias(() => trpc.addons.installFromWorkspace.useMutation);
10699
- /** Generated alias around `trpc.addons.isWorkspaceAvailable.useQuery`. */
10700
- var useAddonsIsWorkspaceAvailable = /*#__PURE__*/ alias(() => trpc.addons.isWorkspaceAvailable.useQuery);
10701
- /** Generated alias around `trpc.addons.listWorkspacePackages.useQuery`. */
10702
- var useAddonsListWorkspacePackages = /*#__PURE__*/ alias(() => trpc.addons.listWorkspacePackages.useQuery);
10703
10730
  /** Generated alias around `trpc.addons.uninstallPackage.useMutation`. */
10704
10731
  var useAddonsUninstallPackage = /*#__PURE__*/ alias(() => trpc.addons.uninstallPackage.useMutation);
10705
10732
  /** Generated alias around `trpc.addons.listInstallations.useQuery`. */
@@ -11422,6 +11449,12 @@ var useDeviceProviderGetChildCreationSchema = /*#__PURE__*/ alias(() => trpc.dev
11422
11449
  var useDeviceProviderCreateDevice = /*#__PURE__*/ alias(() => trpc.deviceProvider.createDevice.useMutation);
11423
11450
  /** Generated alias around `trpc.deviceProvider.testCreationField.useMutation`. */
11424
11451
  var useDeviceProviderTestCreationField = /*#__PURE__*/ alias(() => trpc.deviceProvider.testCreationField.useMutation);
11452
+ /** Generated alias around `trpc.deviceRuns.listRuns.useQuery`. */
11453
+ var useDeviceRunsListRuns = /*#__PURE__*/ alias(() => trpc.deviceRuns.listRuns.useQuery);
11454
+ /** Generated alias around `trpc.deviceRuns.getRun.useQuery`. */
11455
+ var useDeviceRunsGetRun = /*#__PURE__*/ alias(() => trpc.deviceRuns.getRun.useQuery);
11456
+ /** Generated alias around `trpc.deviceRuns.getCurrentRun.useQuery`. */
11457
+ var useDeviceRunsGetCurrentRun = /*#__PURE__*/ alias(() => trpc.deviceRuns.getCurrentRun.useQuery);
11425
11458
  /** Generated alias around `trpc.deviceState.getSnapshot.useQuery`. */
11426
11459
  var useDeviceStateGetSnapshot = /*#__PURE__*/ alias(() => trpc.deviceState.getSnapshot.useQuery);
11427
11460
  /** Generated alias around `trpc.deviceState.getCapSlice.useQuery`. */
@@ -11526,6 +11559,14 @@ var useFanControlSetDirection = /*#__PURE__*/ alias(() => trpc.fanControl.setDir
11526
11559
  var useFanControlSetOscillating = /*#__PURE__*/ alias(() => trpc.fanControl.setOscillating.useMutation);
11527
11560
  /** Generated alias around `trpc.fanControl.getStatus.useQuery`. */
11528
11561
  var useFanControlGetStatus = /*#__PURE__*/ alias(() => trpc.fanControl.getStatus.useQuery);
11562
+ /** Generated alias around `trpc.fanPresets.getPresets.useQuery`. */
11563
+ var useFanPresetsGetPresets = /*#__PURE__*/ alias(() => trpc.fanPresets.getPresets.useQuery);
11564
+ /** Generated alias around `trpc.fanPresets.setPresets.useMutation`. */
11565
+ var useFanPresetsSetPresets = /*#__PURE__*/ alias(() => trpc.fanPresets.setPresets.useMutation);
11566
+ /** Generated alias around `trpc.fanPresets.applyPreset.useMutation`. */
11567
+ var useFanPresetsApplyPreset = /*#__PURE__*/ alias(() => trpc.fanPresets.applyPreset.useMutation);
11568
+ /** Generated alias around `trpc.fanPresets.capturePreset.useQuery`. */
11569
+ var useFanPresetsCapturePreset = /*#__PURE__*/ alias(() => trpc.fanPresets.capturePreset.useQuery);
11529
11570
  /** Generated alias around `trpc.featureProbe.getStatus.useQuery`. */
11530
11571
  var useFeatureProbeGetStatus = /*#__PURE__*/ alias(() => trpc.featureProbe.getStatus.useQuery);
11531
11572
  /** Generated alias around `trpc.flood.getStatus.useQuery`. */
@@ -13056,6 +13097,12 @@ var useTerminalSessionPullOutput = /*#__PURE__*/ alias(() => trpc.terminalSessio
13056
13097
  var useTerminalSessionWriteInput = /*#__PURE__*/ alias(() => trpc.terminalSession.writeInput.useMutation);
13057
13098
  /** Generated alias around `trpc.terminalSession.close.useMutation`. */
13058
13099
  var useTerminalSessionClose = /*#__PURE__*/ alias(() => trpc.terminalSession.close.useMutation);
13100
+ /** Generated alias around `trpc.timedRun.runFor.useMutation`. */
13101
+ var useTimedRunRunFor = /*#__PURE__*/ alias(() => trpc.timedRun.runFor.useMutation);
13102
+ /** Generated alias around `trpc.timedRun.cancel.useMutation`. */
13103
+ var useTimedRunCancel = /*#__PURE__*/ alias(() => trpc.timedRun.cancel.useMutation);
13104
+ /** Generated alias around `trpc.timedRun.getStatus.useQuery`. */
13105
+ var useTimedRunGetStatus = /*#__PURE__*/ alias(() => trpc.timedRun.getStatus.useQuery);
13059
13106
  /** Generated alias around `trpc.toast.onToast.useSubscription`. */
13060
13107
  var useToastOnToast = /*#__PURE__*/ alias(() => trpc.toast.onToast.useSubscription);
13061
13108
  /** Generated alias around `trpc.training.listBaseModels.useQuery`. */
@@ -36270,15 +36317,15 @@ function MobileDrawer({ open, onClose, children, className, width = "w-64" }) {
36270
36317
  document.body.style.overflow = "";
36271
36318
  };
36272
36319
  }, [open, onClose]);
36273
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36274
- className: cn("fixed inset-0 z-40 bg-black/50 transition-opacity duration-200", open ? "opacity-100" : "pointer-events-none opacity-0"),
36320
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36321
+ className: "fixed inset-0 z-40 bg-black/50 animate-in fade-in duration-200",
36275
36322
  onClick: onClose,
36276
36323
  "aria-hidden": "true"
36277
36324
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36278
36325
  ref: drawerRef,
36279
36326
  role: "dialog",
36280
36327
  "aria-modal": "true",
36281
- className: cn("fixed inset-y-0 left-0 z-50 flex flex-col bg-surface border-r border-border shadow-2xl transition-transform duration-200 ease-out pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", width, open ? "translate-x-0" : "-translate-x-full", className),
36328
+ className: cn("fixed inset-y-0 left-0 z-50 flex flex-col bg-surface border-r border-border shadow-2xl transition-transform duration-200 ease-out pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", width, open ? "translate-x-0" : "-translate-x-full shadow-none", className),
36282
36329
  children
36283
36330
  })] });
36284
36331
  }
@@ -53594,7 +53641,7 @@ function resolveBowlReading(kilograms, system) {
53594
53641
  function clampPortion(grams) {
53595
53642
  return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
53596
53643
  }
53597
- var KEYFRAMES$1 = `
53644
+ var KEYFRAMES$3 = `
53598
53645
  @keyframes pf-eat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(2px)} }
53599
53646
  @keyframes pf-pulse { 0%,100%{opacity:.55} 50%{opacity:1} }
53600
53647
  @media (prefers-reduced-motion: reduce) {
@@ -53685,7 +53732,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53685
53732
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53686
53733
  className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
53687
53734
  children: [
53688
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$1 }),
53735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$3 }),
53689
53736
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53690
53737
  className: "flex items-center gap-4 rounded-lg border border-border/50 bg-surface-subtle/40 p-3",
53691
53738
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BowlGauge, {
@@ -54144,6 +54191,137 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
54144
54191
  });
54145
54192
  }
54146
54193
  //#endregion
54194
+ //#region src/composites/device-controls/timed-run-control.tsx
54195
+ /**
54196
+ * "Run for N" - the inline control of the `timed-run` cap (D993).
54197
+ *
54198
+ * A number, the unit it is in, and a Run button: the hub turns the device on and
54199
+ * switches it off again after that long. While a run is armed the control shows
54200
+ * the time left (counted down from the slice's `activeUntil`) and a Cancel. The
54201
+ * maximum is the device type's own bound (`TIMED_RUN_LIMITS`); a value above it
54202
+ * is refused by the hub with a typed reason, which this control says in words.
54203
+ *
54204
+ * Rendered only when the device binds `timed-run`. An unknown slice is "nothing
54205
+ * armed", never a running timer.
54206
+ */
54207
+ var TICK_MS = 1e3;
54208
+ var SECONDS_PER_MINUTE$1 = 60;
54209
+ var MS_PER_SECOND = 1e3;
54210
+ var DEFAULT_MINUTES = 30;
54211
+ var DEFAULT_SECONDS = 60;
54212
+ /** `m:ss` for a countdown; a started run never shows a negative time. */
54213
+ function formatCountdown(remainingMs) {
54214
+ const total = Math.max(0, Math.ceil(remainingMs / MS_PER_SECOND));
54215
+ const minutes = Math.floor(total / SECONDS_PER_MINUTE$1);
54216
+ const seconds = total % SECONDS_PER_MINUTE$1;
54217
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
54218
+ }
54219
+ /** The largest value the field accepts, in the limit's unit. */
54220
+ function maxInUnit(limit) {
54221
+ return limit.unit === "min" ? limit.maxSeconds / SECONDS_PER_MINUTE$1 : limit.maxSeconds;
54222
+ }
54223
+ function useNow(active) {
54224
+ const [now, setNow] = (0, react$1.useState)(() => Date.now());
54225
+ (0, react$1.useEffect)(() => {
54226
+ if (!active) return void 0;
54227
+ setNow(Date.now());
54228
+ const handle = setInterval(() => setNow(Date.now()), TICK_MS);
54229
+ return () => clearInterval(handle);
54230
+ }, [active]);
54231
+ return now;
54232
+ }
54233
+ function TimedRunControl({ trpc, deviceId, limit }) {
54234
+ const t = useUiText();
54235
+ const { cap, slice, online } = useDeviceCapability({
54236
+ trpc,
54237
+ deviceId,
54238
+ cap: (d) => d.timedRun,
54239
+ slice: (d) => d.state.timedRun
54240
+ });
54241
+ const max = maxInUnit(limit);
54242
+ const [draft, setDraft] = (0, react$1.useState)(Math.min(max, limit.unit === "min" ? DEFAULT_MINUTES : DEFAULT_SECONDS));
54243
+ const [refusal, setRefusal] = (0, react$1.useState)(null);
54244
+ const activeUntil = slice?.activeUntil ?? null;
54245
+ const now = useNow(activeUntil !== null);
54246
+ if (cap === void 0) return null;
54247
+ const unitLabel = limit.unit === "min" ? t("uiLibrary.timedRun.minutes", "min") : t("uiLibrary.timedRun.seconds", "s");
54248
+ const valid = Number.isFinite(draft) && draft > 0 && draft <= max;
54249
+ const armed = activeUntil !== null && activeUntil > now;
54250
+ const run = () => {
54251
+ if (!valid) return;
54252
+ setRefusal(null);
54253
+ cap.runFor({
54254
+ duration: draft,
54255
+ unit: limit.unit
54256
+ }).then((result) => {
54257
+ if (!result.ok) setRefusal(result.reason);
54258
+ });
54259
+ };
54260
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54261
+ className: "flex flex-col gap-1.5",
54262
+ "data-testid": "timed-run-control",
54263
+ children: [
54264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54265
+ className: "flex items-center gap-2",
54266
+ children: [
54267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
54268
+ type: "number",
54269
+ inputMode: "numeric",
54270
+ min: 1,
54271
+ max,
54272
+ step: 1,
54273
+ value: Number.isFinite(draft) ? draft : "",
54274
+ disabled: !online,
54275
+ "aria-label": t("uiLibrary.timedRun.duration", "Run for"),
54276
+ className: "w-20 tabular-nums",
54277
+ onChange: (event) => setDraft(event.target.valueAsNumber)
54278
+ }),
54279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54280
+ className: "text-xs text-foreground-muted",
54281
+ children: unitLabel
54282
+ }),
54283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
54284
+ size: "sm",
54285
+ disabled: !online || !valid,
54286
+ onClick: run,
54287
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, {
54288
+ className: "h-4 w-4",
54289
+ "aria-hidden": true
54290
+ }), t("uiLibrary.timedRun.run", "Run")]
54291
+ }),
54292
+ armed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54293
+ className: "text-xs font-medium tabular-nums text-foreground",
54294
+ "data-testid": "timed-run-countdown",
54295
+ children: t("uiLibrary.timedRun.remaining", "{{time}} left", { time: formatCountdown(activeUntil - now) })
54296
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
54297
+ size: "sm",
54298
+ variant: "outline",
54299
+ disabled: !online,
54300
+ "aria-label": t("uiLibrary.timedRun.cancel", "Cancel the timer"),
54301
+ onClick: () => void cap.cancel({}),
54302
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimerOff, {
54303
+ className: "h-4 w-4",
54304
+ "aria-hidden": true
54305
+ })
54306
+ })] })
54307
+ ]
54308
+ }),
54309
+ !valid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54310
+ className: "text-xs text-danger",
54311
+ children: t("uiLibrary.timedRun.range", "Enter 1 to {{max}} {{unit}}", {
54312
+ max,
54313
+ unit: unitLabel
54314
+ })
54315
+ }),
54316
+ refusal !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54317
+ className: "text-xs text-danger",
54318
+ role: "alert",
54319
+ children: refusal === "over-max" ? t("uiLibrary.timedRun.overMax", "That is longer than this device may run.") : refusal === "command-failed" ? t("uiLibrary.timedRun.commandFailed", "The device did not turn on.") : t("uiLibrary.timedRun.unsupported", "This device cannot run on a timer.")
54320
+ })
54321
+ ]
54322
+ });
54323
+ }
54324
+ //#endregion
54147
54325
  //#region src/composites/device-controls/pet-fountain-panel.tsx
54148
54326
  /**
54149
54327
  * Pet-fountain controls for a `DeviceType.PetFountain` device.
@@ -54162,6 +54340,17 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
54162
54340
  * zero. Stored volumes are litres and durations seconds (D944); the card
54163
54341
  * shows millilitres and minutes. Actions are gated on `available`.
54164
54342
  */
54343
+ var KEYFRAMES$2 = `
54344
+ @keyframes pfn-ripple { 0% { transform: scale(.45); opacity: .7 } 100% { transform: scale(1.15); opacity: 0 } }
54345
+ @keyframes pfn-rise { 0% { transform: translateY(0); opacity: 0 } 20% { opacity: .8 } 100% { transform: translateY(-16px); opacity: 0 } }
54346
+ .pfn-ripple, .pfn-bubble { transform-box: fill-box; transform-origin: center; }
54347
+ .pfn-ripple { animation: pfn-ripple 2.4s ease-out infinite; }
54348
+ .pfn-bubble { animation: pfn-rise 2.2s ease-in infinite; }
54349
+ @media (prefers-reduced-motion: reduce) {
54350
+ .pfn-ripple, .pfn-bubble { animation: none !important; opacity: 0; }
54351
+ }
54352
+ `;
54353
+ var PUMP_LIMIT = _camstack_types.TIMED_RUN_LIMITS[_camstack_types.DeviceType.PetFountain];
54165
54354
  var LITRES_TO_ML = 1e3;
54166
54355
  var SECONDS_PER_MINUTE = 60;
54167
54356
  var MAX_PUMP_MINUTES = 240;
@@ -54222,25 +54411,30 @@ function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
54222
54411
  children: [
54223
54412
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54224
54413
  className: "flex items-center gap-4 rounded-lg border border-border-subtle bg-background-elevated p-3",
54225
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankGauge, {
54226
- level,
54227
- empty: display.empty === true
54228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54229
- className: "flex min-w-0 flex-1 flex-col gap-1.5",
54230
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54231
- className: "flex items-baseline gap-2",
54232
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54233
- className: cn("text-3xl font-bold leading-none tabular-nums", display.empty === true ? "text-danger" : "text-info"),
54234
- children: level === null ? "—" : `${Math.round(level)}%`
54235
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54236
- className: "text-sm tabular-nums text-foreground-muted",
54237
- children: millilitres(display.waterVolume)
54414
+ children: [
54415
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$2 }),
54416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankGauge, {
54417
+ level,
54418
+ empty: display.empty === true,
54419
+ pumping: display.pumpRunning === true
54420
+ }),
54421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54422
+ className: "flex min-w-0 flex-1 flex-col gap-1.5",
54423
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54424
+ className: "flex items-baseline gap-2",
54425
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54426
+ className: cn("text-3xl font-bold leading-none tabular-nums", display.empty === true ? "text-danger" : "text-info"),
54427
+ children: level === null ? "—" : `${Math.round(level)}%`
54428
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54429
+ className: "text-sm tabular-nums text-foreground-muted",
54430
+ children: millilitres(display.waterVolume)
54431
+ })]
54432
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusLine, {
54433
+ empty: display.empty,
54434
+ pumpRunning: display.pumpRunning
54238
54435
  })]
54239
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusLine, {
54240
- empty: display.empty,
54241
- pumpRunning: display.pumpRunning
54242
- })]
54243
- })]
54436
+ })
54437
+ ]
54244
54438
  }),
54245
54439
  fault !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54246
54440
  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",
@@ -54321,6 +54515,11 @@ function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
54321
54515
  ]
54322
54516
  })
54323
54517
  }),
54518
+ PUMP_LIMIT !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimedRunControl, {
54519
+ trpc,
54520
+ deviceId,
54521
+ limit: PUMP_LIMIT
54522
+ }),
54324
54523
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
54325
54524
  variant: "outline",
54326
54525
  size: "sm",
@@ -54335,7 +54534,7 @@ function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
54335
54534
  });
54336
54535
  }
54337
54536
  /** A tank outline whose water rises with the level; danger-coloured when empty. */
54338
- function TankGauge({ level, empty }) {
54537
+ function TankGauge({ level, empty, pumping }) {
54339
54538
  const t = useUiText();
54340
54539
  const top = 58 - Math.min(1, Math.max(0, (level ?? 0) / 100)) * 40;
54341
54540
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
@@ -54356,16 +54555,56 @@ function TankGauge({ level, empty }) {
54356
54555
  rx: "6"
54357
54556
  })
54358
54557
  }) }),
54359
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
54558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
54360
54559
  clipPath: "url(#pfn-tank-clip)",
54361
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
54560
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
54362
54561
  x: "18",
54363
54562
  y: top,
54364
54563
  width: "36",
54365
54564
  height: "46",
54366
54565
  className: empty ? "fill-danger" : "fill-info",
54367
54566
  opacity: .75
54368
- })
54567
+ }), pumping && level !== null && !empty && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
54568
+ "data-testid": "fountain-ripple",
54569
+ children: [
54570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ellipse", {
54571
+ cx: "36",
54572
+ cy: top,
54573
+ rx: "14",
54574
+ ry: "3.5",
54575
+ fill: "none",
54576
+ strokeWidth: "1.5",
54577
+ className: "pfn-ripple stroke-info"
54578
+ }),
54579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ellipse", {
54580
+ cx: "36",
54581
+ cy: top,
54582
+ rx: "14",
54583
+ ry: "3.5",
54584
+ fill: "none",
54585
+ strokeWidth: "1.5",
54586
+ className: "pfn-ripple stroke-info [animation-delay:1.2s]"
54587
+ }),
54588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
54589
+ cx: "28",
54590
+ cy: "54",
54591
+ r: "1.8",
54592
+ className: "pfn-bubble fill-info"
54593
+ }),
54594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
54595
+ cx: "38",
54596
+ cy: "56",
54597
+ r: "1.4",
54598
+ className: "pfn-bubble fill-info [animation-delay:0.8s]"
54599
+ }),
54600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
54601
+ cx: "45",
54602
+ cy: "54",
54603
+ r: "1.6",
54604
+ className: "pfn-bubble fill-info [animation-delay:1.5s]"
54605
+ })
54606
+ ]
54607
+ })]
54369
54608
  }),
54370
54609
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
54371
54610
  x: "18",
@@ -54505,661 +54744,215 @@ function PetFountainHeroCard({ trpc, deviceId, features, optimistic }) {
54505
54744
  });
54506
54745
  }
54507
54746
  //#endregion
54508
- //#region src/composites/device-controls/dishwasher-control.tsx
54509
- /**
54510
- * Dishwasher controls for a `DeviceType.Dishwasher` device.
54511
- *
54512
- * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
54513
- * mirroring vacuum-control / pet-feeder patterns.
54514
- *
54515
- * Salt and rinse aid are not here: they are items of the device's `consumables`
54516
- * cap, shown by the page's Consumables section like every device's (D915).
54517
- *
54518
- * UNKNOWN IS NOT OFF. The slice carries `null` for everything the appliance has not
54519
- * reported, and this view renders that as "—" / a disabled control, never as an unchecked
54520
- * box or a zero. It also does not echo a command before the appliance confirms it: the
54521
- * slice moves when the appliance says so.
54522
- */
54523
- /** What the operation state is called to a person, and how it is toned. */
54524
- function dishwasherOperationMeta(operationState, t = defaultUiText) {
54525
- switch (operationState) {
54526
- case "Run": return {
54527
- label: t("uiLibrary.dishwasher.op.run", "Running"),
54528
- tone: "text-primary"
54529
- };
54530
- case "Pause": return {
54531
- label: t("uiLibrary.dishwasher.op.pause", "Paused"),
54532
- tone: "text-warning"
54533
- };
54534
- case "Finished": return {
54535
- label: t("uiLibrary.dishwasher.op.finished", "Finished"),
54536
- tone: "text-success"
54537
- };
54538
- case "Error": return {
54539
- label: t("uiLibrary.dishwasher.op.error", "Error"),
54540
- tone: "text-danger"
54541
- };
54542
- case "Aborting": return {
54543
- label: t("uiLibrary.dishwasher.op.aborting", "Stopping"),
54544
- tone: "text-warning"
54545
- };
54546
- case "ActionRequired": return {
54547
- label: t("uiLibrary.dishwasher.op.actionRequired", "Action required"),
54548
- tone: "text-warning"
54549
- };
54550
- case "DelayedStart": return {
54551
- label: t("uiLibrary.dishwasher.op.delayedStart", "Delayed start"),
54552
- tone: "text-primary"
54553
- };
54554
- case "Ready": return {
54555
- label: t("uiLibrary.dishwasher.op.ready", "Ready"),
54556
- tone: "text-foreground"
54557
- };
54558
- case "Inactive": return {
54559
- label: t("uiLibrary.dishwasher.op.inactive", "Inactive"),
54560
- tone: "text-foreground-subtle"
54561
- };
54562
- case null: return {
54563
- label: t("uiLibrary.dishwasher.unknown", "Unknown"),
54564
- tone: "text-foreground-subtle"
54565
- };
54566
- }
54747
+ //#region src/composites/device-controls/ac-panel.tsx
54748
+ /** Modes that mean the device can cool, dry, or move air — an air conditioner. */
54749
+ var COOLING_MODES = new Set([
54750
+ "cool",
54751
+ "heat_cool",
54752
+ "dry",
54753
+ "fan_only"
54754
+ ]);
54755
+ /** A heater offers heat (and maybe auto/off) and none of the cooling modes. */
54756
+ function climateOffersCooling(modes) {
54757
+ return modes.some((mode) => COOLING_MODES.has(mode));
54567
54758
  }
54568
- /** The headline for the whole slice: the appliance's state, or why there is none. */
54569
- function dishwasherHeadline(status, t = defaultUiText) {
54570
- switch (status.availability) {
54571
- case "offline": return {
54572
- label: t("uiLibrary.dishwasher.offline", "Offline"),
54573
- tone: "text-foreground-subtle"
54574
- };
54575
- case "description-missing": return {
54576
- label: t("uiLibrary.dishwasher.descriptionMissing", "Waiting for the appliance profile"),
54577
- tone: "text-warning"
54578
- };
54579
- case "available": return dishwasherOperationMeta(status.operationState, t);
54759
+ /** Setpoint track bounds. Absent when the integration did not report both ends. */
54760
+ function climateSetpointBounds(status) {
54761
+ const min = status.minTemp;
54762
+ const max = status.maxTemp;
54763
+ if (typeof min !== "number" || typeof max !== "number") return null;
54764
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
54765
+ return {
54766
+ min,
54767
+ max
54768
+ };
54769
+ }
54770
+ /** Map a 0–1 drag fraction onto the setpoint, snapped to `tempStep`. */
54771
+ function climateTargetFromFraction(fraction, bounds, tempStep) {
54772
+ const step = resolveClimateTempStep(tempStep);
54773
+ const clamped = Math.min(1, Math.max(0, fraction));
54774
+ const raw = bounds.min + clamped * (bounds.max - bounds.min);
54775
+ const steps = Math.round((raw - bounds.min) / step);
54776
+ const snapped = bounds.min + steps * step;
54777
+ const rounded = Math.round(snapped * 1e3) / 1e3;
54778
+ return Math.min(bounds.max, Math.max(bounds.min, rounded));
54779
+ }
54780
+ /** AC-appropriate short mode labels. Exhaustive over HvacMode. */
54781
+ function modeLabel(t, mode) {
54782
+ switch (mode) {
54783
+ case "cool": return t("uiLibrary.acPanel.modeCool", "Cool");
54784
+ case "heat": return t("uiLibrary.acPanel.modeHeat", "Heat");
54785
+ case "auto": return t("uiLibrary.acPanel.modeAuto", "Auto");
54786
+ case "heat_cool": return t("uiLibrary.acPanel.modeHeatCool", "Heat·Cool");
54787
+ case "fan_only": return t("uiLibrary.acPanel.modeFan", "Fan");
54788
+ case "dry": return t("uiLibrary.acPanel.modeDry", "Dry");
54789
+ case "off": return t("uiLibrary.acPanel.modeOff", "Off");
54580
54790
  }
54581
54791
  }
54582
- /** Last segment of a feature key, spaced out: `Dishcare.Dishwasher.Program.Eco50` -> `Eco50`. */
54583
- function featureLabel(key) {
54584
- return (key.split(".").pop() ?? key).replace(/([a-z0-9])([A-Z])/g, "$1 $2");
54792
+ /** Per-mode icon (lucide). Exhaustive over HvacMode. */
54793
+ var MODE_ICON = {
54794
+ cool: Snowflake,
54795
+ heat: Flame,
54796
+ auto: Sparkles,
54797
+ heat_cool: Thermometer,
54798
+ fan_only: Fan,
54799
+ dry: Droplets,
54800
+ off: Power
54801
+ };
54802
+ /** Mode accent hex — drives the airflow particles, the glow, and the target
54803
+ * value colour. Mirrors the thermostat gauge palette. Exhaustive. */
54804
+ var MODE_ACCENT = {
54805
+ heat: "#f59e0b",
54806
+ cool: "#60a5fa",
54807
+ auto: "#c084fc",
54808
+ heat_cool: "#e879f9",
54809
+ fan_only: "#94a3b8",
54810
+ dry: "#22d3ee",
54811
+ off: "#a1a1aa"
54812
+ };
54813
+ /** Title-case a snake/lower token for a selector option ('medium_low' → 'Medium
54814
+ * low', 'turbo' → 'Turbo'). Shared by the fan + preset selectors. */
54815
+ function fanLabel(token) {
54816
+ const spaced = token.replace(/_/g, " ");
54817
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
54818
+ }
54819
+ /** Preset → icon. Unknown presets fall back to a neutral sparkle. */
54820
+ function presetIcon(preset) {
54821
+ switch (preset) {
54822
+ case "turbo": return Zap;
54823
+ case "quiet": return Feather;
54824
+ case "sleep": return Moon;
54825
+ case "eco": return Leaf;
54826
+ case "8c_heat": return Snowflake;
54827
+ case "none": return Ban;
54828
+ default: return Sparkles;
54829
+ }
54585
54830
  }
54586
54831
  /**
54587
- * Salt and rinse aid are consumable items (D915). Their events must not
54588
- * reappear on the hero — the Consumables section is the only surface.
54832
+ * Fan level → airflow animation period. Higher speed ⇒ shorter period. Unknown
54833
+ * strings fall back to a medium pace. Turbo (a preset) shortens it further.
54589
54834
  */
54590
- function isConsumableDishwasherEvent(event) {
54591
- const short = (event.split(".").pop() ?? event).toLowerCase();
54592
- return short.includes("salt") || short.includes("rinse");
54593
- }
54594
- /** Raised events the hero may name. Consumable events are dropped. */
54595
- function visibleDishwasherEvents(events) {
54596
- return events.filter((event) => !isConsumableDishwasherEvent(event));
54835
+ function airflowPeriodSec(fanMode) {
54836
+ switch (fanMode) {
54837
+ case "high": return 1;
54838
+ case "medium_high": return 1.3;
54839
+ case "medium": return 1.6;
54840
+ case "medium_low": return 1.9;
54841
+ case "low": return 2.2;
54842
+ default: return 1.8;
54843
+ }
54597
54844
  }
54598
- /** Whole seconds as a short duration: `1 h 24 min`, `42 min`, `30 s`. */
54599
- function formatDuration(seconds, t = defaultUiText) {
54600
- if (seconds < 60) return t("uiLibrary.dishwasher.durationSeconds", "{{n}} s", { n: seconds });
54601
- const minutes = Math.round(seconds / 60);
54602
- if (minutes < 60) return t("uiLibrary.dishwasher.durationMinutes", "{{n}} min", { n: minutes });
54603
- const hours = Math.floor(minutes / 60);
54604
- const rest = minutes % 60;
54605
- return rest === 0 ? t("uiLibrary.dishwasher.durationHours", "{{n}} h", { n: hours }) : t("uiLibrary.dishwasher.durationHoursMinutes", "{{h}} h {{m}} min", {
54606
- h: hours,
54607
- m: rest
54608
- });
54845
+ var KEYFRAMES$1 = `
54846
+ @keyframes acv-fall { 0%{transform:translateY(-1.75rem);opacity:0} 18%{opacity:.7} 82%{opacity:.7} 100%{transform:translateY(7.5rem);opacity:0} }
54847
+ @keyframes acv-rise { 0%{transform:translateY(7.5rem);opacity:0} 18%{opacity:.7} 82%{opacity:.7} 100%{transform:translateY(-1.75rem);opacity:0} }
54848
+ @keyframes acv-sway { 0%,100%{transform:translateX(-9%)} 50%{transform:translateX(9%)} }
54849
+ @keyframes acv-swing-h { 0%,100%{transform:translateX(-22%)} 50%{transform:translateX(22%)} }
54850
+ @keyframes acv-swing-v { 0%,100%{transform:translateY(-22%)} 50%{transform:translateY(22%)} }
54851
+ @keyframes acv-spin { to { transform:rotate(360deg) } }
54852
+ @keyframes acv-glow { 0%,100%{opacity:.35;transform:scale(1)} 50%{opacity:.6;transform:scale(1.06)} }
54853
+ @media (prefers-reduced-motion: reduce) {
54854
+ .acv-streak, .acv-fan, .acv-sway-wrap, .acv-glow, .acv-swing { animation: none !important; }
54609
54855
  }
54610
- function useDishwasher(trpc, deviceId) {
54611
- return useDeviceCapability({
54856
+ `;
54857
+ function AcPanel({ trpc, deviceId, layout, optimistic }) {
54858
+ const t = useUiText();
54859
+ const { cap, slice, available } = useDeviceCapability({
54612
54860
  trpc,
54613
54861
  deviceId,
54614
- cap: (d) => d.dishwasher,
54615
- slice: (d) => d.state.dishwasher
54616
- });
54617
- }
54618
- function DishwasherIconButton({ label, busy, active, onClick, children }) {
54619
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54620
- type: "button",
54621
- "aria-label": label,
54622
- title: label,
54623
- disabled: busy,
54624
- onClick: (e) => {
54625
- e.stopPropagation();
54626
- onClick();
54627
- },
54628
- className: cn("inline-flex h-5 w-5 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors", active && "bg-primary text-primary-foreground", busy && "opacity-40 cursor-not-allowed"),
54629
- children
54630
- });
54631
- }
54632
- function DishwasherGraphic({ running, progressPercent }) {
54633
- const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
54634
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54635
- "aria-hidden": "true",
54636
- className: "relative w-28 h-36 rounded border-2 border-border bg-gradient-to-b from-surface-hover to-surface overflow-hidden flex-shrink-0",
54637
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54638
- className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
54639
- style: { height: `${pct}%` }
54640
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54641
- className: "absolute inset-0 flex items-center justify-center text-foreground-subtle",
54642
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waves, { className: "h-10 w-10 opacity-70" })
54643
- })]
54862
+ cap: (d) => d.climateControl,
54863
+ slice: (d) => d.state.climateControl
54644
54864
  });
54645
- }
54646
- function DishwasherPanel({ trpc, deviceId, layout }) {
54647
- const t = useUiText();
54648
- const { cap, slice, available } = useDishwasher(trpc, deviceId);
54865
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
54649
54866
  if (cap === void 0) return null;
54650
- if (slice === void 0) return null;
54651
- const display = slice;
54652
- const link = display.availability === "available";
54653
- const busy = !available || !link;
54654
- const optionTogglesLocked = display.childLock === true;
54655
- const meta = dishwasherHeadline(display, t);
54656
- const running = display.operationState === "Run";
54657
- const paused = display.operationState === "Pause";
54658
- const startable = display.remoteStartAllowed === true && display.selectedProgram !== null;
54659
- const press = (fn) => {
54660
- if (busy) return;
54661
- fn();
54662
- };
54867
+ if (display === void 0) return null;
54868
+ const busy = !available;
54663
54869
  if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54664
- className: "inline-flex items-center gap-1.5 max-w-full",
54870
+ "data-mode": display.mode,
54871
+ className: "inline-flex items-center gap-1",
54665
54872
  children: [
54666
- display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54667
- className: "inline-flex text-warning shrink-0",
54668
- title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
54669
- "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
54670
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
54671
- }),
54672
54873
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54673
- className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
54674
- children: meta.label
54675
- }),
54676
- display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54677
- className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
54678
- title: display.program,
54679
- children: featureLabel(display.program)
54680
- }),
54681
- display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
54682
- className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
54683
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remaining, t)]
54684
- }),
54685
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
54686
- label: t("uiLibrary.dishwasher.start", "Start"),
54687
- busy: busy || !startable,
54688
- active: running,
54689
- onClick: () => press(() => cap.start({})),
54690
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
54874
+ className: cn("text-xs tabular-nums font-medium", MODE_TEXT_COLOR[display.mode]),
54875
+ children: display.target !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
54876
+ display.target,
54877
+ "°",
54878
+ display.currentTemp !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
54879
+ className: "text-foreground-subtle font-normal",
54880
+ children: [
54881
+ " ",
54882
+ "(",
54883
+ display.currentTemp,
54884
+ "°)"
54885
+ ]
54886
+ })
54887
+ ] }) : display.currentTemp !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
54888
+ className: "text-foreground-subtle",
54889
+ children: [display.currentTemp, "°"]
54890
+ }) : null
54691
54891
  }),
54692
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
54693
- label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
54694
- busy: busy || !(running || paused),
54695
- active: paused,
54696
- onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
54697
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
54892
+ display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54893
+ type: "button",
54894
+ "aria-label": t("uiLibrary.acPanel.decrease", "Decrease target"),
54895
+ disabled: busy,
54896
+ onClick: (e) => {
54897
+ e.stopPropagation();
54898
+ if (!busy && display.target !== null) {
54899
+ const next = nextClimateTarget(display.target, -1, display);
54900
+ applyOptimistic({ target: next });
54901
+ cap.setTarget({ target: next });
54902
+ }
54903
+ },
54904
+ className: cn("inline-flex h-5 w-5 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors text-xs", busy && "opacity-40 cursor-not-allowed"),
54905
+ children: "−"
54906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54907
+ type: "button",
54908
+ "aria-label": t("uiLibrary.acPanel.increase", "Increase target"),
54909
+ disabled: busy,
54910
+ onClick: (e) => {
54911
+ e.stopPropagation();
54912
+ if (!busy && display.target !== null) {
54913
+ const next = nextClimateTarget(display.target, 1, display);
54914
+ applyOptimistic({ target: next });
54915
+ cap.setTarget({ target: next });
54916
+ }
54917
+ },
54918
+ className: cn("inline-flex h-5 w-5 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors text-xs", busy && "opacity-40 cursor-not-allowed"),
54919
+ children: "+"
54920
+ })] }),
54921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
54922
+ "aria-label": t("uiLibrary.acPanel.mode", "Mode"),
54923
+ value: display.mode,
54924
+ disabled: busy,
54925
+ onChange: (e) => {
54926
+ e.stopPropagation();
54927
+ const mode = display.availableModes.find((m) => m === e.target.value);
54928
+ if (!busy && mode) {
54929
+ applyOptimistic({ mode });
54930
+ cap.setMode({ mode });
54931
+ }
54932
+ },
54933
+ className: cn("bg-transparent px-0.5 text-xs text-foreground-subtle hover:text-foreground cursor-pointer", busy && "opacity-40 cursor-not-allowed"),
54934
+ children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
54935
+ value: m,
54936
+ children: modeLabel(t, m)
54937
+ }, m))
54698
54938
  })
54699
54939
  ]
54700
54940
  });
54701
- const programs = display.availablePrograms;
54941
+ const accent = MODE_ACCENT[display.mode];
54942
+ const on = display.mode !== "off";
54943
+ const heatingOnly = !climateOffersCooling(display.availableModes);
54944
+ const setpointBounds = climateSetpointBounds(display);
54945
+ const CurrentPresetIcon = presetIcon(display.preset);
54946
+ const boost = display.preset === "turbo";
54947
+ const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
54948
+ const showVSwing = display.swingVertical !== null;
54949
+ const showHSwing = display.swingHorizontal !== null;
54950
+ const swaying = on && (display.swingVertical === true || display.swingHorizontal === true);
54702
54951
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54703
- className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
54952
+ "data-mode": display.mode,
54953
+ className: cn("relative flex flex-col items-stretch gap-2.5 overflow-hidden rounded-lg border p-3 transition-colors", MODE_COLOR[display.mode]),
54704
54954
  children: [
54705
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54706
- className: "flex items-start gap-4",
54707
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
54708
- running,
54709
- progressPercent: display.progress
54710
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54711
- className: "flex min-w-0 flex-1 flex-col gap-2",
54712
- children: [
54713
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54714
- className: "flex flex-wrap items-center gap-2",
54715
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54716
- className: cn("text-3xl font-semibold", meta.tone),
54717
- children: meta.label
54718
- }), display.powerOn === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54719
- className: "text-xs font-medium text-foreground-subtle",
54720
- children: t("uiLibrary.dishwasher.powerOff", "Power off")
54721
- })]
54722
- }),
54723
- display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54724
- className: "text-sm text-foreground-subtle",
54725
- title: display.program,
54726
- children: featureLabel(display.program)
54727
- }),
54728
- display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
54729
- className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
54730
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remaining, t) })]
54731
- }),
54732
- display.delayedStart != null && display.delayedStart > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54733
- className: "text-xs text-foreground-subtle tabular-nums",
54734
- children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStart, t) })
54735
- }),
54736
- display.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54737
- className: "flex items-center gap-2",
54738
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54739
- className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
54740
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
54741
- className: "h-full bg-primary transition-[width] duration-500",
54742
- style: { width: `${Math.max(0, Math.min(100, display.progress))}%` }
54743
- })
54744
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
54745
- className: "tabular-nums text-xs text-foreground-subtle",
54746
- children: [Math.round(display.progress), "%"]
54747
- })]
54748
- }),
54749
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54750
- className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
54751
- children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
54752
- className: "inline-flex items-center gap-1 text-xs text-warning",
54753
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
54754
- }), visibleDishwasherEvents(display.activeEvents).map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54755
- className: "text-xs text-warning",
54756
- title: event,
54757
- children: featureLabel(event)
54758
- }, event))]
54759
- }),
54760
- display.operationState === "Error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54761
- className: "inline-flex items-center gap-1 text-xs text-danger",
54762
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display.errorLabel != null ? featureLabel(display.errorLabel) : display.errorCode != null ? t("uiLibrary.dishwasher.errorCode", "Error {{code}}", { code: display.errorCode }) : t("uiLibrary.dishwasher.op.error", "Error") })]
54763
- }),
54764
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54765
- className: "flex flex-wrap items-center gap-2",
54766
- children: [
54767
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
54768
- label: t("uiLibrary.dishwasher.start", "Start"),
54769
- busy: busy || !startable,
54770
- active: running,
54771
- onClick: () => press(() => cap.start({})),
54772
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
54773
- }),
54774
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
54775
- label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
54776
- busy: busy || !(running || paused),
54777
- active: paused,
54778
- onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
54779
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
54780
- }),
54781
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
54782
- label: t("uiLibrary.dishwasher.stop", "Stop"),
54783
- busy: busy || !(running || paused),
54784
- active: false,
54785
- onClick: () => press(() => cap.stop({})),
54786
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
54787
- }),
54788
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
54789
- label: t("uiLibrary.dishwasher.power", "Power"),
54790
- busy: busy || display.powerOn === null,
54791
- active: display.powerOn === true,
54792
- onClick: () => {
54793
- const next = display.powerOn !== true;
54794
- press(() => cap.setPower({ on: next }));
54795
- },
54796
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
54797
- })
54798
- ]
54799
- })
54800
- ]
54801
- })]
54802
- }),
54803
- programs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
54804
- className: "flex items-center gap-2 text-xs text-foreground-subtle",
54805
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54806
- className: "w-16 shrink-0",
54807
- children: t("uiLibrary.dishwasher.program", "Program")
54808
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
54809
- "aria-label": t("uiLibrary.dishwasher.program", "Program"),
54810
- value: display.selectedProgram ?? "",
54811
- disabled: busy,
54812
- onChange: (e) => {
54813
- const program = e.target.value;
54814
- if (!program) return;
54815
- cap.setProgram({ program });
54816
- },
54817
- onClick: (e) => {
54818
- e.stopPropagation();
54819
- },
54820
- className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
54821
- children: [display.selectedProgram == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
54822
- value: "",
54823
- children: "—"
54824
- }), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
54825
- value: p,
54826
- children: featureLabel(p)
54827
- }, p))]
54828
- })]
54829
- }),
54830
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54831
- className: "flex flex-wrap gap-4 text-xs",
54832
- children: [
54833
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
54834
- label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
54835
- checked: display.halfLoad,
54836
- shown: link,
54837
- busy: busy || optionTogglesLocked,
54838
- onChange: (on) => void cap.setHalfLoad({ on })
54839
- }),
54840
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
54841
- label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
54842
- checked: display.hygienePlus,
54843
- shown: link,
54844
- busy: busy || optionTogglesLocked,
54845
- onChange: (on) => void cap.setHygienePlus({ on })
54846
- }),
54847
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
54848
- label: t("uiLibrary.dishwasher.extraDry", "Extra dry"),
54849
- checked: display.extraDry,
54850
- shown: link,
54851
- busy: busy || optionTogglesLocked,
54852
- onChange: (on) => void cap.setExtraDry({ on })
54853
- }),
54854
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
54855
- label: t("uiLibrary.dishwasher.intensiveZone", "Intensive zone"),
54856
- checked: display.intensiveZone,
54857
- shown: link,
54858
- busy: busy || optionTogglesLocked,
54859
- onChange: (on) => void cap.setIntensiveZone({ on })
54860
- }),
54861
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
54862
- label: t("uiLibrary.dishwasher.varioSpeedPlus", "VarioSpeed+"),
54863
- checked: display.varioSpeedPlus,
54864
- shown: link,
54865
- busy: busy || optionTogglesLocked,
54866
- onChange: (on) => void cap.setVarioSpeedPlus({ on })
54867
- }),
54868
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
54869
- label: t("uiLibrary.dishwasher.childLock", "Child lock"),
54870
- checked: display.childLock,
54871
- shown: link,
54872
- busy,
54873
- onChange: (on) => void cap.setChildLock({ on })
54874
- })
54875
- ]
54876
- })
54877
- ]
54878
- });
54879
- }
54880
- /**
54881
- * One boolean setting. `checked === null` is "the appliance has not said": on a reachable
54882
- * appliance that means it does not offer the setting (hidden); on an unreachable one the
54883
- * setting is shown but disabled and marked unknown — never as an unchecked box.
54884
- */
54885
- function ToggleRow({ label, checked, shown, busy, onChange }) {
54886
- const t = useUiText();
54887
- if (checked === null && shown) return null;
54888
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
54889
- className: "inline-flex items-center gap-2 cursor-pointer",
54890
- children: [
54891
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
54892
- type: "checkbox",
54893
- checked: checked === true,
54894
- "aria-checked": checked === null ? "mixed" : checked,
54895
- disabled: busy || checked === null,
54896
- onChange: (e) => {
54897
- onChange(e.target.checked);
54898
- },
54899
- onClick: (e) => {
54900
- e.stopPropagation();
54901
- },
54902
- className: "rounded border-border"
54903
- }),
54904
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
54905
- checked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54906
- className: "text-foreground-subtle",
54907
- children: t("uiLibrary.dishwasher.unknownValue", "—")
54908
- })
54909
- ]
54910
- });
54911
- }
54912
- function DishwasherInlineControl(props) {
54913
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
54914
- ...props,
54915
- layout: "compact"
54916
- });
54917
- }
54918
- function DishwasherHeroCard(props) {
54919
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
54920
- ...props,
54921
- layout: "full"
54922
- });
54923
- }
54924
- //#endregion
54925
- //#region src/composites/device-controls/heating-valve-readout.tsx
54926
- function formatHeatingValvePosition(position) {
54927
- return position !== null ? `${position}%` : "—";
54928
- }
54929
- function HeatingValveReadout({ trpc, deviceId, layout }) {
54930
- const t = useUiText();
54931
- const { cap, slice } = useDeviceCapability({
54932
- trpc,
54933
- deviceId,
54934
- cap: (d) => d.heatingValve,
54935
- slice: (d) => d.state.heatingValve
54936
- });
54937
- if (cap === void 0 || slice === void 0) return null;
54938
- const label = t("uiLibrary.heatingValveReadout.valve", "Valve");
54939
- const value = formatHeatingValvePosition(slice.position);
54940
- if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54941
- className: "inline-flex items-center text-xs tabular-nums text-foreground-subtle",
54942
- "aria-label": `${label} ${value}`,
54943
- children: value
54944
- });
54945
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54946
- className: "flex items-center gap-2 text-xs tabular-nums text-foreground-subtle",
54947
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54948
- className: "font-medium text-foreground",
54949
- children: value
54950
- })]
54951
- });
54952
- }
54953
- //#endregion
54954
- //#region src/composites/device-controls/ac-panel.tsx
54955
- /** Modes that mean the device can cool, dry, or move air — an air conditioner. */
54956
- var COOLING_MODES = new Set([
54957
- "cool",
54958
- "heat_cool",
54959
- "dry",
54960
- "fan_only"
54961
- ]);
54962
- /** A heater offers heat (and maybe auto/off) and none of the cooling modes. */
54963
- function climateOffersCooling(modes) {
54964
- return modes.some((mode) => COOLING_MODES.has(mode));
54965
- }
54966
- /** Setpoint track bounds. Absent when the integration did not report both ends. */
54967
- function climateSetpointBounds(status) {
54968
- const min = status.minTemp;
54969
- const max = status.maxTemp;
54970
- if (typeof min !== "number" || typeof max !== "number") return null;
54971
- if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
54972
- return {
54973
- min,
54974
- max
54975
- };
54976
- }
54977
- /** Map a 0–1 drag fraction onto the setpoint, snapped to `tempStep`. */
54978
- function climateTargetFromFraction(fraction, bounds, tempStep) {
54979
- const step = resolveClimateTempStep(tempStep);
54980
- const clamped = Math.min(1, Math.max(0, fraction));
54981
- const raw = bounds.min + clamped * (bounds.max - bounds.min);
54982
- const steps = Math.round((raw - bounds.min) / step);
54983
- const snapped = bounds.min + steps * step;
54984
- const rounded = Math.round(snapped * 1e3) / 1e3;
54985
- return Math.min(bounds.max, Math.max(bounds.min, rounded));
54986
- }
54987
- /** AC-appropriate short mode labels. Exhaustive over HvacMode. */
54988
- function modeLabel(t, mode) {
54989
- switch (mode) {
54990
- case "cool": return t("uiLibrary.acPanel.modeCool", "Cool");
54991
- case "heat": return t("uiLibrary.acPanel.modeHeat", "Heat");
54992
- case "auto": return t("uiLibrary.acPanel.modeAuto", "Auto");
54993
- case "heat_cool": return t("uiLibrary.acPanel.modeHeatCool", "Heat·Cool");
54994
- case "fan_only": return t("uiLibrary.acPanel.modeFan", "Fan");
54995
- case "dry": return t("uiLibrary.acPanel.modeDry", "Dry");
54996
- case "off": return t("uiLibrary.acPanel.modeOff", "Off");
54997
- }
54998
- }
54999
- /** Per-mode icon (lucide). Exhaustive over HvacMode. */
55000
- var MODE_ICON = {
55001
- cool: Snowflake,
55002
- heat: Flame,
55003
- auto: Sparkles,
55004
- heat_cool: Thermometer,
55005
- fan_only: Fan,
55006
- dry: Droplets,
55007
- off: Power
55008
- };
55009
- /** Mode accent hex — drives the airflow particles, the glow, and the target
55010
- * value colour. Mirrors the thermostat gauge palette. Exhaustive. */
55011
- var MODE_ACCENT = {
55012
- heat: "#f59e0b",
55013
- cool: "#60a5fa",
55014
- auto: "#c084fc",
55015
- heat_cool: "#e879f9",
55016
- fan_only: "#94a3b8",
55017
- dry: "#22d3ee",
55018
- off: "#a1a1aa"
55019
- };
55020
- /** Title-case a snake/lower token for a selector option ('medium_low' → 'Medium
55021
- * low', 'turbo' → 'Turbo'). Shared by the fan + preset selectors. */
55022
- function fanLabel(token) {
55023
- const spaced = token.replace(/_/g, " ");
55024
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
55025
- }
55026
- /** Preset → icon. Unknown presets fall back to a neutral sparkle. */
55027
- function presetIcon(preset) {
55028
- switch (preset) {
55029
- case "turbo": return Zap;
55030
- case "quiet": return Feather;
55031
- case "sleep": return Moon;
55032
- case "eco": return Leaf;
55033
- case "8c_heat": return Snowflake;
55034
- case "none": return Ban;
55035
- default: return Sparkles;
55036
- }
55037
- }
55038
- /**
55039
- * Fan level → airflow animation period. Higher speed ⇒ shorter period. Unknown
55040
- * strings fall back to a medium pace. Turbo (a preset) shortens it further.
55041
- */
55042
- function airflowPeriodSec(fanMode) {
55043
- switch (fanMode) {
55044
- case "high": return 1;
55045
- case "medium_high": return 1.3;
55046
- case "medium": return 1.6;
55047
- case "medium_low": return 1.9;
55048
- case "low": return 2.2;
55049
- default: return 1.8;
55050
- }
55051
- }
55052
- var KEYFRAMES = `
55053
- @keyframes acv-fall { 0%{transform:translateY(-1.75rem);opacity:0} 18%{opacity:.7} 82%{opacity:.7} 100%{transform:translateY(7.5rem);opacity:0} }
55054
- @keyframes acv-rise { 0%{transform:translateY(7.5rem);opacity:0} 18%{opacity:.7} 82%{opacity:.7} 100%{transform:translateY(-1.75rem);opacity:0} }
55055
- @keyframes acv-sway { 0%,100%{transform:translateX(-9%)} 50%{transform:translateX(9%)} }
55056
- @keyframes acv-swing-h { 0%,100%{transform:translateX(-22%)} 50%{transform:translateX(22%)} }
55057
- @keyframes acv-swing-v { 0%,100%{transform:translateY(-22%)} 50%{transform:translateY(22%)} }
55058
- @keyframes acv-spin { to { transform:rotate(360deg) } }
55059
- @keyframes acv-glow { 0%,100%{opacity:.35;transform:scale(1)} 50%{opacity:.6;transform:scale(1.06)} }
55060
- @media (prefers-reduced-motion: reduce) {
55061
- .acv-streak, .acv-fan, .acv-sway-wrap, .acv-glow, .acv-swing { animation: none !important; }
55062
- }
55063
- `;
55064
- function AcPanel({ trpc, deviceId, layout, optimistic }) {
55065
- const t = useUiText();
55066
- const { cap, slice, available } = useDeviceCapability({
55067
- trpc,
55068
- deviceId,
55069
- cap: (d) => d.climateControl,
55070
- slice: (d) => d.state.climateControl
55071
- });
55072
- const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
55073
- if (cap === void 0) return null;
55074
- if (display === void 0) return null;
55075
- const busy = !available;
55076
- if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55077
- "data-mode": display.mode,
55078
- className: "inline-flex items-center gap-1",
55079
- children: [
55080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55081
- className: cn("text-xs tabular-nums font-medium", MODE_TEXT_COLOR[display.mode]),
55082
- children: display.target !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
55083
- display.target,
55084
- "°",
55085
- display.currentTemp !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55086
- className: "text-foreground-subtle font-normal",
55087
- children: [
55088
- " ",
55089
- "(",
55090
- display.currentTemp,
55091
- "°)"
55092
- ]
55093
- })
55094
- ] }) : display.currentTemp !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55095
- className: "text-foreground-subtle",
55096
- children: [display.currentTemp, "°"]
55097
- }) : null
55098
- }),
55099
- display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55100
- type: "button",
55101
- "aria-label": t("uiLibrary.acPanel.decrease", "Decrease target"),
55102
- disabled: busy,
55103
- onClick: (e) => {
55104
- e.stopPropagation();
55105
- if (!busy && display.target !== null) {
55106
- const next = nextClimateTarget(display.target, -1, display);
55107
- applyOptimistic({ target: next });
55108
- cap.setTarget({ target: next });
55109
- }
55110
- },
55111
- className: cn("inline-flex h-5 w-5 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors text-xs", busy && "opacity-40 cursor-not-allowed"),
55112
- children: "−"
55113
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55114
- type: "button",
55115
- "aria-label": t("uiLibrary.acPanel.increase", "Increase target"),
55116
- disabled: busy,
55117
- onClick: (e) => {
55118
- e.stopPropagation();
55119
- if (!busy && display.target !== null) {
55120
- const next = nextClimateTarget(display.target, 1, display);
55121
- applyOptimistic({ target: next });
55122
- cap.setTarget({ target: next });
55123
- }
55124
- },
55125
- className: cn("inline-flex h-5 w-5 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors text-xs", busy && "opacity-40 cursor-not-allowed"),
55126
- children: "+"
55127
- })] }),
55128
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
55129
- "aria-label": t("uiLibrary.acPanel.mode", "Mode"),
55130
- value: display.mode,
55131
- disabled: busy,
55132
- onChange: (e) => {
55133
- e.stopPropagation();
55134
- const mode = display.availableModes.find((m) => m === e.target.value);
55135
- if (!busy && mode) {
55136
- applyOptimistic({ mode });
55137
- cap.setMode({ mode });
55138
- }
55139
- },
55140
- className: cn("bg-transparent px-0.5 text-xs text-foreground-subtle hover:text-foreground cursor-pointer", busy && "opacity-40 cursor-not-allowed"),
55141
- children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
55142
- value: m,
55143
- children: modeLabel(t, m)
55144
- }, m))
55145
- })
55146
- ]
55147
- });
55148
- const accent = MODE_ACCENT[display.mode];
55149
- const on = display.mode !== "off";
55150
- const heatingOnly = !climateOffersCooling(display.availableModes);
55151
- const setpointBounds = climateSetpointBounds(display);
55152
- const CurrentPresetIcon = presetIcon(display.preset);
55153
- const boost = display.preset === "turbo";
55154
- const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
55155
- const showVSwing = display.swingVertical !== null;
55156
- const showHSwing = display.swingHorizontal !== null;
55157
- const swaying = on && (display.swingVertical === true || display.swingHorizontal === true);
55158
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55159
- "data-mode": display.mode,
55160
- className: cn("relative flex flex-col items-stretch gap-2.5 overflow-hidden rounded-lg border p-3 transition-colors", MODE_COLOR[display.mode]),
55161
- children: [
55162
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES }),
54955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$1 }),
55163
54956
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55164
54957
  className: "relative z-10 flex flex-col items-center justify-center gap-1 overflow-hidden rounded-lg border border-border/50 bg-surface/30 px-3 py-3",
55165
54958
  children: [
@@ -55645,6 +55438,739 @@ function SwingToggle({ axis, on, busy, onToggle }) {
55645
55438
  });
55646
55439
  }
55647
55440
  //#endregion
55441
+ //#region src/composites/device-controls/heater-control.tsx
55442
+ /**
55443
+ * Heater controls for a `DeviceType.Heater` device (a dumb plug-in stove).
55444
+ *
55445
+ * A heater is a switch that heats while it is on, and the hub can switch it off
55446
+ * again after a chosen time (`timed-run`, D993):
55447
+ * - HeaterInlineControl (device-list row): the on/off switch and a one-tap
55448
+ * "Run N min".
55449
+ * - HeaterHeroCard (device-detail hero): the glowing heat backdrop (the same
55450
+ * rising streaks a heating air conditioner draws) while it is on, the
55451
+ * on/off switch, the power draw when the plug measures one, and the timed
55452
+ * run (number, unit, Run, countdown, Cancel).
55453
+ *
55454
+ * Every reading may be unknown (`null`) and is drawn as a dash, never as zero.
55455
+ */
55456
+ var HEATER_LIMIT = _camstack_types.TIMED_RUN_LIMITS[_camstack_types.DeviceType.Heater];
55457
+ var QUICK_RUN_MINUTES = 30;
55458
+ var HEAT_ACCENT = "var(--color-warning)";
55459
+ var HEAT_PERIOD_SEC = 1.8;
55460
+ function useHeater({ trpc, deviceId, optimistic }) {
55461
+ const { cap, slice, available } = useDeviceCapability({
55462
+ trpc,
55463
+ deviceId,
55464
+ cap: (d) => d.switch,
55465
+ slice: (d) => d.state.switch
55466
+ });
55467
+ const running = useDeviceCapability({
55468
+ trpc,
55469
+ deviceId,
55470
+ cap: (d) => d.runningState,
55471
+ slice: (d) => d.state.runningState
55472
+ });
55473
+ const power = useDeviceCapability({
55474
+ trpc,
55475
+ deviceId,
55476
+ cap: (d) => d.powerMeter,
55477
+ slice: (d) => d.state.powerMeter
55478
+ });
55479
+ const timer = useDeviceCapability({
55480
+ trpc,
55481
+ deviceId,
55482
+ cap: (d) => d.timedRun,
55483
+ slice: (d) => d.state.timedRun
55484
+ });
55485
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
55486
+ return {
55487
+ cap,
55488
+ display,
55489
+ available,
55490
+ applyOptimistic,
55491
+ running: running.slice,
55492
+ power: power.slice,
55493
+ timer
55494
+ };
55495
+ }
55496
+ function HeaterInlineControl(props) {
55497
+ const t = useUiText();
55498
+ const { cap, display, available, applyOptimistic, timer } = useHeater(props);
55499
+ if (cap === void 0 || display === void 0) return null;
55500
+ const on = display.on === true;
55501
+ const quickMinutes = HEATER_LIMIT === void 0 ? QUICK_RUN_MINUTES : Math.min(QUICK_RUN_MINUTES, maxInUnit(HEATER_LIMIT));
55502
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55503
+ className: "inline-flex items-center gap-2",
55504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55505
+ onClick: (e) => e.stopPropagation(),
55506
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
55507
+ checked: on,
55508
+ disabled: !available,
55509
+ "aria-label": t("uiLibrary.heater.power", "Heater"),
55510
+ onCheckedChange: (next) => {
55511
+ applyOptimistic({ on: next });
55512
+ cap.setState({ on: next });
55513
+ }
55514
+ })
55515
+ }), timer.cap !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55516
+ onClick: (e) => e.stopPropagation(),
55517
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
55518
+ size: "sm",
55519
+ variant: "outline",
55520
+ disabled: !timer.online,
55521
+ onClick: () => void timer.cap?.runFor({
55522
+ duration: quickMinutes,
55523
+ unit: "min"
55524
+ }),
55525
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, {
55526
+ className: "h-3.5 w-3.5",
55527
+ "aria-hidden": true
55528
+ }), t("uiLibrary.heater.quickRun", "Run {{minutes}} min", { minutes: quickMinutes })]
55529
+ })
55530
+ })]
55531
+ });
55532
+ }
55533
+ function HeaterHeroCard(props) {
55534
+ const t = useUiText();
55535
+ const { cap, display, available, applyOptimistic, running, power, timer } = useHeater(props);
55536
+ if (cap === void 0 || display === void 0) return null;
55537
+ const on = display.on === true;
55538
+ const heating = running?.running ?? on;
55539
+ const watts = power?.power ?? null;
55540
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55541
+ className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
55542
+ children: [
55543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$1 }),
55544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55545
+ className: "relative flex h-32 items-center justify-center overflow-hidden rounded-lg border border-border-subtle bg-background-elevated",
55546
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55547
+ className: cn("pointer-events-none absolute inset-0", heating ? "opacity-60" : "opacity-20"),
55548
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
55549
+ mode: heating ? "heat" : "off",
55550
+ accent: HEAT_ACCENT,
55551
+ periodSec: HEAT_PERIOD_SEC,
55552
+ swaying: false,
55553
+ forceRising: true
55554
+ })
55555
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55556
+ className: "relative z-10 flex flex-col items-center gap-1",
55557
+ children: [
55558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Flame, {
55559
+ className: cn("h-8 w-8", heating ? "text-warning" : "text-foreground-subtle"),
55560
+ "aria-hidden": true
55561
+ }),
55562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55563
+ className: "text-sm font-medium text-foreground",
55564
+ children: heating ? t("uiLibrary.heater.heating", "Heating") : t("uiLibrary.heater.off", "Off")
55565
+ }),
55566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55567
+ className: "text-xs tabular-nums text-foreground-muted",
55568
+ "data-testid": "heater-watts",
55569
+ children: watts === null ? "—" : `${Math.round(watts)} W`
55570
+ })
55571
+ ]
55572
+ })]
55573
+ }),
55574
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
55575
+ className: "flex items-center justify-between gap-3 text-sm text-foreground",
55576
+ children: [t("uiLibrary.heater.power", "Heater"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
55577
+ checked: on,
55578
+ disabled: !available,
55579
+ onCheckedChange: (next) => {
55580
+ applyOptimistic({ on: next });
55581
+ cap.setState({ on: next });
55582
+ }
55583
+ })]
55584
+ }),
55585
+ timer.cap !== void 0 && HEATER_LIMIT !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimedRunControl, {
55586
+ trpc: props.trpc,
55587
+ deviceId: props.deviceId,
55588
+ limit: HEATER_LIMIT
55589
+ })
55590
+ ]
55591
+ });
55592
+ }
55593
+ //#endregion
55594
+ //#region src/composites/device-controls/dishwasher-control.tsx
55595
+ /**
55596
+ * Dishwasher controls for a `DeviceType.Dishwasher` device.
55597
+ *
55598
+ * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
55599
+ * mirroring vacuum-control / pet-feeder patterns.
55600
+ *
55601
+ * Salt and rinse aid are not here: they are items of the device's `consumables`
55602
+ * cap, shown by the page's Consumables section like every device's (D915).
55603
+ *
55604
+ * UNKNOWN IS NOT OFF. The slice carries `null` for everything the appliance has not
55605
+ * reported, and this view renders that as "—" / a disabled control, never as an unchecked
55606
+ * box or a zero. It also does not echo a command before the appliance confirms it: the
55607
+ * slice moves when the appliance says so.
55608
+ */
55609
+ /** What the operation state is called to a person, and how it is toned. */
55610
+ function dishwasherOperationMeta(operationState, t = defaultUiText) {
55611
+ switch (operationState) {
55612
+ case "Run": return {
55613
+ label: t("uiLibrary.dishwasher.op.run", "Running"),
55614
+ tone: "text-primary"
55615
+ };
55616
+ case "Pause": return {
55617
+ label: t("uiLibrary.dishwasher.op.pause", "Paused"),
55618
+ tone: "text-warning"
55619
+ };
55620
+ case "Finished": return {
55621
+ label: t("uiLibrary.dishwasher.op.finished", "Finished"),
55622
+ tone: "text-success"
55623
+ };
55624
+ case "Error": return {
55625
+ label: t("uiLibrary.dishwasher.op.error", "Error"),
55626
+ tone: "text-danger"
55627
+ };
55628
+ case "Aborting": return {
55629
+ label: t("uiLibrary.dishwasher.op.aborting", "Stopping"),
55630
+ tone: "text-warning"
55631
+ };
55632
+ case "ActionRequired": return {
55633
+ label: t("uiLibrary.dishwasher.op.actionRequired", "Action required"),
55634
+ tone: "text-warning"
55635
+ };
55636
+ case "DelayedStart": return {
55637
+ label: t("uiLibrary.dishwasher.op.delayedStart", "Delayed start"),
55638
+ tone: "text-primary"
55639
+ };
55640
+ case "Ready": return {
55641
+ label: t("uiLibrary.dishwasher.op.ready", "Ready"),
55642
+ tone: "text-foreground"
55643
+ };
55644
+ case "Inactive": return {
55645
+ label: t("uiLibrary.dishwasher.op.inactive", "Inactive"),
55646
+ tone: "text-foreground-subtle"
55647
+ };
55648
+ case null: return {
55649
+ label: t("uiLibrary.dishwasher.unknown", "Unknown"),
55650
+ tone: "text-foreground-subtle"
55651
+ };
55652
+ }
55653
+ }
55654
+ /** The headline for the whole slice: the appliance's state, or why there is none. */
55655
+ function dishwasherHeadline(status, t = defaultUiText) {
55656
+ switch (status.availability) {
55657
+ case "offline": return {
55658
+ label: t("uiLibrary.dishwasher.offline", "Offline"),
55659
+ tone: "text-foreground-subtle"
55660
+ };
55661
+ case "description-missing": return {
55662
+ label: t("uiLibrary.dishwasher.descriptionMissing", "Waiting for the appliance profile"),
55663
+ tone: "text-warning"
55664
+ };
55665
+ case "available": return dishwasherOperationMeta(status.operationState, t);
55666
+ }
55667
+ }
55668
+ /** Last segment of a feature key, spaced out: `Dishcare.Dishwasher.Program.Eco50` -> `Eco50`. */
55669
+ function featureLabel(key) {
55670
+ return (key.split(".").pop() ?? key).replace(/([a-z0-9])([A-Z])/g, "$1 $2");
55671
+ }
55672
+ /**
55673
+ * Salt and rinse aid are consumable items (D915). Their events must not
55674
+ * reappear on the hero — the Consumables section is the only surface.
55675
+ */
55676
+ function isConsumableDishwasherEvent(event) {
55677
+ const short = (event.split(".").pop() ?? event).toLowerCase();
55678
+ return short.includes("salt") || short.includes("rinse");
55679
+ }
55680
+ /** Raised events the hero may name. Consumable events are dropped. */
55681
+ function visibleDishwasherEvents(events) {
55682
+ return events.filter((event) => !isConsumableDishwasherEvent(event));
55683
+ }
55684
+ /** Whole seconds as a short duration: `1 h 24 min`, `42 min`, `30 s`. */
55685
+ function formatDuration(seconds, t = defaultUiText) {
55686
+ if (seconds < 60) return t("uiLibrary.dishwasher.durationSeconds", "{{n}} s", { n: seconds });
55687
+ const minutes = Math.round(seconds / 60);
55688
+ if (minutes < 60) return t("uiLibrary.dishwasher.durationMinutes", "{{n}} min", { n: minutes });
55689
+ const hours = Math.floor(minutes / 60);
55690
+ const rest = minutes % 60;
55691
+ return rest === 0 ? t("uiLibrary.dishwasher.durationHours", "{{n}} h", { n: hours }) : t("uiLibrary.dishwasher.durationHoursMinutes", "{{h}} h {{m}} min", {
55692
+ h: hours,
55693
+ m: rest
55694
+ });
55695
+ }
55696
+ function useDishwasher(trpc, deviceId) {
55697
+ return useDeviceCapability({
55698
+ trpc,
55699
+ deviceId,
55700
+ cap: (d) => d.dishwasher,
55701
+ slice: (d) => d.state.dishwasher
55702
+ });
55703
+ }
55704
+ function DishwasherIconButton({ label, busy, active, onClick, children }) {
55705
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
55706
+ type: "button",
55707
+ "aria-label": label,
55708
+ title: label,
55709
+ disabled: busy,
55710
+ onClick: (e) => {
55711
+ e.stopPropagation();
55712
+ onClick();
55713
+ },
55714
+ className: cn("inline-flex h-5 w-5 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors", active && "bg-primary text-primary-foreground", busy && "opacity-40 cursor-not-allowed"),
55715
+ children
55716
+ });
55717
+ }
55718
+ function DishwasherGraphic({ running, progressPercent }) {
55719
+ const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
55720
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55721
+ "aria-hidden": "true",
55722
+ className: "relative w-28 h-36 rounded border-2 border-border bg-gradient-to-b from-surface-hover to-surface overflow-hidden flex-shrink-0",
55723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55724
+ className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
55725
+ style: { height: `${pct}%` }
55726
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55727
+ className: "absolute inset-0 flex items-center justify-center text-foreground-subtle",
55728
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waves, { className: "h-10 w-10 opacity-70" })
55729
+ })]
55730
+ });
55731
+ }
55732
+ function DishwasherPanel({ trpc, deviceId, layout }) {
55733
+ const t = useUiText();
55734
+ const { cap, slice, available } = useDishwasher(trpc, deviceId);
55735
+ if (cap === void 0) return null;
55736
+ if (slice === void 0) return null;
55737
+ const display = slice;
55738
+ const link = display.availability === "available";
55739
+ const busy = !available || !link;
55740
+ const optionTogglesLocked = display.childLock === true;
55741
+ const meta = dishwasherHeadline(display, t);
55742
+ const running = display.operationState === "Run";
55743
+ const paused = display.operationState === "Pause";
55744
+ const startable = display.remoteStartAllowed === true && display.selectedProgram !== null;
55745
+ const press = (fn) => {
55746
+ if (busy) return;
55747
+ fn();
55748
+ };
55749
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55750
+ className: "inline-flex items-center gap-1.5 max-w-full",
55751
+ children: [
55752
+ display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55753
+ className: "inline-flex text-warning shrink-0",
55754
+ title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
55755
+ "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
55756
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
55757
+ }),
55758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55759
+ className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
55760
+ children: meta.label
55761
+ }),
55762
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55763
+ className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
55764
+ title: display.program,
55765
+ children: featureLabel(display.program)
55766
+ }),
55767
+ display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55768
+ className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
55769
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remaining, t)]
55770
+ }),
55771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
55772
+ label: t("uiLibrary.dishwasher.start", "Start"),
55773
+ busy: busy || !startable,
55774
+ active: running,
55775
+ onClick: () => press(() => cap.start({})),
55776
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
55777
+ }),
55778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
55779
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
55780
+ busy: busy || !(running || paused),
55781
+ active: paused,
55782
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
55783
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
55784
+ })
55785
+ ]
55786
+ });
55787
+ const programs = display.availablePrograms;
55788
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55789
+ className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
55790
+ children: [
55791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55792
+ className: "flex items-start gap-4",
55793
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
55794
+ running,
55795
+ progressPercent: display.progress
55796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55797
+ className: "flex min-w-0 flex-1 flex-col gap-2",
55798
+ children: [
55799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55800
+ className: "flex flex-wrap items-center gap-2",
55801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55802
+ className: cn("text-3xl font-semibold", meta.tone),
55803
+ children: meta.label
55804
+ }), display.powerOn === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55805
+ className: "text-xs font-medium text-foreground-subtle",
55806
+ children: t("uiLibrary.dishwasher.powerOff", "Power off")
55807
+ })]
55808
+ }),
55809
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55810
+ className: "text-sm text-foreground-subtle",
55811
+ title: display.program,
55812
+ children: featureLabel(display.program)
55813
+ }),
55814
+ display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55815
+ className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
55816
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remaining, t) })]
55817
+ }),
55818
+ display.delayedStart != null && display.delayedStart > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55819
+ className: "text-xs text-foreground-subtle tabular-nums",
55820
+ children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStart, t) })
55821
+ }),
55822
+ display.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55823
+ className: "flex items-center gap-2",
55824
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55825
+ className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
55826
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55827
+ className: "h-full bg-primary transition-[width] duration-500",
55828
+ style: { width: `${Math.max(0, Math.min(100, display.progress))}%` }
55829
+ })
55830
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55831
+ className: "tabular-nums text-xs text-foreground-subtle",
55832
+ children: [Math.round(display.progress), "%"]
55833
+ })]
55834
+ }),
55835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55836
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
55837
+ children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
55838
+ className: "inline-flex items-center gap-1 text-xs text-warning",
55839
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
55840
+ }), visibleDishwasherEvents(display.activeEvents).map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55841
+ className: "text-xs text-warning",
55842
+ title: event,
55843
+ children: featureLabel(event)
55844
+ }, event))]
55845
+ }),
55846
+ display.operationState === "Error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55847
+ className: "inline-flex items-center gap-1 text-xs text-danger",
55848
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display.errorLabel != null ? featureLabel(display.errorLabel) : display.errorCode != null ? t("uiLibrary.dishwasher.errorCode", "Error {{code}}", { code: display.errorCode }) : t("uiLibrary.dishwasher.op.error", "Error") })]
55849
+ }),
55850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55851
+ className: "flex flex-wrap items-center gap-2",
55852
+ children: [
55853
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
55854
+ label: t("uiLibrary.dishwasher.start", "Start"),
55855
+ busy: busy || !startable,
55856
+ active: running,
55857
+ onClick: () => press(() => cap.start({})),
55858
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
55859
+ }),
55860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
55861
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
55862
+ busy: busy || !(running || paused),
55863
+ active: paused,
55864
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
55865
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
55866
+ }),
55867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
55868
+ label: t("uiLibrary.dishwasher.stop", "Stop"),
55869
+ busy: busy || !(running || paused),
55870
+ active: false,
55871
+ onClick: () => press(() => cap.stop({})),
55872
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
55873
+ }),
55874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
55875
+ label: t("uiLibrary.dishwasher.power", "Power"),
55876
+ busy: busy || display.powerOn === null,
55877
+ active: display.powerOn === true,
55878
+ onClick: () => {
55879
+ const next = display.powerOn !== true;
55880
+ press(() => cap.setPower({ on: next }));
55881
+ },
55882
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
55883
+ })
55884
+ ]
55885
+ })
55886
+ ]
55887
+ })]
55888
+ }),
55889
+ programs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
55890
+ className: "flex items-center gap-2 text-xs text-foreground-subtle",
55891
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55892
+ className: "w-16 shrink-0",
55893
+ children: t("uiLibrary.dishwasher.program", "Program")
55894
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
55895
+ "aria-label": t("uiLibrary.dishwasher.program", "Program"),
55896
+ value: display.selectedProgram ?? "",
55897
+ disabled: busy,
55898
+ onChange: (e) => {
55899
+ const program = e.target.value;
55900
+ if (!program) return;
55901
+ cap.setProgram({ program });
55902
+ },
55903
+ onClick: (e) => {
55904
+ e.stopPropagation();
55905
+ },
55906
+ className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
55907
+ children: [display.selectedProgram == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
55908
+ value: "",
55909
+ children: "—"
55910
+ }), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
55911
+ value: p,
55912
+ children: featureLabel(p)
55913
+ }, p))]
55914
+ })]
55915
+ }),
55916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55917
+ className: "flex flex-wrap gap-4 text-xs",
55918
+ children: [
55919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
55920
+ label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
55921
+ checked: display.halfLoad,
55922
+ shown: link,
55923
+ busy: busy || optionTogglesLocked,
55924
+ onChange: (on) => void cap.setHalfLoad({ on })
55925
+ }),
55926
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
55927
+ label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
55928
+ checked: display.hygienePlus,
55929
+ shown: link,
55930
+ busy: busy || optionTogglesLocked,
55931
+ onChange: (on) => void cap.setHygienePlus({ on })
55932
+ }),
55933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
55934
+ label: t("uiLibrary.dishwasher.extraDry", "Extra dry"),
55935
+ checked: display.extraDry,
55936
+ shown: link,
55937
+ busy: busy || optionTogglesLocked,
55938
+ onChange: (on) => void cap.setExtraDry({ on })
55939
+ }),
55940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
55941
+ label: t("uiLibrary.dishwasher.intensiveZone", "Intensive zone"),
55942
+ checked: display.intensiveZone,
55943
+ shown: link,
55944
+ busy: busy || optionTogglesLocked,
55945
+ onChange: (on) => void cap.setIntensiveZone({ on })
55946
+ }),
55947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
55948
+ label: t("uiLibrary.dishwasher.varioSpeedPlus", "VarioSpeed+"),
55949
+ checked: display.varioSpeedPlus,
55950
+ shown: link,
55951
+ busy: busy || optionTogglesLocked,
55952
+ onChange: (on) => void cap.setVarioSpeedPlus({ on })
55953
+ }),
55954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
55955
+ label: t("uiLibrary.dishwasher.childLock", "Child lock"),
55956
+ checked: display.childLock,
55957
+ shown: link,
55958
+ busy,
55959
+ onChange: (on) => void cap.setChildLock({ on })
55960
+ })
55961
+ ]
55962
+ })
55963
+ ]
55964
+ });
55965
+ }
55966
+ /**
55967
+ * One boolean setting. `checked === null` is "the appliance has not said": on a reachable
55968
+ * appliance that means it does not offer the setting (hidden); on an unreachable one the
55969
+ * setting is shown but disabled and marked unknown — never as an unchecked box.
55970
+ */
55971
+ function ToggleRow({ label, checked, shown, busy, onChange }) {
55972
+ const t = useUiText();
55973
+ if (checked === null && shown) return null;
55974
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
55975
+ className: "inline-flex items-center gap-2 cursor-pointer",
55976
+ children: [
55977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
55978
+ type: "checkbox",
55979
+ checked: checked === true,
55980
+ "aria-checked": checked === null ? "mixed" : checked,
55981
+ disabled: busy || checked === null,
55982
+ onChange: (e) => {
55983
+ onChange(e.target.checked);
55984
+ },
55985
+ onClick: (e) => {
55986
+ e.stopPropagation();
55987
+ },
55988
+ className: "rounded border-border"
55989
+ }),
55990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
55991
+ checked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55992
+ className: "text-foreground-subtle",
55993
+ children: t("uiLibrary.dishwasher.unknownValue", "—")
55994
+ })
55995
+ ]
55996
+ });
55997
+ }
55998
+ function DishwasherInlineControl(props) {
55999
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
56000
+ ...props,
56001
+ layout: "compact"
56002
+ });
56003
+ }
56004
+ function DishwasherHeroCard(props) {
56005
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
56006
+ ...props,
56007
+ layout: "full"
56008
+ });
56009
+ }
56010
+ //#endregion
56011
+ //#region src/composites/device-controls/appliance-control.tsx
56012
+ /**
56013
+ * Appliance card for a `DeviceType.Washer` / `DeviceType.Dryer` device.
56014
+ *
56015
+ * These appliances have no program surface: what is worth showing is whether
56016
+ * the cycle is running (the `running-state` cap), for how long, and what the
56017
+ * appliance draws right now (the `power-meter` cap). Both slices are read
56018
+ * straight from the device; neither is required.
56019
+ * - ApplianceInlineControl (device-list row): the running badge round the
56020
+ * drum icon, plus the live watts when there are any.
56021
+ * - ApplianceHeroCard (device-detail hero): the drum (turning only while a
56022
+ * cycle runs), the state, the phase, the dwell and the power.
56023
+ *
56024
+ * UNKNOWN IS NOT OFF (D393). `running: null` is drawn as "Unknown" and the
56025
+ * drum stays still and unbadged; a missing watt reading is a dash, never
56026
+ * "0 W". A slice that has not landed hides its row instead of faking a value.
56027
+ */
56028
+ var KEYFRAMES = `
56029
+ @keyframes apl-tumble { to { transform: rotate(360deg) } }
56030
+ .apl-drum-run { animation: apl-tumble 3s linear infinite; }
56031
+ @media (prefers-reduced-motion: reduce) {
56032
+ .apl-drum-run { animation: none !important; }
56033
+ }
56034
+ `;
56035
+ /** Whole watts, or a dash when the appliance has not reported a reading. */
56036
+ function formatWatts(power) {
56037
+ return power === void 0 || !Number.isFinite(power) ? "—" : `${Math.round(power)} W`;
56038
+ }
56039
+ /** The state to a person: running / idle, or why there is none (unknown). */
56040
+ function applianceStateLabel(running, t) {
56041
+ if (running === true) return {
56042
+ label: t("uiLibrary.appliance.running", "Running"),
56043
+ tone: "text-primary"
56044
+ };
56045
+ if (running === false) return {
56046
+ label: t("uiLibrary.appliance.idle", "Idle"),
56047
+ tone: "text-foreground"
56048
+ };
56049
+ return {
56050
+ label: t("uiLibrary.appliance.unknown", "Unknown"),
56051
+ tone: "text-foreground-subtle"
56052
+ };
56053
+ }
56054
+ function dwellText(running, since, now, t) {
56055
+ if (running !== true) return null;
56056
+ const { duration } = runningStatusParts(running, since, null, now);
56057
+ if (duration === void 0) return null;
56058
+ return "minutes" in duration ? t("uiLibrary.appliance.forMinutes", "for {{count}} min", { count: duration.minutes }) : t("uiLibrary.appliance.forHours", "for {{count}} h", { count: duration.hours });
56059
+ }
56060
+ function useApplianceState(props) {
56061
+ return {
56062
+ running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState),
56063
+ power: useDeviceState(props.trpc, props.deviceId, (s) => s.powerMeter)
56064
+ };
56065
+ }
56066
+ /** The drum: a ring with a few items in it, turning only while a cycle runs. */
56067
+ function Drum({ running }) {
56068
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
56069
+ "aria-hidden": "true",
56070
+ "data-testid": "appliance-drum",
56071
+ "data-running": running === true ? "running" : running === false ? "idle" : "unknown",
56072
+ className: "relative flex h-28 w-28 shrink-0 items-center justify-center rounded-full border-2 border-border bg-surface-hover",
56073
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56074
+ className: cn("relative h-20 w-20 rounded-full border-2", running === true ? "apl-drum-run border-primary" : "border-border"),
56075
+ "data-testid": "appliance-drum-ring",
56076
+ children: [
56077
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "absolute left-2 top-4 h-3 w-3 rounded-full bg-primary/40" }),
56078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "absolute bottom-3 right-3 h-4 w-4 rounded-full bg-info/40" }),
56079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "absolute bottom-4 left-4 h-2.5 w-2.5 rounded-full bg-foreground-subtle/40" })
56080
+ ]
56081
+ })
56082
+ });
56083
+ }
56084
+ function ApplianceHeroCard(props) {
56085
+ const t = useUiText();
56086
+ const { running, power } = useApplianceState(props);
56087
+ const state = running?.running ?? null;
56088
+ const meta = applianceStateLabel(state, t);
56089
+ const phase = running?.phase?.trim() ?? "";
56090
+ const dwell = running === void 0 ? null : dwellText(state, running.since, Date.now(), t);
56091
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56092
+ className: "flex items-center gap-4 rounded-lg border border-border bg-surface p-4",
56093
+ children: [
56094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES }),
56095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Drum, { running: state }),
56096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56097
+ className: "flex min-w-0 flex-1 flex-col gap-2",
56098
+ children: [
56099
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56100
+ className: "flex flex-wrap items-baseline gap-x-2",
56101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56102
+ "data-testid": "appliance-state",
56103
+ className: cn("text-3xl font-semibold", meta.tone),
56104
+ children: meta.label
56105
+ }), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56106
+ className: "text-sm tabular-nums text-foreground-subtle",
56107
+ children: dwell
56108
+ })]
56109
+ }),
56110
+ phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56111
+ className: "text-sm text-foreground-subtle",
56112
+ children: phase
56113
+ }),
56114
+ power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
56115
+ "data-testid": "appliance-power",
56116
+ className: "inline-flex items-center gap-1 text-sm tabular-nums text-foreground-muted",
56117
+ title: power.power === void 0 ? t("uiLibrary.appliance.powerUnknown", "Power unknown") : void 0,
56118
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
56119
+ className: "h-3.5 w-3.5",
56120
+ "aria-hidden": true
56121
+ }), formatWatts(power.power)]
56122
+ })
56123
+ ]
56124
+ })
56125
+ ]
56126
+ });
56127
+ }
56128
+ function ApplianceInlineControl(props) {
56129
+ const { running, power } = useApplianceState(props);
56130
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56131
+ className: "inline-flex items-center gap-1.5 max-w-full",
56132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56133
+ running: running?.running ?? null,
56134
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WashingMachine, {
56135
+ className: "h-4 w-4 text-foreground-muted",
56136
+ "aria-hidden": true
56137
+ })
56138
+ }), power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56139
+ className: "text-xs tabular-nums text-foreground-subtle",
56140
+ children: formatWatts(power.power)
56141
+ })]
56142
+ });
56143
+ }
56144
+ //#endregion
56145
+ //#region src/composites/device-controls/heating-valve-readout.tsx
56146
+ function formatHeatingValvePosition(position) {
56147
+ return position !== null ? `${position}%` : "—";
56148
+ }
56149
+ function HeatingValveReadout({ trpc, deviceId, layout }) {
56150
+ const t = useUiText();
56151
+ const { cap, slice } = useDeviceCapability({
56152
+ trpc,
56153
+ deviceId,
56154
+ cap: (d) => d.heatingValve,
56155
+ slice: (d) => d.state.heatingValve
56156
+ });
56157
+ if (cap === void 0 || slice === void 0) return null;
56158
+ const label = t("uiLibrary.heatingValveReadout.valve", "Valve");
56159
+ const value = formatHeatingValvePosition(slice.position);
56160
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56161
+ className: "inline-flex items-center text-xs tabular-nums text-foreground-subtle",
56162
+ "aria-label": `${label} ${value}`,
56163
+ children: value
56164
+ });
56165
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56166
+ className: "flex items-center gap-2 text-xs tabular-nums text-foreground-subtle",
56167
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56168
+ className: "font-medium text-foreground",
56169
+ children: value
56170
+ })]
56171
+ });
56172
+ }
56173
+ //#endregion
55648
56174
  //#region src/composites/device-controls/thermostat-hero-card.tsx
55649
56175
  /**
55650
56176
  * Default temperature range (Celsius) used to normalize the setpoint fraction
@@ -57733,10 +58259,18 @@ var CONTROL_KIND_COMPONENTS = {
57733
58259
  InlineControl: PetFountainInlineControl,
57734
58260
  HeroCard: PetFountainHeroCard
57735
58261
  },
58262
+ heater: {
58263
+ InlineControl: HeaterInlineControl,
58264
+ HeroCard: HeaterHeroCard
58265
+ },
57736
58266
  dishwasher: {
57737
58267
  InlineControl: DishwasherInlineControl,
57738
58268
  HeroCard: DishwasherHeroCard
57739
58269
  },
58270
+ appliance: {
58271
+ InlineControl: ApplianceInlineControl,
58272
+ HeroCard: ApplianceHeroCard
58273
+ },
57740
58274
  script: {
57741
58275
  InlineControl: ScriptInlineControl,
57742
58276
  HeroCard: ScriptHeroCard
@@ -57816,6 +58350,7 @@ var CONTROL_FILLS = {
57816
58350
  [_camstack_types.DeviceType.Image]: false,
57817
58351
  [_camstack_types.DeviceType.PetFeeder]: false,
57818
58352
  [_camstack_types.DeviceType.PetFountain]: false,
58353
+ [_camstack_types.DeviceType.Heater]: false,
57819
58354
  [_camstack_types.DeviceType.Dishwasher]: false,
57820
58355
  [_camstack_types.DeviceType.Washer]: false,
57821
58356
  [_camstack_types.DeviceType.Dryer]: false
@@ -73823,6 +74358,7 @@ exports.ThermostatInlineControl = ThermostatInlineControl;
73823
74358
  exports.TimeSeriesChart = TimeSeriesChart;
73824
74359
  exports.TimelineSlider = TimelineSlider;
73825
74360
  exports.Timer = Timer;
74361
+ exports.TimerOff = TimerOff;
73826
74362
  exports.TimezoneSelector = TimezoneSelector;
73827
74363
  exports.ToggleLeft = ToggleLeft;
73828
74364
  exports.ToggleRight = ToggleRight;
@@ -74270,9 +74806,7 @@ exports.useAddonsGetLastRestart = useAddonsGetLastRestart;
74270
74806
  exports.useAddonsGetLogs = useAddonsGetLogs;
74271
74807
  exports.useAddonsGetPackageDocs = useAddonsGetPackageDocs;
74272
74808
  exports.useAddonsGetVersions = useAddonsGetVersions;
74273
- exports.useAddonsInstallFromWorkspace = useAddonsInstallFromWorkspace;
74274
74809
  exports.useAddonsInstallPackage = useAddonsInstallPackage;
74275
- exports.useAddonsIsWorkspaceAvailable = useAddonsIsWorkspaceAvailable;
74276
74810
  exports.useAddonsList = useAddonsList;
74277
74811
  exports.useAddonsListCapabilityProviders = useAddonsListCapabilityProviders;
74278
74812
  exports.useAddonsListFrameworkPackages = useAddonsListFrameworkPackages;
@@ -74280,7 +74814,6 @@ exports.useAddonsListInstallations = useAddonsListInstallations;
74280
74814
  exports.useAddonsListJobs = useAddonsListJobs;
74281
74815
  exports.useAddonsListPackages = useAddonsListPackages;
74282
74816
  exports.useAddonsListUpdates = useAddonsListUpdates;
74283
- exports.useAddonsListWorkspacePackages = useAddonsListWorkspacePackages;
74284
74817
  exports.useAddonsOnAddonLogs = useAddonsOnAddonLogs;
74285
74818
  exports.useAddonsReloadPackages = useAddonsReloadPackages;
74286
74819
  exports.useAddonsRestartAddon = useAddonsRestartAddon;
@@ -74649,6 +75182,9 @@ exports.useDeviceProviderTestCreationField = useDeviceProviderTestCreationField;
74649
75182
  exports.useDeviceProxy = useDeviceProxy;
74650
75183
  exports.useDeviceProxy$1 = useDeviceProxy;
74651
75184
  exports.useDeviceQuickDetailOpen = useDeviceQuickDetailOpen;
75185
+ exports.useDeviceRunsGetCurrentRun = useDeviceRunsGetCurrentRun;
75186
+ exports.useDeviceRunsGetRun = useDeviceRunsGetRun;
75187
+ exports.useDeviceRunsListRuns = useDeviceRunsListRuns;
74652
75188
  exports.useDeviceSeriesList = useDeviceSeriesList;
74653
75189
  exports.useDeviceSnapshot = useDeviceSnapshot;
74654
75190
  exports.useDeviceSnapshotImage = useDeviceSnapshotImage;
@@ -74717,6 +75253,10 @@ exports.useFanControlSetDirection = useFanControlSetDirection;
74717
75253
  exports.useFanControlSetOscillating = useFanControlSetOscillating;
74718
75254
  exports.useFanControlSetPercentage = useFanControlSetPercentage;
74719
75255
  exports.useFanControlSetPreset = useFanControlSetPreset;
75256
+ exports.useFanPresetsApplyPreset = useFanPresetsApplyPreset;
75257
+ exports.useFanPresetsCapturePreset = useFanPresetsCapturePreset;
75258
+ exports.useFanPresetsGetPresets = useFanPresetsGetPresets;
75259
+ exports.useFanPresetsSetPresets = useFanPresetsSetPresets;
74720
75260
  exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
74721
75261
  exports.useFloodGetStatus = useFloodGetStatus;
74722
75262
  exports.useGasGetStatus = useGasGetStatus;
@@ -75518,6 +76058,9 @@ exports.useTerminalSessionSetInstanceEnabled = useTerminalSessionSetInstanceEnab
75518
76058
  exports.useTerminalSessionUpdateInstance = useTerminalSessionUpdateInstance;
75519
76059
  exports.useTerminalSessionWriteInput = useTerminalSessionWriteInput;
75520
76060
  exports.useThemeMode = require_theme_index.useThemeMode$1;
76061
+ exports.useTimedRunCancel = useTimedRunCancel;
76062
+ exports.useTimedRunGetStatus = useTimedRunGetStatus;
76063
+ exports.useTimedRunRunFor = useTimedRunRunFor;
75521
76064
  exports.useToastOnToast = useToastOnToast;
75522
76065
  exports.useTrainingCanaryResult = useTrainingCanaryResult;
75523
76066
  exports.useTrainingCancelJob = useTrainingCancelJob;