@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/CHANGELOG.md +15 -0
- package/dist/{MotionZonesSettings-dx3wObgK.js → MotionZonesSettings-Zcfa5c9l.js} +1 -1
- package/dist/{PrivacyMaskSettings-BM2C_xWz.js → PrivacyMaskSettings-B_X42yzN.js} +1 -1
- package/dist/{SceneMonitorEditor-CI72S4eN.js → SceneMonitorEditor-C8YXVzYI.js} +1 -1
- package/dist/composites/device-controls/ac-panel.d.ts +18 -0
- package/dist/composites/device-controls/appliance-control.d.ts +14 -0
- package/dist/composites/device-controls/heater-control.d.ts +4 -0
- package/dist/composites/device-controls/timed-run-control.d.ts +13 -0
- package/dist/generated/system-hooks.d.ts +20 -6
- package/dist/{icons-jbAtRNh8.cjs → icons-DXwPEKdJ.cjs} +1 -29
- package/dist/{icons-CZwJd50b.js → icons-e-xohmFb.js} +1 -29
- package/dist/index.cjs +1214 -671
- package/dist/index.js +1210 -675
- package/package.json +1 -1
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-
|
|
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:
|
|
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$
|
|
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$
|
|
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: [
|
|
54226
|
-
|
|
54227
|
-
|
|
54228
|
-
|
|
54229
|
-
|
|
54230
|
-
|
|
54231
|
-
|
|
54232
|
-
|
|
54233
|
-
|
|
54234
|
-
|
|
54235
|
-
|
|
54236
|
-
|
|
54237
|
-
|
|
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
|
-
})
|
|
54240
|
-
|
|
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.
|
|
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/
|
|
54509
|
-
/**
|
|
54510
|
-
|
|
54511
|
-
|
|
54512
|
-
|
|
54513
|
-
|
|
54514
|
-
|
|
54515
|
-
|
|
54516
|
-
|
|
54517
|
-
|
|
54518
|
-
|
|
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
|
-
/**
|
|
54569
|
-
function
|
|
54570
|
-
|
|
54571
|
-
|
|
54572
|
-
|
|
54573
|
-
|
|
54574
|
-
|
|
54575
|
-
|
|
54576
|
-
|
|
54577
|
-
|
|
54578
|
-
|
|
54579
|
-
|
|
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
|
-
/**
|
|
54583
|
-
|
|
54584
|
-
|
|
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
|
-
*
|
|
54588
|
-
*
|
|
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
|
|
54591
|
-
|
|
54592
|
-
|
|
54593
|
-
|
|
54594
|
-
|
|
54595
|
-
|
|
54596
|
-
|
|
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
|
-
|
|
54599
|
-
|
|
54600
|
-
|
|
54601
|
-
|
|
54602
|
-
|
|
54603
|
-
|
|
54604
|
-
|
|
54605
|
-
|
|
54606
|
-
|
|
54607
|
-
|
|
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
|
-
|
|
54611
|
-
|
|
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.
|
|
54615
|
-
slice: (d) => d.state.
|
|
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 (
|
|
54651
|
-
const
|
|
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
|
-
|
|
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
|
|
54674
|
-
children:
|
|
54675
|
-
|
|
54676
|
-
|
|
54677
|
-
|
|
54678
|
-
|
|
54679
|
-
|
|
54680
|
-
|
|
54681
|
-
|
|
54682
|
-
|
|
54683
|
-
|
|
54684
|
-
|
|
54685
|
-
|
|
54686
|
-
|
|
54687
|
-
|
|
54688
|
-
|
|
54689
|
-
|
|
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)(
|
|
54693
|
-
|
|
54694
|
-
|
|
54695
|
-
|
|
54696
|
-
onClick: () =>
|
|
54697
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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;
|