@camstack/ui-library 1.2.368 → 1.2.370
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 +14 -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/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 +18 -0
- package/dist/hotkeys/hotkey-provider.d.ts +13 -0
- package/dist/hotkeys/index.d.ts +2 -2
- 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 +1579 -1173
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1574 -1180
- package/dist/ui-library-version.d.ts +1 -0
- 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) => {
|
|
@@ -6043,6 +6076,7 @@ function matchesCombo(event, combo, platform) {
|
|
|
6043
6076
|
var PAGE_SCOPE = "page";
|
|
6044
6077
|
/** Asked last, after the page: a global handler that yields to whatever the page registers. */
|
|
6045
6078
|
var FALLBACK_SCOPE = "fallback";
|
|
6079
|
+
var NO_HOTKEYS = [];
|
|
6046
6080
|
var RegistryContext = (0, react$1.createContext)(null);
|
|
6047
6081
|
var ScopeContext = (0, react$1.createContext)(PAGE_SCOPE);
|
|
6048
6082
|
function isTypingTarget(target) {
|
|
@@ -6081,32 +6115,49 @@ function HotkeyProvider({ children, platform }) {
|
|
|
6081
6115
|
scopes: []
|
|
6082
6116
|
});
|
|
6083
6117
|
const effectivePlatform = platform ?? detectPlatform();
|
|
6084
|
-
const
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6118
|
+
const listenersRef = (0, react$1.useRef)(/* @__PURE__ */ new Set());
|
|
6119
|
+
const listRef = (0, react$1.useRef)(NO_HOTKEYS);
|
|
6120
|
+
const registry = (0, react$1.useMemo)(() => {
|
|
6121
|
+
const publish = () => {
|
|
6122
|
+
listRef.current = distinctHotkeys(stateRef.current.bindings);
|
|
6123
|
+
for (const listener of listenersRef.current) listener();
|
|
6124
|
+
};
|
|
6125
|
+
return {
|
|
6126
|
+
subscribe: (listener) => {
|
|
6127
|
+
listenersRef.current = new Set([...listenersRef.current, listener]);
|
|
6128
|
+
return () => {
|
|
6129
|
+
listenersRef.current = new Set([...listenersRef.current].filter((l) => l !== listener));
|
|
6130
|
+
};
|
|
6131
|
+
},
|
|
6132
|
+
snapshot: () => listRef.current,
|
|
6133
|
+
addBinding: (binding) => {
|
|
6091
6134
|
stateRef.current = {
|
|
6092
6135
|
...stateRef.current,
|
|
6093
|
-
bindings: stateRef.current.bindings
|
|
6136
|
+
bindings: [...stateRef.current.bindings, binding]
|
|
6094
6137
|
};
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6138
|
+
publish();
|
|
6139
|
+
return () => {
|
|
6140
|
+
stateRef.current = {
|
|
6141
|
+
...stateRef.current,
|
|
6142
|
+
bindings: stateRef.current.bindings.filter((b) => b.id !== binding.id)
|
|
6143
|
+
};
|
|
6144
|
+
publish();
|
|
6145
|
+
};
|
|
6146
|
+
},
|
|
6147
|
+
addScope: (scope) => {
|
|
6103
6148
|
stateRef.current = {
|
|
6104
6149
|
...stateRef.current,
|
|
6105
|
-
scopes: stateRef.current.scopes
|
|
6150
|
+
scopes: [...stateRef.current.scopes, scope]
|
|
6106
6151
|
};
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6152
|
+
return () => {
|
|
6153
|
+
stateRef.current = {
|
|
6154
|
+
...stateRef.current,
|
|
6155
|
+
scopes: stateRef.current.scopes.filter((s) => s !== scope)
|
|
6156
|
+
};
|
|
6157
|
+
};
|
|
6158
|
+
}
|
|
6159
|
+
};
|
|
6160
|
+
}, []);
|
|
6110
6161
|
(0, react$1.useEffect)(() => {
|
|
6111
6162
|
const onKeyDown = (event) => dispatch(stateRef.current, event, effectivePlatform);
|
|
6112
6163
|
window.addEventListener("keydown", onKeyDown, true);
|
|
@@ -6118,6 +6169,29 @@ function HotkeyProvider({ children, platform }) {
|
|
|
6118
6169
|
});
|
|
6119
6170
|
}
|
|
6120
6171
|
var nextBindingId = 0;
|
|
6172
|
+
function distinctHotkeys(bindings) {
|
|
6173
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6174
|
+
for (const b of bindings) {
|
|
6175
|
+
const key = `${b.scope}|${b.combo}`;
|
|
6176
|
+
const prev = seen.get(key);
|
|
6177
|
+
if (!prev || prev.label === null && b.label !== null) seen.set(key, {
|
|
6178
|
+
combo: b.combo,
|
|
6179
|
+
scope: b.scope,
|
|
6180
|
+
label: b.label
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
return [...seen.values()];
|
|
6184
|
+
}
|
|
6185
|
+
/**
|
|
6186
|
+
* The bindings registered below the `HotkeyProvider` right now (read-only; there
|
|
6187
|
+
* is no rebinding). Empty without a provider.
|
|
6188
|
+
*/
|
|
6189
|
+
function useRegisteredHotkeys() {
|
|
6190
|
+
const registry = (0, react$1.useContext)(RegistryContext);
|
|
6191
|
+
return (0, react$1.useSyncExternalStore)(registry ? registry.subscribe : noSubscribe, registry ? registry.snapshot : emptySnapshot);
|
|
6192
|
+
}
|
|
6193
|
+
var noSubscribe = () => () => void 0;
|
|
6194
|
+
var emptySnapshot = () => NO_HOTKEYS;
|
|
6121
6195
|
/**
|
|
6122
6196
|
* Bind a combo for the life of the component. Without a `HotkeyProvider` above
|
|
6123
6197
|
* it does nothing, so the browser keeps its own shortcut.
|
|
@@ -6137,6 +6211,7 @@ function useHotkey(combo, handler, options = {}) {
|
|
|
6137
6211
|
return registry.addBinding({
|
|
6138
6212
|
id: nextBindingId,
|
|
6139
6213
|
combo,
|
|
6214
|
+
label: options.label ?? null,
|
|
6140
6215
|
scope,
|
|
6141
6216
|
handlerRef,
|
|
6142
6217
|
preventDefault,
|
|
@@ -6148,7 +6223,8 @@ function useHotkey(combo, handler, options = {}) {
|
|
|
6148
6223
|
combo,
|
|
6149
6224
|
scope,
|
|
6150
6225
|
preventDefault,
|
|
6151
|
-
allowInInput
|
|
6226
|
+
allowInInput,
|
|
6227
|
+
options.label
|
|
6152
6228
|
]);
|
|
6153
6229
|
}
|
|
6154
6230
|
/** Bindings made below this component belong to `id`; while enabled it is the topmost scope. */
|
|
@@ -6193,11 +6269,13 @@ function useSearchFieldHotkey(ref, options = {}) {
|
|
|
6193
6269
|
};
|
|
6194
6270
|
useHotkey("mod+f", focusField, {
|
|
6195
6271
|
enabled,
|
|
6196
|
-
scope
|
|
6272
|
+
scope,
|
|
6273
|
+
label: "Focus the search field"
|
|
6197
6274
|
});
|
|
6198
6275
|
useHotkey("mod+k", focusField, {
|
|
6199
6276
|
enabled,
|
|
6200
|
-
scope
|
|
6277
|
+
scope,
|
|
6278
|
+
label: "Focus the search field"
|
|
6201
6279
|
});
|
|
6202
6280
|
}
|
|
6203
6281
|
//#endregion
|
|
@@ -11478,6 +11556,10 @@ var useDisplayPaletteGetPalette = /*#__PURE__*/ alias(() => trpc.displayPalette.
|
|
|
11478
11556
|
var useDisplayPaletteListPresets = /*#__PURE__*/ alias(() => trpc.displayPalette.listPresets.useQuery);
|
|
11479
11557
|
/** Generated alias around `trpc.displayPalette.setPalette.useMutation`. */
|
|
11480
11558
|
var useDisplayPaletteSetPalette = /*#__PURE__*/ alias(() => trpc.displayPalette.setPalette.useMutation);
|
|
11559
|
+
/** Generated alias around `trpc.doNotDisturb.setEnabled.useMutation`. */
|
|
11560
|
+
var useDoNotDisturbSetEnabled = /*#__PURE__*/ alias(() => trpc.doNotDisturb.setEnabled.useMutation);
|
|
11561
|
+
/** Generated alias around `trpc.doNotDisturb.getStatus.useQuery`. */
|
|
11562
|
+
var useDoNotDisturbGetStatus = /*#__PURE__*/ alias(() => trpc.doNotDisturb.getStatus.useQuery);
|
|
11481
11563
|
/** Generated alias around `trpc.doorbell.getStatus.useQuery`. */
|
|
11482
11564
|
var useDoorbellGetStatus = /*#__PURE__*/ alias(() => trpc.doorbell.getStatus.useQuery);
|
|
11483
11565
|
/** Generated alias around `trpc.enumSensor.getStatus.useQuery`. */
|
|
@@ -11526,6 +11608,14 @@ var useFanControlSetDirection = /*#__PURE__*/ alias(() => trpc.fanControl.setDir
|
|
|
11526
11608
|
var useFanControlSetOscillating = /*#__PURE__*/ alias(() => trpc.fanControl.setOscillating.useMutation);
|
|
11527
11609
|
/** Generated alias around `trpc.fanControl.getStatus.useQuery`. */
|
|
11528
11610
|
var useFanControlGetStatus = /*#__PURE__*/ alias(() => trpc.fanControl.getStatus.useQuery);
|
|
11611
|
+
/** Generated alias around `trpc.fanPresets.getPresets.useQuery`. */
|
|
11612
|
+
var useFanPresetsGetPresets = /*#__PURE__*/ alias(() => trpc.fanPresets.getPresets.useQuery);
|
|
11613
|
+
/** Generated alias around `trpc.fanPresets.setPresets.useMutation`. */
|
|
11614
|
+
var useFanPresetsSetPresets = /*#__PURE__*/ alias(() => trpc.fanPresets.setPresets.useMutation);
|
|
11615
|
+
/** Generated alias around `trpc.fanPresets.applyPreset.useMutation`. */
|
|
11616
|
+
var useFanPresetsApplyPreset = /*#__PURE__*/ alias(() => trpc.fanPresets.applyPreset.useMutation);
|
|
11617
|
+
/** Generated alias around `trpc.fanPresets.capturePreset.useQuery`. */
|
|
11618
|
+
var useFanPresetsCapturePreset = /*#__PURE__*/ alias(() => trpc.fanPresets.capturePreset.useQuery);
|
|
11529
11619
|
/** Generated alias around `trpc.featureProbe.getStatus.useQuery`. */
|
|
11530
11620
|
var useFeatureProbeGetStatus = /*#__PURE__*/ alias(() => trpc.featureProbe.getStatus.useQuery);
|
|
11531
11621
|
/** Generated alias around `trpc.flood.getStatus.useQuery`. */
|
|
@@ -13056,6 +13146,12 @@ var useTerminalSessionPullOutput = /*#__PURE__*/ alias(() => trpc.terminalSessio
|
|
|
13056
13146
|
var useTerminalSessionWriteInput = /*#__PURE__*/ alias(() => trpc.terminalSession.writeInput.useMutation);
|
|
13057
13147
|
/** Generated alias around `trpc.terminalSession.close.useMutation`. */
|
|
13058
13148
|
var useTerminalSessionClose = /*#__PURE__*/ alias(() => trpc.terminalSession.close.useMutation);
|
|
13149
|
+
/** Generated alias around `trpc.timedRun.runFor.useMutation`. */
|
|
13150
|
+
var useTimedRunRunFor = /*#__PURE__*/ alias(() => trpc.timedRun.runFor.useMutation);
|
|
13151
|
+
/** Generated alias around `trpc.timedRun.cancel.useMutation`. */
|
|
13152
|
+
var useTimedRunCancel = /*#__PURE__*/ alias(() => trpc.timedRun.cancel.useMutation);
|
|
13153
|
+
/** Generated alias around `trpc.timedRun.getStatus.useQuery`. */
|
|
13154
|
+
var useTimedRunGetStatus = /*#__PURE__*/ alias(() => trpc.timedRun.getStatus.useQuery);
|
|
13059
13155
|
/** Generated alias around `trpc.toast.onToast.useSubscription`. */
|
|
13060
13156
|
var useToastOnToast = /*#__PURE__*/ alias(() => trpc.toast.onToast.useSubscription);
|
|
13061
13157
|
/** Generated alias around `trpc.training.listBaseModels.useQuery`. */
|
|
@@ -54144,6 +54240,137 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
|
|
|
54144
54240
|
});
|
|
54145
54241
|
}
|
|
54146
54242
|
//#endregion
|
|
54243
|
+
//#region src/composites/device-controls/timed-run-control.tsx
|
|
54244
|
+
/**
|
|
54245
|
+
* "Run for N" - the inline control of the `timed-run` cap (D993).
|
|
54246
|
+
*
|
|
54247
|
+
* A number, the unit it is in, and a Run button: the hub turns the device on and
|
|
54248
|
+
* switches it off again after that long. While a run is armed the control shows
|
|
54249
|
+
* the time left (counted down from the slice's `activeUntil`) and a Cancel. The
|
|
54250
|
+
* maximum is the device type's own bound (`TIMED_RUN_LIMITS`); a value above it
|
|
54251
|
+
* is refused by the hub with a typed reason, which this control says in words.
|
|
54252
|
+
*
|
|
54253
|
+
* Rendered only when the device binds `timed-run`. An unknown slice is "nothing
|
|
54254
|
+
* armed", never a running timer.
|
|
54255
|
+
*/
|
|
54256
|
+
var TICK_MS = 1e3;
|
|
54257
|
+
var SECONDS_PER_MINUTE$1 = 60;
|
|
54258
|
+
var MS_PER_SECOND = 1e3;
|
|
54259
|
+
var DEFAULT_MINUTES = 30;
|
|
54260
|
+
var DEFAULT_SECONDS = 60;
|
|
54261
|
+
/** `m:ss` for a countdown; a started run never shows a negative time. */
|
|
54262
|
+
function formatCountdown(remainingMs) {
|
|
54263
|
+
const total = Math.max(0, Math.ceil(remainingMs / MS_PER_SECOND));
|
|
54264
|
+
const minutes = Math.floor(total / SECONDS_PER_MINUTE$1);
|
|
54265
|
+
const seconds = total % SECONDS_PER_MINUTE$1;
|
|
54266
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
54267
|
+
}
|
|
54268
|
+
/** The largest value the field accepts, in the limit's unit. */
|
|
54269
|
+
function maxInUnit(limit) {
|
|
54270
|
+
return limit.unit === "min" ? limit.maxSeconds / SECONDS_PER_MINUTE$1 : limit.maxSeconds;
|
|
54271
|
+
}
|
|
54272
|
+
function useNow(active) {
|
|
54273
|
+
const [now, setNow] = (0, react$1.useState)(() => Date.now());
|
|
54274
|
+
(0, react$1.useEffect)(() => {
|
|
54275
|
+
if (!active) return void 0;
|
|
54276
|
+
setNow(Date.now());
|
|
54277
|
+
const handle = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
54278
|
+
return () => clearInterval(handle);
|
|
54279
|
+
}, [active]);
|
|
54280
|
+
return now;
|
|
54281
|
+
}
|
|
54282
|
+
function TimedRunControl({ trpc, deviceId, limit }) {
|
|
54283
|
+
const t = useUiText();
|
|
54284
|
+
const { cap, slice, online } = useDeviceCapability({
|
|
54285
|
+
trpc,
|
|
54286
|
+
deviceId,
|
|
54287
|
+
cap: (d) => d.timedRun,
|
|
54288
|
+
slice: (d) => d.state.timedRun
|
|
54289
|
+
});
|
|
54290
|
+
const max = maxInUnit(limit);
|
|
54291
|
+
const [draft, setDraft] = (0, react$1.useState)(Math.min(max, limit.unit === "min" ? DEFAULT_MINUTES : DEFAULT_SECONDS));
|
|
54292
|
+
const [refusal, setRefusal] = (0, react$1.useState)(null);
|
|
54293
|
+
const activeUntil = slice?.activeUntil ?? null;
|
|
54294
|
+
const now = useNow(activeUntil !== null);
|
|
54295
|
+
if (cap === void 0) return null;
|
|
54296
|
+
const unitLabel = limit.unit === "min" ? t("uiLibrary.timedRun.minutes", "min") : t("uiLibrary.timedRun.seconds", "s");
|
|
54297
|
+
const valid = Number.isFinite(draft) && draft > 0 && draft <= max;
|
|
54298
|
+
const armed = activeUntil !== null && activeUntil > now;
|
|
54299
|
+
const run = () => {
|
|
54300
|
+
if (!valid) return;
|
|
54301
|
+
setRefusal(null);
|
|
54302
|
+
cap.runFor({
|
|
54303
|
+
duration: draft,
|
|
54304
|
+
unit: limit.unit
|
|
54305
|
+
}).then((result) => {
|
|
54306
|
+
if (!result.ok) setRefusal(result.reason);
|
|
54307
|
+
});
|
|
54308
|
+
};
|
|
54309
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54310
|
+
className: "flex flex-col gap-1.5",
|
|
54311
|
+
"data-testid": "timed-run-control",
|
|
54312
|
+
children: [
|
|
54313
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54314
|
+
className: "flex items-center gap-2",
|
|
54315
|
+
children: [
|
|
54316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
54317
|
+
type: "number",
|
|
54318
|
+
inputMode: "numeric",
|
|
54319
|
+
min: 1,
|
|
54320
|
+
max,
|
|
54321
|
+
step: 1,
|
|
54322
|
+
value: Number.isFinite(draft) ? draft : "",
|
|
54323
|
+
disabled: !online,
|
|
54324
|
+
"aria-label": t("uiLibrary.timedRun.duration", "Run for"),
|
|
54325
|
+
className: "w-20 tabular-nums",
|
|
54326
|
+
onChange: (event) => setDraft(event.target.valueAsNumber)
|
|
54327
|
+
}),
|
|
54328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54329
|
+
className: "text-xs text-foreground-muted",
|
|
54330
|
+
children: unitLabel
|
|
54331
|
+
}),
|
|
54332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
54333
|
+
size: "sm",
|
|
54334
|
+
disabled: !online || !valid,
|
|
54335
|
+
onClick: run,
|
|
54336
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, {
|
|
54337
|
+
className: "h-4 w-4",
|
|
54338
|
+
"aria-hidden": true
|
|
54339
|
+
}), t("uiLibrary.timedRun.run", "Run")]
|
|
54340
|
+
}),
|
|
54341
|
+
armed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54342
|
+
className: "text-xs font-medium tabular-nums text-foreground",
|
|
54343
|
+
"data-testid": "timed-run-countdown",
|
|
54344
|
+
children: t("uiLibrary.timedRun.remaining", "{{time}} left", { time: formatCountdown(activeUntil - now) })
|
|
54345
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
54346
|
+
size: "sm",
|
|
54347
|
+
variant: "outline",
|
|
54348
|
+
disabled: !online,
|
|
54349
|
+
"aria-label": t("uiLibrary.timedRun.cancel", "Cancel the timer"),
|
|
54350
|
+
onClick: () => void cap.cancel({}),
|
|
54351
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimerOff, {
|
|
54352
|
+
className: "h-4 w-4",
|
|
54353
|
+
"aria-hidden": true
|
|
54354
|
+
})
|
|
54355
|
+
})] })
|
|
54356
|
+
]
|
|
54357
|
+
}),
|
|
54358
|
+
!valid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54359
|
+
className: "text-xs text-danger",
|
|
54360
|
+
children: t("uiLibrary.timedRun.range", "Enter 1 to {{max}} {{unit}}", {
|
|
54361
|
+
max,
|
|
54362
|
+
unit: unitLabel
|
|
54363
|
+
})
|
|
54364
|
+
}),
|
|
54365
|
+
refusal !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54366
|
+
className: "text-xs text-danger",
|
|
54367
|
+
role: "alert",
|
|
54368
|
+
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.")
|
|
54369
|
+
})
|
|
54370
|
+
]
|
|
54371
|
+
});
|
|
54372
|
+
}
|
|
54373
|
+
//#endregion
|
|
54147
54374
|
//#region src/composites/device-controls/pet-fountain-panel.tsx
|
|
54148
54375
|
/**
|
|
54149
54376
|
* Pet-fountain controls for a `DeviceType.PetFountain` device.
|
|
@@ -54172,6 +54399,7 @@ var KEYFRAMES$2 = `
|
|
|
54172
54399
|
.pfn-ripple, .pfn-bubble { animation: none !important; opacity: 0; }
|
|
54173
54400
|
}
|
|
54174
54401
|
`;
|
|
54402
|
+
var PUMP_LIMIT = _camstack_types.TIMED_RUN_LIMITS[_camstack_types.DeviceType.PetFountain];
|
|
54175
54403
|
var LITRES_TO_ML = 1e3;
|
|
54176
54404
|
var SECONDS_PER_MINUTE = 60;
|
|
54177
54405
|
var MAX_PUMP_MINUTES = 240;
|
|
@@ -54336,6 +54564,11 @@ function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
54336
54564
|
]
|
|
54337
54565
|
})
|
|
54338
54566
|
}),
|
|
54567
|
+
PUMP_LIMIT !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimedRunControl, {
|
|
54568
|
+
trpc,
|
|
54569
|
+
deviceId,
|
|
54570
|
+
limit: PUMP_LIMIT
|
|
54571
|
+
}),
|
|
54339
54572
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
54340
54573
|
variant: "outline",
|
|
54341
54574
|
size: "sm",
|
|
@@ -54560,1277 +54793,1430 @@ function PetFountainHeroCard({ trpc, deviceId, features, optimistic }) {
|
|
|
54560
54793
|
});
|
|
54561
54794
|
}
|
|
54562
54795
|
//#endregion
|
|
54563
|
-
//#region src/composites/device-controls/
|
|
54564
|
-
/**
|
|
54565
|
-
|
|
54566
|
-
|
|
54567
|
-
|
|
54568
|
-
|
|
54569
|
-
|
|
54570
|
-
|
|
54571
|
-
|
|
54572
|
-
|
|
54573
|
-
|
|
54574
|
-
* reported, and this view renders that as "—" / a disabled control, never as an unchecked
|
|
54575
|
-
* box or a zero. It also does not echo a command before the appliance confirms it: the
|
|
54576
|
-
* slice moves when the appliance says so.
|
|
54577
|
-
*/
|
|
54578
|
-
/** What the operation state is called to a person, and how it is toned. */
|
|
54579
|
-
function dishwasherOperationMeta(operationState, t = defaultUiText) {
|
|
54580
|
-
switch (operationState) {
|
|
54581
|
-
case "Run": return {
|
|
54582
|
-
label: t("uiLibrary.dishwasher.op.run", "Running"),
|
|
54583
|
-
tone: "text-primary"
|
|
54584
|
-
};
|
|
54585
|
-
case "Pause": return {
|
|
54586
|
-
label: t("uiLibrary.dishwasher.op.pause", "Paused"),
|
|
54587
|
-
tone: "text-warning"
|
|
54588
|
-
};
|
|
54589
|
-
case "Finished": return {
|
|
54590
|
-
label: t("uiLibrary.dishwasher.op.finished", "Finished"),
|
|
54591
|
-
tone: "text-success"
|
|
54592
|
-
};
|
|
54593
|
-
case "Error": return {
|
|
54594
|
-
label: t("uiLibrary.dishwasher.op.error", "Error"),
|
|
54595
|
-
tone: "text-danger"
|
|
54596
|
-
};
|
|
54597
|
-
case "Aborting": return {
|
|
54598
|
-
label: t("uiLibrary.dishwasher.op.aborting", "Stopping"),
|
|
54599
|
-
tone: "text-warning"
|
|
54600
|
-
};
|
|
54601
|
-
case "ActionRequired": return {
|
|
54602
|
-
label: t("uiLibrary.dishwasher.op.actionRequired", "Action required"),
|
|
54603
|
-
tone: "text-warning"
|
|
54604
|
-
};
|
|
54605
|
-
case "DelayedStart": return {
|
|
54606
|
-
label: t("uiLibrary.dishwasher.op.delayedStart", "Delayed start"),
|
|
54607
|
-
tone: "text-primary"
|
|
54608
|
-
};
|
|
54609
|
-
case "Ready": return {
|
|
54610
|
-
label: t("uiLibrary.dishwasher.op.ready", "Ready"),
|
|
54611
|
-
tone: "text-foreground"
|
|
54612
|
-
};
|
|
54613
|
-
case "Inactive": return {
|
|
54614
|
-
label: t("uiLibrary.dishwasher.op.inactive", "Inactive"),
|
|
54615
|
-
tone: "text-foreground-subtle"
|
|
54616
|
-
};
|
|
54617
|
-
case null: return {
|
|
54618
|
-
label: t("uiLibrary.dishwasher.unknown", "Unknown"),
|
|
54619
|
-
tone: "text-foreground-subtle"
|
|
54620
|
-
};
|
|
54621
|
-
}
|
|
54796
|
+
//#region src/composites/device-controls/ac-panel.tsx
|
|
54797
|
+
/** Modes that mean the device can cool, dry, or move air — an air conditioner. */
|
|
54798
|
+
var COOLING_MODES = new Set([
|
|
54799
|
+
"cool",
|
|
54800
|
+
"heat_cool",
|
|
54801
|
+
"dry",
|
|
54802
|
+
"fan_only"
|
|
54803
|
+
]);
|
|
54804
|
+
/** A heater offers heat (and maybe auto/off) and none of the cooling modes. */
|
|
54805
|
+
function climateOffersCooling(modes) {
|
|
54806
|
+
return modes.some((mode) => COOLING_MODES.has(mode));
|
|
54622
54807
|
}
|
|
54623
|
-
/**
|
|
54624
|
-
function
|
|
54625
|
-
|
|
54626
|
-
|
|
54627
|
-
|
|
54628
|
-
|
|
54629
|
-
|
|
54630
|
-
|
|
54631
|
-
|
|
54632
|
-
|
|
54633
|
-
|
|
54634
|
-
|
|
54808
|
+
/** Setpoint track bounds. Absent when the integration did not report both ends. */
|
|
54809
|
+
function climateSetpointBounds(status) {
|
|
54810
|
+
const min = status.minTemp;
|
|
54811
|
+
const max = status.maxTemp;
|
|
54812
|
+
if (typeof min !== "number" || typeof max !== "number") return null;
|
|
54813
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
|
|
54814
|
+
return {
|
|
54815
|
+
min,
|
|
54816
|
+
max
|
|
54817
|
+
};
|
|
54818
|
+
}
|
|
54819
|
+
/** Map a 0–1 drag fraction onto the setpoint, snapped to `tempStep`. */
|
|
54820
|
+
function climateTargetFromFraction(fraction, bounds, tempStep) {
|
|
54821
|
+
const step = resolveClimateTempStep(tempStep);
|
|
54822
|
+
const clamped = Math.min(1, Math.max(0, fraction));
|
|
54823
|
+
const raw = bounds.min + clamped * (bounds.max - bounds.min);
|
|
54824
|
+
const steps = Math.round((raw - bounds.min) / step);
|
|
54825
|
+
const snapped = bounds.min + steps * step;
|
|
54826
|
+
const rounded = Math.round(snapped * 1e3) / 1e3;
|
|
54827
|
+
return Math.min(bounds.max, Math.max(bounds.min, rounded));
|
|
54828
|
+
}
|
|
54829
|
+
/** AC-appropriate short mode labels. Exhaustive over HvacMode. */
|
|
54830
|
+
function modeLabel(t, mode) {
|
|
54831
|
+
switch (mode) {
|
|
54832
|
+
case "cool": return t("uiLibrary.acPanel.modeCool", "Cool");
|
|
54833
|
+
case "heat": return t("uiLibrary.acPanel.modeHeat", "Heat");
|
|
54834
|
+
case "auto": return t("uiLibrary.acPanel.modeAuto", "Auto");
|
|
54835
|
+
case "heat_cool": return t("uiLibrary.acPanel.modeHeatCool", "Heat·Cool");
|
|
54836
|
+
case "fan_only": return t("uiLibrary.acPanel.modeFan", "Fan");
|
|
54837
|
+
case "dry": return t("uiLibrary.acPanel.modeDry", "Dry");
|
|
54838
|
+
case "off": return t("uiLibrary.acPanel.modeOff", "Off");
|
|
54635
54839
|
}
|
|
54636
54840
|
}
|
|
54637
|
-
/**
|
|
54638
|
-
|
|
54639
|
-
|
|
54841
|
+
/** Per-mode icon (lucide). Exhaustive over HvacMode. */
|
|
54842
|
+
var MODE_ICON = {
|
|
54843
|
+
cool: Snowflake,
|
|
54844
|
+
heat: Flame,
|
|
54845
|
+
auto: Sparkles,
|
|
54846
|
+
heat_cool: Thermometer,
|
|
54847
|
+
fan_only: Fan,
|
|
54848
|
+
dry: Droplets,
|
|
54849
|
+
off: Power
|
|
54850
|
+
};
|
|
54851
|
+
/** Mode accent hex — drives the airflow particles, the glow, and the target
|
|
54852
|
+
* value colour. Mirrors the thermostat gauge palette. Exhaustive. */
|
|
54853
|
+
var MODE_ACCENT = {
|
|
54854
|
+
heat: "#f59e0b",
|
|
54855
|
+
cool: "#60a5fa",
|
|
54856
|
+
auto: "#c084fc",
|
|
54857
|
+
heat_cool: "#e879f9",
|
|
54858
|
+
fan_only: "#94a3b8",
|
|
54859
|
+
dry: "#22d3ee",
|
|
54860
|
+
off: "#a1a1aa"
|
|
54861
|
+
};
|
|
54862
|
+
/** Title-case a snake/lower token for a selector option ('medium_low' → 'Medium
|
|
54863
|
+
* low', 'turbo' → 'Turbo'). Shared by the fan + preset selectors. */
|
|
54864
|
+
function fanLabel(token) {
|
|
54865
|
+
const spaced = token.replace(/_/g, " ");
|
|
54866
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
54867
|
+
}
|
|
54868
|
+
/** Preset → icon. Unknown presets fall back to a neutral sparkle. */
|
|
54869
|
+
function presetIcon(preset) {
|
|
54870
|
+
switch (preset) {
|
|
54871
|
+
case "turbo": return Zap;
|
|
54872
|
+
case "quiet": return Feather;
|
|
54873
|
+
case "sleep": return Moon;
|
|
54874
|
+
case "eco": return Leaf;
|
|
54875
|
+
case "8c_heat": return Snowflake;
|
|
54876
|
+
case "none": return Ban;
|
|
54877
|
+
default: return Sparkles;
|
|
54878
|
+
}
|
|
54640
54879
|
}
|
|
54641
54880
|
/**
|
|
54642
|
-
*
|
|
54643
|
-
*
|
|
54881
|
+
* Fan level → airflow animation period. Higher speed ⇒ shorter period. Unknown
|
|
54882
|
+
* strings fall back to a medium pace. Turbo (a preset) shortens it further.
|
|
54644
54883
|
*/
|
|
54645
|
-
function
|
|
54646
|
-
|
|
54647
|
-
|
|
54648
|
-
|
|
54649
|
-
|
|
54650
|
-
|
|
54651
|
-
|
|
54884
|
+
function airflowPeriodSec(fanMode) {
|
|
54885
|
+
switch (fanMode) {
|
|
54886
|
+
case "high": return 1;
|
|
54887
|
+
case "medium_high": return 1.3;
|
|
54888
|
+
case "medium": return 1.6;
|
|
54889
|
+
case "medium_low": return 1.9;
|
|
54890
|
+
case "low": return 2.2;
|
|
54891
|
+
default: return 1.8;
|
|
54892
|
+
}
|
|
54652
54893
|
}
|
|
54653
|
-
|
|
54654
|
-
|
|
54655
|
-
|
|
54656
|
-
|
|
54657
|
-
|
|
54658
|
-
|
|
54659
|
-
|
|
54660
|
-
|
|
54661
|
-
|
|
54662
|
-
|
|
54663
|
-
});
|
|
54894
|
+
var KEYFRAMES$1 = `
|
|
54895
|
+
@keyframes acv-fall { 0%{transform:translateY(-1.75rem);opacity:0} 18%{opacity:.7} 82%{opacity:.7} 100%{transform:translateY(7.5rem);opacity:0} }
|
|
54896
|
+
@keyframes acv-rise { 0%{transform:translateY(7.5rem);opacity:0} 18%{opacity:.7} 82%{opacity:.7} 100%{transform:translateY(-1.75rem);opacity:0} }
|
|
54897
|
+
@keyframes acv-sway { 0%,100%{transform:translateX(-9%)} 50%{transform:translateX(9%)} }
|
|
54898
|
+
@keyframes acv-swing-h { 0%,100%{transform:translateX(-22%)} 50%{transform:translateX(22%)} }
|
|
54899
|
+
@keyframes acv-swing-v { 0%,100%{transform:translateY(-22%)} 50%{transform:translateY(22%)} }
|
|
54900
|
+
@keyframes acv-spin { to { transform:rotate(360deg) } }
|
|
54901
|
+
@keyframes acv-glow { 0%,100%{opacity:.35;transform:scale(1)} 50%{opacity:.6;transform:scale(1.06)} }
|
|
54902
|
+
@media (prefers-reduced-motion: reduce) {
|
|
54903
|
+
.acv-streak, .acv-fan, .acv-sway-wrap, .acv-glow, .acv-swing { animation: none !important; }
|
|
54664
54904
|
}
|
|
54665
|
-
|
|
54666
|
-
|
|
54905
|
+
`;
|
|
54906
|
+
function AcPanel({ trpc, deviceId, layout, optimistic }) {
|
|
54907
|
+
const t = useUiText();
|
|
54908
|
+
const { cap, slice, available } = useDeviceCapability({
|
|
54667
54909
|
trpc,
|
|
54668
54910
|
deviceId,
|
|
54669
|
-
cap: (d) => d.
|
|
54670
|
-
slice: (d) => d.state.
|
|
54671
|
-
});
|
|
54672
|
-
}
|
|
54673
|
-
function DishwasherIconButton({ label, busy, active, onClick, children }) {
|
|
54674
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
54675
|
-
type: "button",
|
|
54676
|
-
"aria-label": label,
|
|
54677
|
-
title: label,
|
|
54678
|
-
disabled: busy,
|
|
54679
|
-
onClick: (e) => {
|
|
54680
|
-
e.stopPropagation();
|
|
54681
|
-
onClick();
|
|
54682
|
-
},
|
|
54683
|
-
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"),
|
|
54684
|
-
children
|
|
54685
|
-
});
|
|
54686
|
-
}
|
|
54687
|
-
function DishwasherGraphic({ running, progressPercent }) {
|
|
54688
|
-
const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
|
|
54689
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54690
|
-
"aria-hidden": "true",
|
|
54691
|
-
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",
|
|
54692
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
54693
|
-
className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
|
|
54694
|
-
style: { height: `${pct}%` }
|
|
54695
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
54696
|
-
className: "absolute inset-0 flex items-center justify-center text-foreground-subtle",
|
|
54697
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waves, { className: "h-10 w-10 opacity-70" })
|
|
54698
|
-
})]
|
|
54911
|
+
cap: (d) => d.climateControl,
|
|
54912
|
+
slice: (d) => d.state.climateControl
|
|
54699
54913
|
});
|
|
54700
|
-
}
|
|
54701
|
-
function DishwasherPanel({ trpc, deviceId, layout }) {
|
|
54702
|
-
const t = useUiText();
|
|
54703
|
-
const { cap, slice, available } = useDishwasher(trpc, deviceId);
|
|
54914
|
+
const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
|
|
54704
54915
|
if (cap === void 0) return null;
|
|
54705
|
-
if (
|
|
54706
|
-
const
|
|
54707
|
-
const link = display.availability === "available";
|
|
54708
|
-
const busy = !available || !link;
|
|
54709
|
-
const optionTogglesLocked = display.childLock === true;
|
|
54710
|
-
const meta = dishwasherHeadline(display, t);
|
|
54711
|
-
const running = display.operationState === "Run";
|
|
54712
|
-
const paused = display.operationState === "Pause";
|
|
54713
|
-
const startable = display.remoteStartAllowed === true && display.selectedProgram !== null;
|
|
54714
|
-
const press = (fn) => {
|
|
54715
|
-
if (busy) return;
|
|
54716
|
-
fn();
|
|
54717
|
-
};
|
|
54916
|
+
if (display === void 0) return null;
|
|
54917
|
+
const busy = !available;
|
|
54718
54918
|
if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54719
|
-
|
|
54919
|
+
"data-mode": display.mode,
|
|
54920
|
+
className: "inline-flex items-center gap-1",
|
|
54720
54921
|
children: [
|
|
54721
|
-
display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54722
|
-
className: "inline-flex text-warning shrink-0",
|
|
54723
|
-
title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
|
|
54724
|
-
"aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
|
|
54725
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
|
|
54726
|
-
}),
|
|
54727
54922
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54728
|
-
className: cn("text-xs
|
|
54729
|
-
children:
|
|
54730
|
-
|
|
54731
|
-
|
|
54732
|
-
|
|
54733
|
-
|
|
54734
|
-
|
|
54735
|
-
|
|
54736
|
-
|
|
54737
|
-
|
|
54738
|
-
|
|
54739
|
-
|
|
54740
|
-
|
|
54741
|
-
|
|
54742
|
-
|
|
54743
|
-
|
|
54744
|
-
|
|
54745
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
|
|
54923
|
+
className: cn("text-xs tabular-nums font-medium", MODE_TEXT_COLOR[display.mode]),
|
|
54924
|
+
children: display.target !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
54925
|
+
display.target,
|
|
54926
|
+
"°",
|
|
54927
|
+
display.currentTemp !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
54928
|
+
className: "text-foreground-subtle font-normal",
|
|
54929
|
+
children: [
|
|
54930
|
+
" ",
|
|
54931
|
+
"(",
|
|
54932
|
+
display.currentTemp,
|
|
54933
|
+
"°)"
|
|
54934
|
+
]
|
|
54935
|
+
})
|
|
54936
|
+
] }) : display.currentTemp !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
54937
|
+
className: "text-foreground-subtle",
|
|
54938
|
+
children: [display.currentTemp, "°"]
|
|
54939
|
+
}) : null
|
|
54746
54940
|
}),
|
|
54747
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54748
|
-
|
|
54749
|
-
|
|
54750
|
-
|
|
54751
|
-
onClick: () =>
|
|
54752
|
-
|
|
54941
|
+
display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
54942
|
+
type: "button",
|
|
54943
|
+
"aria-label": t("uiLibrary.acPanel.decrease", "Decrease target"),
|
|
54944
|
+
disabled: busy,
|
|
54945
|
+
onClick: (e) => {
|
|
54946
|
+
e.stopPropagation();
|
|
54947
|
+
if (!busy && display.target !== null) {
|
|
54948
|
+
const next = nextClimateTarget(display.target, -1, display);
|
|
54949
|
+
applyOptimistic({ target: next });
|
|
54950
|
+
cap.setTarget({ target: next });
|
|
54951
|
+
}
|
|
54952
|
+
},
|
|
54953
|
+
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"),
|
|
54954
|
+
children: "−"
|
|
54955
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
54956
|
+
type: "button",
|
|
54957
|
+
"aria-label": t("uiLibrary.acPanel.increase", "Increase target"),
|
|
54958
|
+
disabled: busy,
|
|
54959
|
+
onClick: (e) => {
|
|
54960
|
+
e.stopPropagation();
|
|
54961
|
+
if (!busy && display.target !== null) {
|
|
54962
|
+
const next = nextClimateTarget(display.target, 1, display);
|
|
54963
|
+
applyOptimistic({ target: next });
|
|
54964
|
+
cap.setTarget({ target: next });
|
|
54965
|
+
}
|
|
54966
|
+
},
|
|
54967
|
+
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"),
|
|
54968
|
+
children: "+"
|
|
54969
|
+
})] }),
|
|
54970
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
54971
|
+
"aria-label": t("uiLibrary.acPanel.mode", "Mode"),
|
|
54972
|
+
value: display.mode,
|
|
54973
|
+
disabled: busy,
|
|
54974
|
+
onChange: (e) => {
|
|
54975
|
+
e.stopPropagation();
|
|
54976
|
+
const mode = display.availableModes.find((m) => m === e.target.value);
|
|
54977
|
+
if (!busy && mode) {
|
|
54978
|
+
applyOptimistic({ mode });
|
|
54979
|
+
cap.setMode({ mode });
|
|
54980
|
+
}
|
|
54981
|
+
},
|
|
54982
|
+
className: cn("bg-transparent px-0.5 text-xs text-foreground-subtle hover:text-foreground cursor-pointer", busy && "opacity-40 cursor-not-allowed"),
|
|
54983
|
+
children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
54984
|
+
value: m,
|
|
54985
|
+
children: modeLabel(t, m)
|
|
54986
|
+
}, m))
|
|
54753
54987
|
})
|
|
54754
54988
|
]
|
|
54755
54989
|
});
|
|
54756
|
-
const
|
|
54990
|
+
const accent = MODE_ACCENT[display.mode];
|
|
54991
|
+
const on = display.mode !== "off";
|
|
54992
|
+
const heatingOnly = !climateOffersCooling(display.availableModes);
|
|
54993
|
+
const setpointBounds = climateSetpointBounds(display);
|
|
54994
|
+
const CurrentPresetIcon = presetIcon(display.preset);
|
|
54995
|
+
const boost = display.preset === "turbo";
|
|
54996
|
+
const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
|
|
54997
|
+
const showVSwing = display.swingVertical !== null;
|
|
54998
|
+
const showHSwing = display.swingHorizontal !== null;
|
|
54999
|
+
const swaying = on && (display.swingVertical === true || display.swingHorizontal === true);
|
|
54757
55000
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54758
|
-
|
|
55001
|
+
"data-mode": display.mode,
|
|
55002
|
+
className: cn("relative flex flex-col items-stretch gap-2.5 overflow-hidden rounded-lg border p-3 transition-colors", MODE_COLOR[display.mode]),
|
|
54759
55003
|
children: [
|
|
55004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$1 }),
|
|
54760
55005
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54761
|
-
className: "flex items-
|
|
54762
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
|
|
54763
|
-
running,
|
|
54764
|
-
progressPercent: display.progress
|
|
54765
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54766
|
-
className: "flex min-w-0 flex-1 flex-col gap-2",
|
|
54767
|
-
children: [
|
|
54768
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54769
|
-
className: "flex flex-wrap items-center gap-2",
|
|
54770
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54771
|
-
className: cn("text-3xl font-semibold", meta.tone),
|
|
54772
|
-
children: meta.label
|
|
54773
|
-
}), display.powerOn === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54774
|
-
className: "text-xs font-medium text-foreground-subtle",
|
|
54775
|
-
children: t("uiLibrary.dishwasher.powerOff", "Power off")
|
|
54776
|
-
})]
|
|
54777
|
-
}),
|
|
54778
|
-
display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54779
|
-
className: "text-sm text-foreground-subtle",
|
|
54780
|
-
title: display.program,
|
|
54781
|
-
children: featureLabel(display.program)
|
|
54782
|
-
}),
|
|
54783
|
-
display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
54784
|
-
className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
|
|
54785
|
-
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) })]
|
|
54786
|
-
}),
|
|
54787
|
-
display.delayedStart != null && display.delayedStart > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54788
|
-
className: "text-xs text-foreground-subtle tabular-nums",
|
|
54789
|
-
children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStart, t) })
|
|
54790
|
-
}),
|
|
54791
|
-
display.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54792
|
-
className: "flex items-center gap-2",
|
|
54793
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
54794
|
-
className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
|
|
54795
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
54796
|
-
className: "h-full bg-primary transition-[width] duration-500",
|
|
54797
|
-
style: { width: `${Math.max(0, Math.min(100, display.progress))}%` }
|
|
54798
|
-
})
|
|
54799
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
54800
|
-
className: "tabular-nums text-xs text-foreground-subtle",
|
|
54801
|
-
children: [Math.round(display.progress), "%"]
|
|
54802
|
-
})]
|
|
54803
|
-
}),
|
|
54804
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54805
|
-
className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
|
|
54806
|
-
children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
54807
|
-
className: "inline-flex items-center gap-1 text-xs text-warning",
|
|
54808
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
|
|
54809
|
-
}), visibleDishwasherEvents(display.activeEvents).map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54810
|
-
className: "text-xs text-warning",
|
|
54811
|
-
title: event,
|
|
54812
|
-
children: featureLabel(event)
|
|
54813
|
-
}, event))]
|
|
54814
|
-
}),
|
|
54815
|
-
display.operationState === "Error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54816
|
-
className: "inline-flex items-center gap-1 text-xs text-danger",
|
|
54817
|
-
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") })]
|
|
54818
|
-
}),
|
|
54819
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54820
|
-
className: "flex flex-wrap items-center gap-2",
|
|
54821
|
-
children: [
|
|
54822
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
54823
|
-
label: t("uiLibrary.dishwasher.start", "Start"),
|
|
54824
|
-
busy: busy || !startable,
|
|
54825
|
-
active: running,
|
|
54826
|
-
onClick: () => press(() => cap.start({})),
|
|
54827
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
|
|
54828
|
-
}),
|
|
54829
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
54830
|
-
label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
|
|
54831
|
-
busy: busy || !(running || paused),
|
|
54832
|
-
active: paused,
|
|
54833
|
-
onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
|
|
54834
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
|
|
54835
|
-
}),
|
|
54836
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
54837
|
-
label: t("uiLibrary.dishwasher.stop", "Stop"),
|
|
54838
|
-
busy: busy || !(running || paused),
|
|
54839
|
-
active: false,
|
|
54840
|
-
onClick: () => press(() => cap.stop({})),
|
|
54841
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
|
|
54842
|
-
}),
|
|
54843
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
54844
|
-
label: t("uiLibrary.dishwasher.power", "Power"),
|
|
54845
|
-
busy: busy || display.powerOn === null,
|
|
54846
|
-
active: display.powerOn === true,
|
|
54847
|
-
onClick: () => {
|
|
54848
|
-
const next = display.powerOn !== true;
|
|
54849
|
-
press(() => cap.setPower({ on: next }));
|
|
54850
|
-
},
|
|
54851
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
|
|
54852
|
-
})
|
|
54853
|
-
]
|
|
54854
|
-
})
|
|
54855
|
-
]
|
|
54856
|
-
})]
|
|
54857
|
-
}),
|
|
54858
|
-
programs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
54859
|
-
className: "flex items-center gap-2 text-xs text-foreground-subtle",
|
|
54860
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
54861
|
-
className: "w-16 shrink-0",
|
|
54862
|
-
children: t("uiLibrary.dishwasher.program", "Program")
|
|
54863
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
54864
|
-
"aria-label": t("uiLibrary.dishwasher.program", "Program"),
|
|
54865
|
-
value: display.selectedProgram ?? "",
|
|
54866
|
-
disabled: busy,
|
|
54867
|
-
onChange: (e) => {
|
|
54868
|
-
const program = e.target.value;
|
|
54869
|
-
if (!program) return;
|
|
54870
|
-
cap.setProgram({ program });
|
|
54871
|
-
},
|
|
54872
|
-
onClick: (e) => {
|
|
54873
|
-
e.stopPropagation();
|
|
54874
|
-
},
|
|
54875
|
-
className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
|
|
54876
|
-
children: [display.selectedProgram == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
54877
|
-
value: "",
|
|
54878
|
-
children: "—"
|
|
54879
|
-
}), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
54880
|
-
value: p,
|
|
54881
|
-
children: featureLabel(p)
|
|
54882
|
-
}, p))]
|
|
54883
|
-
})]
|
|
54884
|
-
}),
|
|
54885
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54886
|
-
className: "flex flex-wrap gap-4 text-xs",
|
|
55006
|
+
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",
|
|
54887
55007
|
children: [
|
|
54888
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54889
|
-
|
|
54890
|
-
|
|
54891
|
-
|
|
54892
|
-
|
|
54893
|
-
|
|
55008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55009
|
+
className: "pointer-events-none absolute inset-0 opacity-60",
|
|
55010
|
+
children: heatingOnly && !on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RadiatorFins, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
|
|
55011
|
+
mode: display.mode,
|
|
55012
|
+
accent,
|
|
55013
|
+
periodSec: period,
|
|
55014
|
+
swaying,
|
|
55015
|
+
forceRising: heatingOnly
|
|
55016
|
+
})
|
|
54894
55017
|
}),
|
|
54895
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54896
|
-
|
|
54897
|
-
|
|
54898
|
-
|
|
54899
|
-
|
|
54900
|
-
|
|
55018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 bg-gradient-to-b from-surface/50 via-transparent to-surface/50" }),
|
|
55019
|
+
on && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55020
|
+
"aria-hidden": true,
|
|
55021
|
+
className: "acv-glow pointer-events-none absolute left-1/2 top-1/2 h-16 w-16 -translate-x-1/2 -translate-y-1/2 rounded-full blur-2xl",
|
|
55022
|
+
style: {
|
|
55023
|
+
background: accent,
|
|
55024
|
+
animation: "acv-glow 3.2s ease-in-out infinite"
|
|
55025
|
+
}
|
|
54901
55026
|
}),
|
|
54902
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
54903
|
-
|
|
54904
|
-
|
|
54905
|
-
|
|
54906
|
-
|
|
54907
|
-
|
|
55027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55028
|
+
className: "relative z-10 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide",
|
|
55029
|
+
style: { color: accent },
|
|
55030
|
+
children: [
|
|
55031
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
|
|
55032
|
+
mode: display.mode,
|
|
55033
|
+
heatingOnly,
|
|
55034
|
+
className: "h-3.5 w-3.5"
|
|
55035
|
+
}),
|
|
55036
|
+
modeLabel(t, display.mode),
|
|
55037
|
+
display.currentTemp !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55038
|
+
className: "normal-case text-foreground-subtle",
|
|
55039
|
+
children: t("uiLibrary.acPanel.now", "· {{temp}}° now", { temp: display.currentTemp })
|
|
55040
|
+
}),
|
|
55041
|
+
display.currentHumidity !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55042
|
+
className: "inline-flex items-center gap-0.5 normal-case text-foreground-subtle",
|
|
55043
|
+
children: [
|
|
55044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplets, { className: "h-3 w-3" }),
|
|
55045
|
+
display.currentHumidity,
|
|
55046
|
+
"%"
|
|
55047
|
+
]
|
|
55048
|
+
})
|
|
55049
|
+
]
|
|
54908
55050
|
}),
|
|
54909
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
54910
|
-
|
|
54911
|
-
|
|
54912
|
-
|
|
54913
|
-
|
|
54914
|
-
|
|
55051
|
+
display.target !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55052
|
+
className: "relative z-10 flex items-center gap-3",
|
|
55053
|
+
children: [
|
|
55054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep, {
|
|
55055
|
+
label: t("uiLibrary.acPanel.decrease", "Decrease target"),
|
|
55056
|
+
symbol: "−",
|
|
55057
|
+
disabled: busy,
|
|
55058
|
+
onClick: () => {
|
|
55059
|
+
if (busy || display.target === null) return;
|
|
55060
|
+
const next = nextClimateTarget(display.target, -1, display);
|
|
55061
|
+
applyOptimistic({ target: next });
|
|
55062
|
+
cap.setTarget({ target: next });
|
|
55063
|
+
}
|
|
55064
|
+
}),
|
|
55065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55066
|
+
className: "flex items-baseline gap-0.5",
|
|
55067
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55068
|
+
className: cn("text-4xl font-bold leading-none tabular-nums", climateTargetWidthClass(display)),
|
|
55069
|
+
style: { color: accent },
|
|
55070
|
+
children: display.target
|
|
55071
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55072
|
+
className: "text-lg text-foreground-subtle",
|
|
55073
|
+
children: "°"
|
|
55074
|
+
})]
|
|
55075
|
+
}),
|
|
55076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep, {
|
|
55077
|
+
label: t("uiLibrary.acPanel.increase", "Increase target"),
|
|
55078
|
+
symbol: "+",
|
|
55079
|
+
disabled: busy,
|
|
55080
|
+
onClick: () => {
|
|
55081
|
+
if (busy || display.target === null) return;
|
|
55082
|
+
const next = nextClimateTarget(display.target, 1, display);
|
|
55083
|
+
applyOptimistic({ target: next });
|
|
55084
|
+
cap.setTarget({ target: next });
|
|
55085
|
+
}
|
|
55086
|
+
})
|
|
55087
|
+
]
|
|
55088
|
+
}), setpointBounds !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55089
|
+
className: "relative z-10 w-full px-1",
|
|
55090
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeTrack, {
|
|
55091
|
+
value: display.target,
|
|
55092
|
+
min: setpointBounds.min,
|
|
55093
|
+
max: setpointBounds.max,
|
|
55094
|
+
step: display.tempStep,
|
|
55095
|
+
accent,
|
|
55096
|
+
disabled: busy,
|
|
55097
|
+
label: t("uiLibrary.acPanel.setpoint", "Setpoint"),
|
|
55098
|
+
onPreview: (next) => {
|
|
55099
|
+
applyOptimistic({ target: next });
|
|
55100
|
+
},
|
|
55101
|
+
onCommit: (next) => {
|
|
55102
|
+
applyOptimistic({ target: next });
|
|
55103
|
+
cap.setTarget({ target: next });
|
|
55104
|
+
}
|
|
55105
|
+
})
|
|
55106
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55107
|
+
className: "relative z-10 flex items-baseline gap-0.5",
|
|
55108
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55109
|
+
className: cn("text-4xl font-bold leading-none tabular-nums text-foreground", climateTargetWidthClass(display)),
|
|
55110
|
+
children: display.currentTemp ?? "—"
|
|
55111
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55112
|
+
className: "text-lg text-foreground-subtle",
|
|
55113
|
+
children: "°"
|
|
55114
|
+
})]
|
|
55115
|
+
})
|
|
55116
|
+
]
|
|
55117
|
+
}),
|
|
55118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55119
|
+
className: "relative z-10 grid grid-cols-3 gap-2",
|
|
55120
|
+
children: [
|
|
55121
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompactSelect, {
|
|
55122
|
+
label: t("uiLibrary.acPanel.mode", "Mode"),
|
|
55123
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55124
|
+
style: { color: accent },
|
|
55125
|
+
className: "flex",
|
|
55126
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
|
|
55127
|
+
mode: display.mode,
|
|
55128
|
+
heatingOnly,
|
|
55129
|
+
className: "h-4 w-4"
|
|
55130
|
+
})
|
|
55131
|
+
}),
|
|
55132
|
+
value: display.mode,
|
|
55133
|
+
options: display.availableModes.map((m) => ({
|
|
55134
|
+
value: m,
|
|
55135
|
+
label: modeLabel(t, m)
|
|
55136
|
+
})),
|
|
55137
|
+
disabled: busy,
|
|
55138
|
+
onChange: (v) => {
|
|
55139
|
+
const mode = display.availableModes.find((m) => m === v);
|
|
55140
|
+
if (mode) {
|
|
55141
|
+
applyOptimistic({ mode });
|
|
55142
|
+
cap.setMode({ mode });
|
|
55143
|
+
}
|
|
55144
|
+
}
|
|
54915
55145
|
}),
|
|
54916
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54917
|
-
label: t("uiLibrary.
|
|
54918
|
-
|
|
54919
|
-
|
|
54920
|
-
|
|
54921
|
-
|
|
55146
|
+
display.availableFanModes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompactSelect, {
|
|
55147
|
+
label: t("uiLibrary.acPanel.fan", "Fan"),
|
|
55148
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-4 w-4 text-sky-500" }),
|
|
55149
|
+
value: display.fanMode,
|
|
55150
|
+
options: display.availableFanModes.map((f) => ({
|
|
55151
|
+
value: f,
|
|
55152
|
+
label: fanLabel(f)
|
|
55153
|
+
})),
|
|
55154
|
+
disabled: busy,
|
|
55155
|
+
onChange: (v) => {
|
|
55156
|
+
applyOptimistic({ fanMode: v });
|
|
55157
|
+
cap.setFanMode({ fanMode: v });
|
|
55158
|
+
}
|
|
54922
55159
|
}),
|
|
54923
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54924
|
-
label: t("uiLibrary.
|
|
54925
|
-
|
|
54926
|
-
|
|
54927
|
-
|
|
54928
|
-
|
|
55160
|
+
display.availablePresets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompactSelect, {
|
|
55161
|
+
label: t("uiLibrary.acPanel.preset", "Preset"),
|
|
55162
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CurrentPresetIcon, { className: "h-4 w-4 text-emerald-500" }),
|
|
55163
|
+
value: display.preset,
|
|
55164
|
+
options: display.availablePresets.map((p) => ({
|
|
55165
|
+
value: p,
|
|
55166
|
+
label: fanLabel(p)
|
|
55167
|
+
})),
|
|
55168
|
+
disabled: busy,
|
|
55169
|
+
onChange: (v) => {
|
|
55170
|
+
applyOptimistic({ preset: v });
|
|
55171
|
+
cap.setPreset({ preset: v });
|
|
55172
|
+
}
|
|
54929
55173
|
})
|
|
54930
55174
|
]
|
|
54931
|
-
})
|
|
54932
|
-
]
|
|
54933
|
-
});
|
|
54934
|
-
}
|
|
54935
|
-
/**
|
|
54936
|
-
* One boolean setting. `checked === null` is "the appliance has not said": on a reachable
|
|
54937
|
-
* appliance that means it does not offer the setting (hidden); on an unreachable one the
|
|
54938
|
-
* setting is shown but disabled and marked unknown — never as an unchecked box.
|
|
54939
|
-
*/
|
|
54940
|
-
function ToggleRow({ label, checked, shown, busy, onChange }) {
|
|
54941
|
-
const t = useUiText();
|
|
54942
|
-
if (checked === null && shown) return null;
|
|
54943
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
54944
|
-
className: "inline-flex items-center gap-2 cursor-pointer",
|
|
54945
|
-
children: [
|
|
54946
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
54947
|
-
type: "checkbox",
|
|
54948
|
-
checked: checked === true,
|
|
54949
|
-
"aria-checked": checked === null ? "mixed" : checked,
|
|
54950
|
-
disabled: busy || checked === null,
|
|
54951
|
-
onChange: (e) => {
|
|
54952
|
-
onChange(e.target.checked);
|
|
54953
|
-
},
|
|
54954
|
-
onClick: (e) => {
|
|
54955
|
-
e.stopPropagation();
|
|
54956
|
-
},
|
|
54957
|
-
className: "rounded border-border"
|
|
54958
55175
|
}),
|
|
54959
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54960
|
-
|
|
54961
|
-
|
|
54962
|
-
|
|
55176
|
+
(showVSwing || showHSwing) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
55177
|
+
label: t("uiLibrary.acPanel.swing", "Swing"),
|
|
55178
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55179
|
+
className: "grid grid-cols-2 gap-1",
|
|
55180
|
+
children: [showVSwing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwingToggle, {
|
|
55181
|
+
axis: "vertical",
|
|
55182
|
+
on: display.swingVertical ?? false,
|
|
55183
|
+
busy,
|
|
55184
|
+
onToggle: (next) => {
|
|
55185
|
+
applyOptimistic({ swingVertical: next });
|
|
55186
|
+
cap.setSwingVertical({ on: next });
|
|
55187
|
+
}
|
|
55188
|
+
}), showHSwing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwingToggle, {
|
|
55189
|
+
axis: "horizontal",
|
|
55190
|
+
on: display.swingHorizontal ?? false,
|
|
55191
|
+
busy,
|
|
55192
|
+
onToggle: (next) => {
|
|
55193
|
+
applyOptimistic({ swingHorizontal: next });
|
|
55194
|
+
cap.setSwingHorizontal({ on: next });
|
|
55195
|
+
}
|
|
55196
|
+
})]
|
|
55197
|
+
})
|
|
54963
55198
|
})
|
|
54964
55199
|
]
|
|
54965
55200
|
});
|
|
54966
55201
|
}
|
|
54967
|
-
|
|
54968
|
-
|
|
54969
|
-
|
|
54970
|
-
|
|
55202
|
+
/** Labelled control group — a small uppercase header above its control. */
|
|
55203
|
+
function Field({ label, children }) {
|
|
55204
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55205
|
+
className: "relative z-10 flex flex-col gap-1",
|
|
55206
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55207
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
|
|
55208
|
+
children: label
|
|
55209
|
+
}), children]
|
|
54971
55210
|
});
|
|
54972
55211
|
}
|
|
54973
|
-
function
|
|
54974
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
54975
|
-
|
|
54976
|
-
|
|
55212
|
+
function ModeIcon({ mode, heatingOnly = false, className }) {
|
|
55213
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(heatingOnly && mode === "heat" ? Heater : MODE_ICON[mode], {
|
|
55214
|
+
className,
|
|
55215
|
+
"aria-hidden": true
|
|
54977
55216
|
});
|
|
54978
55217
|
}
|
|
54979
|
-
|
|
54980
|
-
|
|
54981
|
-
|
|
54982
|
-
|
|
54983
|
-
|
|
54984
|
-
|
|
54985
|
-
|
|
54986
|
-
|
|
54987
|
-
|
|
54988
|
-
|
|
54989
|
-
|
|
54990
|
-
|
|
54991
|
-
|
|
54992
|
-
|
|
54993
|
-
|
|
54994
|
-
|
|
54995
|
-
|
|
54996
|
-
|
|
54997
|
-
|
|
54998
|
-
|
|
54999
|
-
.
|
|
55000
|
-
|
|
55001
|
-
|
|
55218
|
+
/** Compact labelled dropdown — a leading icon + native `<select>` + chevron,
|
|
55219
|
+
* used for the Mode / Fan / Preset selectors so all three fit in one row. */
|
|
55220
|
+
function CompactSelect({ label, icon, value, options, disabled, onChange }) {
|
|
55221
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55222
|
+
className: "flex min-w-0 flex-col gap-1",
|
|
55223
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55224
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
|
|
55225
|
+
children: label
|
|
55226
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55227
|
+
className: cn("flex items-center gap-1 overflow-hidden rounded-lg border border-border/60 bg-surface-subtle px-1.5 py-1.5 has-[:focus-visible]:border-primary", disabled && "opacity-40"),
|
|
55228
|
+
children: [
|
|
55229
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55230
|
+
className: "flex shrink-0",
|
|
55231
|
+
children: icon
|
|
55232
|
+
}),
|
|
55233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
55234
|
+
"aria-label": label,
|
|
55235
|
+
value,
|
|
55236
|
+
disabled,
|
|
55237
|
+
onChange: (e) => {
|
|
55238
|
+
e.stopPropagation();
|
|
55239
|
+
if (!disabled) onChange(e.target.value);
|
|
55240
|
+
},
|
|
55241
|
+
className: "min-w-0 flex-1 cursor-pointer appearance-none truncate bg-transparent text-xs font-medium text-foreground outline-none",
|
|
55242
|
+
children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
55243
|
+
value: o.value,
|
|
55244
|
+
children: o.label
|
|
55245
|
+
}, o.value))
|
|
55246
|
+
}),
|
|
55247
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 shrink-0 text-foreground-subtle" })
|
|
55248
|
+
]
|
|
55249
|
+
})]
|
|
55250
|
+
});
|
|
55002
55251
|
}
|
|
55003
|
-
|
|
55004
|
-
|
|
55005
|
-
|
|
55006
|
-
|
|
55252
|
+
/** Circular ± button for the target. */
|
|
55253
|
+
function RoundStep({ label, symbol, disabled, onClick }) {
|
|
55254
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
55255
|
+
type: "button",
|
|
55256
|
+
"aria-label": label,
|
|
55257
|
+
disabled,
|
|
55258
|
+
onClick,
|
|
55259
|
+
className: cn("flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-border bg-surface text-lg leading-none text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground active:scale-95", disabled && "opacity-40 cursor-not-allowed"),
|
|
55260
|
+
children: symbol
|
|
55261
|
+
});
|
|
55007
55262
|
}
|
|
55008
|
-
/**
|
|
55009
|
-
function
|
|
55010
|
-
|
|
55011
|
-
|
|
55012
|
-
|
|
55263
|
+
/** Setpoint track. Drag previews; the command is sent when the pointer is released. */
|
|
55264
|
+
function RangeTrack({ value, min, max, step, accent, disabled, label, onPreview, onCommit }) {
|
|
55265
|
+
const trackRef = (0, react$1.useRef)(null);
|
|
55266
|
+
const draggingRef = (0, react$1.useRef)(false);
|
|
55267
|
+
const [draft, setDraft] = (0, react$1.useState)(null);
|
|
55268
|
+
const shown = draft ?? value;
|
|
55269
|
+
const pct = `${Math.min(1, Math.max(0, (shown - min) / (max - min))) * 100}%`;
|
|
55270
|
+
const tempAt = (clientX) => {
|
|
55271
|
+
const el = trackRef.current;
|
|
55272
|
+
if (el === null) return value;
|
|
55273
|
+
const rect = el.getBoundingClientRect();
|
|
55274
|
+
if (rect.width <= 0) return value;
|
|
55275
|
+
return climateTargetFromFraction((clientX - rect.left) / rect.width, {
|
|
55276
|
+
min,
|
|
55277
|
+
max
|
|
55278
|
+
}, step);
|
|
55013
55279
|
};
|
|
55014
|
-
|
|
55015
|
-
|
|
55016
|
-
|
|
55280
|
+
const onPointerDown = (event) => {
|
|
55281
|
+
if (disabled) return;
|
|
55282
|
+
event.stopPropagation();
|
|
55283
|
+
event.preventDefault();
|
|
55284
|
+
draggingRef.current = true;
|
|
55285
|
+
if (typeof event.currentTarget.setPointerCapture === "function") event.currentTarget.setPointerCapture(event.pointerId);
|
|
55286
|
+
const next = tempAt(event.clientX);
|
|
55287
|
+
setDraft(next);
|
|
55288
|
+
onPreview(next);
|
|
55017
55289
|
};
|
|
55018
|
-
|
|
55019
|
-
|
|
55020
|
-
|
|
55290
|
+
const onPointerMove = (event) => {
|
|
55291
|
+
if (disabled || !draggingRef.current) return;
|
|
55292
|
+
event.stopPropagation();
|
|
55293
|
+
const next = tempAt(event.clientX);
|
|
55294
|
+
setDraft(next);
|
|
55295
|
+
onPreview(next);
|
|
55021
55296
|
};
|
|
55022
|
-
|
|
55023
|
-
|
|
55024
|
-
|
|
55025
|
-
|
|
55026
|
-
|
|
55027
|
-
|
|
55028
|
-
|
|
55029
|
-
|
|
55030
|
-
return {
|
|
55031
|
-
running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState),
|
|
55032
|
-
power: useDeviceState(props.trpc, props.deviceId, (s) => s.powerMeter)
|
|
55297
|
+
const endDrag = (event) => {
|
|
55298
|
+
if (!draggingRef.current) return;
|
|
55299
|
+
event.stopPropagation();
|
|
55300
|
+
draggingRef.current = false;
|
|
55301
|
+
const next = tempAt(event.clientX);
|
|
55302
|
+
setDraft(null);
|
|
55303
|
+
if (event.currentTarget.hasPointerCapture?.(event.pointerId) === true) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
55304
|
+
onCommit(next);
|
|
55033
55305
|
};
|
|
55306
|
+
const onKeyDown = (event) => {
|
|
55307
|
+
if (disabled) return;
|
|
55308
|
+
const current = draft ?? value;
|
|
55309
|
+
if (event.key === "ArrowRight" || event.key === "ArrowUp") {
|
|
55310
|
+
event.preventDefault();
|
|
55311
|
+
onCommit(nextClimateTarget(current, 1, {
|
|
55312
|
+
tempStep: step,
|
|
55313
|
+
minTemp: min,
|
|
55314
|
+
maxTemp: max
|
|
55315
|
+
}));
|
|
55316
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
|
|
55317
|
+
event.preventDefault();
|
|
55318
|
+
onCommit(nextClimateTarget(current, -1, {
|
|
55319
|
+
tempStep: step,
|
|
55320
|
+
minTemp: min,
|
|
55321
|
+
maxTemp: max
|
|
55322
|
+
}));
|
|
55323
|
+
} else if (event.key === "Home") {
|
|
55324
|
+
event.preventDefault();
|
|
55325
|
+
onCommit(min);
|
|
55326
|
+
} else if (event.key === "End") {
|
|
55327
|
+
event.preventDefault();
|
|
55328
|
+
onCommit(max);
|
|
55329
|
+
}
|
|
55330
|
+
};
|
|
55331
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55332
|
+
ref: trackRef,
|
|
55333
|
+
role: "slider",
|
|
55334
|
+
"aria-label": label,
|
|
55335
|
+
"aria-valuemin": min,
|
|
55336
|
+
"aria-valuemax": max,
|
|
55337
|
+
"aria-valuenow": shown,
|
|
55338
|
+
"aria-disabled": disabled,
|
|
55339
|
+
tabIndex: disabled ? -1 : 0,
|
|
55340
|
+
onPointerDown,
|
|
55341
|
+
onPointerMove,
|
|
55342
|
+
onPointerUp: endDrag,
|
|
55343
|
+
onPointerCancel: endDrag,
|
|
55344
|
+
onKeyDown,
|
|
55345
|
+
className: cn("mt-1 w-full touch-none select-none py-2", disabled ? "cursor-not-allowed opacity-40" : "cursor-ew-resize"),
|
|
55346
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55347
|
+
className: "relative h-1.5 w-full rounded-full bg-surface-subtle",
|
|
55348
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55349
|
+
className: "absolute inset-y-0 left-0 rounded-full",
|
|
55350
|
+
style: {
|
|
55351
|
+
width: pct,
|
|
55352
|
+
background: accent
|
|
55353
|
+
}
|
|
55354
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55355
|
+
className: "absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-surface shadow",
|
|
55356
|
+
style: {
|
|
55357
|
+
left: pct,
|
|
55358
|
+
background: accent
|
|
55359
|
+
}
|
|
55360
|
+
})]
|
|
55361
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55362
|
+
className: "mt-0.5 flex justify-between text-[9px] tabular-nums text-foreground-subtle",
|
|
55363
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [min, "°"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [max, "°"] })]
|
|
55364
|
+
})]
|
|
55365
|
+
});
|
|
55034
55366
|
}
|
|
55035
|
-
/**
|
|
55036
|
-
|
|
55367
|
+
/**
|
|
55368
|
+
* Animated centerpiece. `fan_only` renders a spinning fan; `off` a dim static
|
|
55369
|
+
* vent; every active mode renders drifting particles that FALL (cool/dry/auto)
|
|
55370
|
+
* or RISE (heat), with a horizontal sweep overlaid when a swing axis is active.
|
|
55371
|
+
*/
|
|
55372
|
+
function AirflowViz({ mode, accent, periodSec, swaying, forceRising = false }) {
|
|
55373
|
+
const columns = [
|
|
55374
|
+
0,
|
|
55375
|
+
1,
|
|
55376
|
+
2,
|
|
55377
|
+
3,
|
|
55378
|
+
4,
|
|
55379
|
+
5
|
|
55380
|
+
];
|
|
55381
|
+
const rising = forceRising || mode === "heat";
|
|
55382
|
+
const isDroplet = mode === "dry";
|
|
55037
55383
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55038
|
-
"aria-hidden":
|
|
55039
|
-
|
|
55040
|
-
|
|
55041
|
-
|
|
55042
|
-
|
|
55043
|
-
|
|
55044
|
-
|
|
55045
|
-
children:
|
|
55046
|
-
|
|
55047
|
-
|
|
55048
|
-
|
|
55049
|
-
|
|
55384
|
+
"aria-hidden": true,
|
|
55385
|
+
className: "absolute inset-0 overflow-hidden",
|
|
55386
|
+
children: mode === "off" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55387
|
+
className: "flex h-full items-center justify-center gap-2 opacity-30",
|
|
55388
|
+
children: columns.map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-8 w-1 rounded-full bg-foreground-subtle opacity-40" }, i))
|
|
55389
|
+
}) : mode === "fan_only" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55390
|
+
className: "flex h-full items-center justify-center",
|
|
55391
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FanBlades, {
|
|
55392
|
+
color: accent,
|
|
55393
|
+
periodSec: Math.max(.5, periodSec * .7),
|
|
55394
|
+
size: 56
|
|
55395
|
+
})
|
|
55396
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55397
|
+
"data-testid": forceRising ? "climate-heat-rise" : void 0,
|
|
55398
|
+
className: cn("acv-sway-wrap absolute inset-0", swaying && "acv-sway"),
|
|
55399
|
+
style: swaying ? { animation: "acv-sway 2.6s ease-in-out infinite" } : void 0,
|
|
55400
|
+
children: columns.map((i) => {
|
|
55401
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55402
|
+
className: "acv-streak absolute rounded-full",
|
|
55403
|
+
style: {
|
|
55404
|
+
left: `${8 + i * 15}%`,
|
|
55405
|
+
width: isDroplet ? "0.4rem" : "0.3rem",
|
|
55406
|
+
height: isDroplet ? "0.4rem" : "1.9rem",
|
|
55407
|
+
background: accent,
|
|
55408
|
+
boxShadow: `0 0 6px ${accent}`,
|
|
55409
|
+
animationName: rising ? "acv-rise" : "acv-fall",
|
|
55410
|
+
animationTimingFunction: "linear",
|
|
55411
|
+
animationIterationCount: "infinite",
|
|
55412
|
+
animationDuration: `${periodSec}s`,
|
|
55413
|
+
animationDelay: `${i % 3 * (periodSec / 3) + i * .05}s`
|
|
55414
|
+
}
|
|
55415
|
+
}, i);
|
|
55416
|
+
})
|
|
55050
55417
|
})
|
|
55051
55418
|
});
|
|
55052
55419
|
}
|
|
55053
|
-
|
|
55054
|
-
|
|
55055
|
-
|
|
55056
|
-
|
|
55057
|
-
|
|
55058
|
-
|
|
55059
|
-
const dwell = running === void 0 ? null : dwellText(state, running.since, Date.now(), t);
|
|
55060
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55061
|
-
className: "flex items-center gap-4 rounded-lg border border-border bg-surface p-4",
|
|
55420
|
+
/** Still radiator fins — the off state of a heating-only climate. */
|
|
55421
|
+
function RadiatorFins() {
|
|
55422
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55423
|
+
"data-testid": "climate-radiator",
|
|
55424
|
+
"aria-hidden": true,
|
|
55425
|
+
className: "flex h-full items-end justify-center gap-1.5 px-8 pb-3 opacity-40",
|
|
55062
55426
|
children: [
|
|
55063
|
-
|
|
55064
|
-
|
|
55065
|
-
|
|
55066
|
-
|
|
55067
|
-
|
|
55068
|
-
|
|
55069
|
-
|
|
55070
|
-
|
|
55071
|
-
"data-testid": "appliance-state",
|
|
55072
|
-
className: cn("text-3xl font-semibold", meta.tone),
|
|
55073
|
-
children: meta.label
|
|
55074
|
-
}), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55075
|
-
className: "text-sm tabular-nums text-foreground-subtle",
|
|
55076
|
-
children: dwell
|
|
55077
|
-
})]
|
|
55078
|
-
}),
|
|
55079
|
-
phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55080
|
-
className: "text-sm text-foreground-subtle",
|
|
55081
|
-
children: phase
|
|
55082
|
-
}),
|
|
55083
|
-
power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55084
|
-
"data-testid": "appliance-power",
|
|
55085
|
-
className: "inline-flex items-center gap-1 text-sm tabular-nums text-foreground-muted",
|
|
55086
|
-
title: power.power === void 0 ? t("uiLibrary.appliance.powerUnknown", "Power unknown") : void 0,
|
|
55087
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
|
|
55088
|
-
className: "h-3.5 w-3.5",
|
|
55089
|
-
"aria-hidden": true
|
|
55090
|
-
}), formatWatts(power.power)]
|
|
55091
|
-
})
|
|
55092
|
-
]
|
|
55093
|
-
})
|
|
55094
|
-
]
|
|
55427
|
+
0,
|
|
55428
|
+
1,
|
|
55429
|
+
2,
|
|
55430
|
+
3,
|
|
55431
|
+
4,
|
|
55432
|
+
5,
|
|
55433
|
+
6
|
|
55434
|
+
].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-10 w-1.5 rounded-t-full bg-foreground-subtle" }, i))
|
|
55095
55435
|
});
|
|
55096
55436
|
}
|
|
55097
|
-
|
|
55098
|
-
|
|
55099
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
55100
|
-
|
|
55101
|
-
|
|
55102
|
-
|
|
55103
|
-
|
|
55104
|
-
|
|
55105
|
-
|
|
55106
|
-
|
|
55107
|
-
}
|
|
55108
|
-
|
|
55109
|
-
|
|
55437
|
+
/** Big spinning fan for `fan_only` mode. */
|
|
55438
|
+
function FanBlades({ color, periodSec, size }) {
|
|
55439
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
55440
|
+
width: size,
|
|
55441
|
+
height: size,
|
|
55442
|
+
viewBox: "0 0 100 100",
|
|
55443
|
+
className: "acv-fan",
|
|
55444
|
+
style: {
|
|
55445
|
+
animation: `acv-spin ${periodSec}s linear infinite`,
|
|
55446
|
+
transformOrigin: "50% 50%"
|
|
55447
|
+
},
|
|
55448
|
+
"aria-hidden": true,
|
|
55449
|
+
children: [[
|
|
55450
|
+
0,
|
|
55451
|
+
120,
|
|
55452
|
+
240
|
|
55453
|
+
].map((deg) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
55454
|
+
d: "M50 50 C 58 30, 78 26, 84 12 C 66 20, 54 34, 50 50 Z",
|
|
55455
|
+
fill: color,
|
|
55456
|
+
opacity: .85,
|
|
55457
|
+
transform: `rotate(${deg} 50 50)`
|
|
55458
|
+
}, deg)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
55459
|
+
cx: "50",
|
|
55460
|
+
cy: "50",
|
|
55461
|
+
r: "7",
|
|
55462
|
+
fill: color
|
|
55110
55463
|
})]
|
|
55111
55464
|
});
|
|
55112
55465
|
}
|
|
55113
|
-
|
|
55114
|
-
|
|
55115
|
-
function
|
|
55116
|
-
return position !== null ? `${position}%` : "—";
|
|
55117
|
-
}
|
|
55118
|
-
function HeatingValveReadout({ trpc, deviceId, layout }) {
|
|
55466
|
+
/** Swing axis toggle — a card with a MoveVertical/Horizontal glyph that
|
|
55467
|
+
* oscillates along its axis when on. On/off is non-destructive → plain tap. */
|
|
55468
|
+
function SwingToggle({ axis, on, busy, onToggle }) {
|
|
55119
55469
|
const t = useUiText();
|
|
55120
|
-
const
|
|
55470
|
+
const label = axis === "vertical" ? t("uiLibrary.acPanel.vertical", "Vertical") : t("uiLibrary.acPanel.horizontal", "Horizontal");
|
|
55471
|
+
const Icon = axis === "vertical" ? MoveVertical : MoveHorizontal;
|
|
55472
|
+
const anim = axis === "vertical" ? "acv-swing-v" : "acv-swing-h";
|
|
55473
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
55474
|
+
type: "button",
|
|
55475
|
+
"aria-label": t("uiLibrary.acPanel.swingAxis", "{{axis}} swing", { axis: label }),
|
|
55476
|
+
"aria-pressed": on,
|
|
55477
|
+
disabled: busy,
|
|
55478
|
+
onClick: () => {
|
|
55479
|
+
if (!busy) onToggle(!on);
|
|
55480
|
+
},
|
|
55481
|
+
className: cn("flex items-center justify-center gap-2 rounded-lg border px-2 py-1.5 text-xs font-medium transition-colors", on ? "border-transparent bg-indigo-400 text-indigo-950 shadow-sm" : "border-border/60 bg-surface-subtle text-foreground-subtle hover:bg-surface-hover hover:text-foreground", busy && "opacity-40 cursor-not-allowed"),
|
|
55482
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
55483
|
+
className: "acv-swing h-4 w-4",
|
|
55484
|
+
"aria-hidden": true,
|
|
55485
|
+
style: on ? { animation: `${anim} 1.4s ease-in-out infinite` } : void 0
|
|
55486
|
+
}), label]
|
|
55487
|
+
});
|
|
55488
|
+
}
|
|
55489
|
+
//#endregion
|
|
55490
|
+
//#region src/composites/device-controls/heater-control.tsx
|
|
55491
|
+
/**
|
|
55492
|
+
* Heater controls for a `DeviceType.Heater` device (a dumb plug-in stove).
|
|
55493
|
+
*
|
|
55494
|
+
* A heater is a switch that heats while it is on, and the hub can switch it off
|
|
55495
|
+
* again after a chosen time (`timed-run`, D993):
|
|
55496
|
+
* - HeaterInlineControl (device-list row): the on/off switch and a one-tap
|
|
55497
|
+
* "Run N min".
|
|
55498
|
+
* - HeaterHeroCard (device-detail hero): the glowing heat backdrop (the same
|
|
55499
|
+
* rising streaks a heating air conditioner draws) while it is on, the
|
|
55500
|
+
* on/off switch, the power draw when the plug measures one, and the timed
|
|
55501
|
+
* run (number, unit, Run, countdown, Cancel).
|
|
55502
|
+
*
|
|
55503
|
+
* Every reading may be unknown (`null`) and is drawn as a dash, never as zero.
|
|
55504
|
+
*/
|
|
55505
|
+
var HEATER_LIMIT = _camstack_types.TIMED_RUN_LIMITS[_camstack_types.DeviceType.Heater];
|
|
55506
|
+
var QUICK_RUN_MINUTES = 30;
|
|
55507
|
+
var HEAT_ACCENT = "var(--color-warning)";
|
|
55508
|
+
var HEAT_PERIOD_SEC = 1.8;
|
|
55509
|
+
function useHeater({ trpc, deviceId, optimistic }) {
|
|
55510
|
+
const { cap, slice, available } = useDeviceCapability({
|
|
55121
55511
|
trpc,
|
|
55122
55512
|
deviceId,
|
|
55123
|
-
cap: (d) => d.
|
|
55124
|
-
slice: (d) => d.state.
|
|
55513
|
+
cap: (d) => d.switch,
|
|
55514
|
+
slice: (d) => d.state.switch
|
|
55125
55515
|
});
|
|
55126
|
-
|
|
55127
|
-
|
|
55128
|
-
|
|
55129
|
-
|
|
55130
|
-
|
|
55131
|
-
"aria-label": `${label} ${value}`,
|
|
55132
|
-
children: value
|
|
55516
|
+
const running = useDeviceCapability({
|
|
55517
|
+
trpc,
|
|
55518
|
+
deviceId,
|
|
55519
|
+
cap: (d) => d.runningState,
|
|
55520
|
+
slice: (d) => d.state.runningState
|
|
55133
55521
|
});
|
|
55134
|
-
|
|
55135
|
-
|
|
55136
|
-
|
|
55137
|
-
|
|
55138
|
-
|
|
55139
|
-
})]
|
|
55522
|
+
const power = useDeviceCapability({
|
|
55523
|
+
trpc,
|
|
55524
|
+
deviceId,
|
|
55525
|
+
cap: (d) => d.powerMeter,
|
|
55526
|
+
slice: (d) => d.state.powerMeter
|
|
55140
55527
|
});
|
|
55141
|
-
|
|
55142
|
-
|
|
55143
|
-
|
|
55144
|
-
|
|
55145
|
-
|
|
55146
|
-
|
|
55147
|
-
|
|
55148
|
-
"dry",
|
|
55149
|
-
"fan_only"
|
|
55150
|
-
]);
|
|
55151
|
-
/** A heater offers heat (and maybe auto/off) and none of the cooling modes. */
|
|
55152
|
-
function climateOffersCooling(modes) {
|
|
55153
|
-
return modes.some((mode) => COOLING_MODES.has(mode));
|
|
55154
|
-
}
|
|
55155
|
-
/** Setpoint track bounds. Absent when the integration did not report both ends. */
|
|
55156
|
-
function climateSetpointBounds(status) {
|
|
55157
|
-
const min = status.minTemp;
|
|
55158
|
-
const max = status.maxTemp;
|
|
55159
|
-
if (typeof min !== "number" || typeof max !== "number") return null;
|
|
55160
|
-
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
|
|
55528
|
+
const timer = useDeviceCapability({
|
|
55529
|
+
trpc,
|
|
55530
|
+
deviceId,
|
|
55531
|
+
cap: (d) => d.timedRun,
|
|
55532
|
+
slice: (d) => d.state.timedRun
|
|
55533
|
+
});
|
|
55534
|
+
const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
|
|
55161
55535
|
return {
|
|
55162
|
-
|
|
55163
|
-
|
|
55536
|
+
cap,
|
|
55537
|
+
display,
|
|
55538
|
+
available,
|
|
55539
|
+
applyOptimistic,
|
|
55540
|
+
running: running.slice,
|
|
55541
|
+
power: power.slice,
|
|
55542
|
+
timer
|
|
55164
55543
|
};
|
|
55165
55544
|
}
|
|
55166
|
-
|
|
55167
|
-
|
|
55168
|
-
const
|
|
55169
|
-
|
|
55170
|
-
const
|
|
55171
|
-
const
|
|
55172
|
-
|
|
55173
|
-
|
|
55174
|
-
|
|
55175
|
-
|
|
55176
|
-
|
|
55177
|
-
|
|
55178
|
-
|
|
55179
|
-
|
|
55180
|
-
|
|
55181
|
-
|
|
55182
|
-
|
|
55183
|
-
|
|
55184
|
-
|
|
55185
|
-
|
|
55186
|
-
|
|
55545
|
+
function HeaterInlineControl(props) {
|
|
55546
|
+
const t = useUiText();
|
|
55547
|
+
const { cap, display, available, applyOptimistic, timer } = useHeater(props);
|
|
55548
|
+
if (cap === void 0 || display === void 0) return null;
|
|
55549
|
+
const on = display.on === true;
|
|
55550
|
+
const quickMinutes = HEATER_LIMIT === void 0 ? QUICK_RUN_MINUTES : Math.min(QUICK_RUN_MINUTES, maxInUnit(HEATER_LIMIT));
|
|
55551
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55552
|
+
className: "inline-flex items-center gap-2",
|
|
55553
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55554
|
+
onClick: (e) => e.stopPropagation(),
|
|
55555
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
55556
|
+
checked: on,
|
|
55557
|
+
disabled: !available,
|
|
55558
|
+
"aria-label": t("uiLibrary.heater.power", "Heater"),
|
|
55559
|
+
onCheckedChange: (next) => {
|
|
55560
|
+
applyOptimistic({ on: next });
|
|
55561
|
+
cap.setState({ on: next });
|
|
55562
|
+
}
|
|
55563
|
+
})
|
|
55564
|
+
}), timer.cap !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55565
|
+
onClick: (e) => e.stopPropagation(),
|
|
55566
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
55567
|
+
size: "sm",
|
|
55568
|
+
variant: "outline",
|
|
55569
|
+
disabled: !timer.online,
|
|
55570
|
+
onClick: () => void timer.cap?.runFor({
|
|
55571
|
+
duration: quickMinutes,
|
|
55572
|
+
unit: "min"
|
|
55573
|
+
}),
|
|
55574
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, {
|
|
55575
|
+
className: "h-3.5 w-3.5",
|
|
55576
|
+
"aria-hidden": true
|
|
55577
|
+
}), t("uiLibrary.heater.quickRun", "Run {{minutes}} min", { minutes: quickMinutes })]
|
|
55578
|
+
})
|
|
55579
|
+
})]
|
|
55580
|
+
});
|
|
55187
55581
|
}
|
|
55188
|
-
|
|
55189
|
-
|
|
55190
|
-
|
|
55191
|
-
|
|
55192
|
-
|
|
55193
|
-
|
|
55194
|
-
|
|
55195
|
-
|
|
55196
|
-
|
|
55197
|
-
|
|
55198
|
-
|
|
55199
|
-
|
|
55200
|
-
|
|
55201
|
-
|
|
55202
|
-
|
|
55203
|
-
|
|
55204
|
-
|
|
55205
|
-
|
|
55206
|
-
|
|
55207
|
-
|
|
55208
|
-
|
|
55209
|
-
|
|
55210
|
-
|
|
55211
|
-
|
|
55212
|
-
|
|
55213
|
-
|
|
55582
|
+
function HeaterHeroCard(props) {
|
|
55583
|
+
const t = useUiText();
|
|
55584
|
+
const { cap, display, available, applyOptimistic, running, power, timer } = useHeater(props);
|
|
55585
|
+
if (cap === void 0 || display === void 0) return null;
|
|
55586
|
+
const on = display.on === true;
|
|
55587
|
+
const heating = running?.running ?? on;
|
|
55588
|
+
const watts = power?.power ?? null;
|
|
55589
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55590
|
+
className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
|
|
55591
|
+
children: [
|
|
55592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES$1 }),
|
|
55593
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55594
|
+
className: "relative flex h-32 items-center justify-center overflow-hidden rounded-lg border border-border-subtle bg-background-elevated",
|
|
55595
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55596
|
+
className: cn("pointer-events-none absolute inset-0", heating ? "opacity-60" : "opacity-20"),
|
|
55597
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
|
|
55598
|
+
mode: heating ? "heat" : "off",
|
|
55599
|
+
accent: HEAT_ACCENT,
|
|
55600
|
+
periodSec: HEAT_PERIOD_SEC,
|
|
55601
|
+
swaying: false,
|
|
55602
|
+
forceRising: true
|
|
55603
|
+
})
|
|
55604
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55605
|
+
className: "relative z-10 flex flex-col items-center gap-1",
|
|
55606
|
+
children: [
|
|
55607
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Flame, {
|
|
55608
|
+
className: cn("h-8 w-8", heating ? "text-warning" : "text-foreground-subtle"),
|
|
55609
|
+
"aria-hidden": true
|
|
55610
|
+
}),
|
|
55611
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55612
|
+
className: "text-sm font-medium text-foreground",
|
|
55613
|
+
children: heating ? t("uiLibrary.heater.heating", "Heating") : t("uiLibrary.heater.off", "Off")
|
|
55614
|
+
}),
|
|
55615
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55616
|
+
className: "text-xs tabular-nums text-foreground-muted",
|
|
55617
|
+
"data-testid": "heater-watts",
|
|
55618
|
+
children: watts === null ? "—" : `${Math.round(watts)} W`
|
|
55619
|
+
})
|
|
55620
|
+
]
|
|
55621
|
+
})]
|
|
55622
|
+
}),
|
|
55623
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
55624
|
+
className: "flex items-center justify-between gap-3 text-sm text-foreground",
|
|
55625
|
+
children: [t("uiLibrary.heater.power", "Heater"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
55626
|
+
checked: on,
|
|
55627
|
+
disabled: !available,
|
|
55628
|
+
onCheckedChange: (next) => {
|
|
55629
|
+
applyOptimistic({ on: next });
|
|
55630
|
+
cap.setState({ on: next });
|
|
55631
|
+
}
|
|
55632
|
+
})]
|
|
55633
|
+
}),
|
|
55634
|
+
timer.cap !== void 0 && HEATER_LIMIT !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimedRunControl, {
|
|
55635
|
+
trpc: props.trpc,
|
|
55636
|
+
deviceId: props.deviceId,
|
|
55637
|
+
limit: HEATER_LIMIT
|
|
55638
|
+
})
|
|
55639
|
+
]
|
|
55640
|
+
});
|
|
55214
55641
|
}
|
|
55215
|
-
|
|
55216
|
-
|
|
55217
|
-
|
|
55218
|
-
|
|
55219
|
-
|
|
55220
|
-
|
|
55221
|
-
|
|
55222
|
-
|
|
55223
|
-
|
|
55224
|
-
|
|
55642
|
+
//#endregion
|
|
55643
|
+
//#region src/composites/device-controls/dishwasher-control.tsx
|
|
55644
|
+
/**
|
|
55645
|
+
* Dishwasher controls for a `DeviceType.Dishwasher` device.
|
|
55646
|
+
*
|
|
55647
|
+
* Reads the `dishwasher` cap status slice and renders inline + hero surfaces
|
|
55648
|
+
* mirroring vacuum-control / pet-feeder patterns.
|
|
55649
|
+
*
|
|
55650
|
+
* Salt and rinse aid are not here: they are items of the device's `consumables`
|
|
55651
|
+
* cap, shown by the page's Consumables section like every device's (D915).
|
|
55652
|
+
*
|
|
55653
|
+
* UNKNOWN IS NOT OFF. The slice carries `null` for everything the appliance has not
|
|
55654
|
+
* reported, and this view renders that as "—" / a disabled control, never as an unchecked
|
|
55655
|
+
* box or a zero. It also does not echo a command before the appliance confirms it: the
|
|
55656
|
+
* slice moves when the appliance says so.
|
|
55657
|
+
*/
|
|
55658
|
+
/** What the operation state is called to a person, and how it is toned. */
|
|
55659
|
+
function dishwasherOperationMeta(operationState, t = defaultUiText) {
|
|
55660
|
+
switch (operationState) {
|
|
55661
|
+
case "Run": return {
|
|
55662
|
+
label: t("uiLibrary.dishwasher.op.run", "Running"),
|
|
55663
|
+
tone: "text-primary"
|
|
55664
|
+
};
|
|
55665
|
+
case "Pause": return {
|
|
55666
|
+
label: t("uiLibrary.dishwasher.op.pause", "Paused"),
|
|
55667
|
+
tone: "text-warning"
|
|
55668
|
+
};
|
|
55669
|
+
case "Finished": return {
|
|
55670
|
+
label: t("uiLibrary.dishwasher.op.finished", "Finished"),
|
|
55671
|
+
tone: "text-success"
|
|
55672
|
+
};
|
|
55673
|
+
case "Error": return {
|
|
55674
|
+
label: t("uiLibrary.dishwasher.op.error", "Error"),
|
|
55675
|
+
tone: "text-danger"
|
|
55676
|
+
};
|
|
55677
|
+
case "Aborting": return {
|
|
55678
|
+
label: t("uiLibrary.dishwasher.op.aborting", "Stopping"),
|
|
55679
|
+
tone: "text-warning"
|
|
55680
|
+
};
|
|
55681
|
+
case "ActionRequired": return {
|
|
55682
|
+
label: t("uiLibrary.dishwasher.op.actionRequired", "Action required"),
|
|
55683
|
+
tone: "text-warning"
|
|
55684
|
+
};
|
|
55685
|
+
case "DelayedStart": return {
|
|
55686
|
+
label: t("uiLibrary.dishwasher.op.delayedStart", "Delayed start"),
|
|
55687
|
+
tone: "text-primary"
|
|
55688
|
+
};
|
|
55689
|
+
case "Ready": return {
|
|
55690
|
+
label: t("uiLibrary.dishwasher.op.ready", "Ready"),
|
|
55691
|
+
tone: "text-foreground"
|
|
55692
|
+
};
|
|
55693
|
+
case "Inactive": return {
|
|
55694
|
+
label: t("uiLibrary.dishwasher.op.inactive", "Inactive"),
|
|
55695
|
+
tone: "text-foreground-subtle"
|
|
55696
|
+
};
|
|
55697
|
+
case null: return {
|
|
55698
|
+
label: t("uiLibrary.dishwasher.unknown", "Unknown"),
|
|
55699
|
+
tone: "text-foreground-subtle"
|
|
55700
|
+
};
|
|
55701
|
+
}
|
|
55702
|
+
}
|
|
55703
|
+
/** The headline for the whole slice: the appliance's state, or why there is none. */
|
|
55704
|
+
function dishwasherHeadline(status, t = defaultUiText) {
|
|
55705
|
+
switch (status.availability) {
|
|
55706
|
+
case "offline": return {
|
|
55707
|
+
label: t("uiLibrary.dishwasher.offline", "Offline"),
|
|
55708
|
+
tone: "text-foreground-subtle"
|
|
55709
|
+
};
|
|
55710
|
+
case "description-missing": return {
|
|
55711
|
+
label: t("uiLibrary.dishwasher.descriptionMissing", "Waiting for the appliance profile"),
|
|
55712
|
+
tone: "text-warning"
|
|
55713
|
+
};
|
|
55714
|
+
case "available": return dishwasherOperationMeta(status.operationState, t);
|
|
55225
55715
|
}
|
|
55226
55716
|
}
|
|
55717
|
+
/** Last segment of a feature key, spaced out: `Dishcare.Dishwasher.Program.Eco50` -> `Eco50`. */
|
|
55718
|
+
function featureLabel(key) {
|
|
55719
|
+
return (key.split(".").pop() ?? key).replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
55720
|
+
}
|
|
55227
55721
|
/**
|
|
55228
|
-
*
|
|
55229
|
-
*
|
|
55722
|
+
* Salt and rinse aid are consumable items (D915). Their events must not
|
|
55723
|
+
* reappear on the hero — the Consumables section is the only surface.
|
|
55230
55724
|
*/
|
|
55231
|
-
function
|
|
55232
|
-
|
|
55233
|
-
|
|
55234
|
-
case "medium_high": return 1.3;
|
|
55235
|
-
case "medium": return 1.6;
|
|
55236
|
-
case "medium_low": return 1.9;
|
|
55237
|
-
case "low": return 2.2;
|
|
55238
|
-
default: return 1.8;
|
|
55239
|
-
}
|
|
55725
|
+
function isConsumableDishwasherEvent(event) {
|
|
55726
|
+
const short = (event.split(".").pop() ?? event).toLowerCase();
|
|
55727
|
+
return short.includes("salt") || short.includes("rinse");
|
|
55240
55728
|
}
|
|
55241
|
-
|
|
55242
|
-
|
|
55243
|
-
|
|
55244
|
-
@keyframes acv-sway { 0%,100%{transform:translateX(-9%)} 50%{transform:translateX(9%)} }
|
|
55245
|
-
@keyframes acv-swing-h { 0%,100%{transform:translateX(-22%)} 50%{transform:translateX(22%)} }
|
|
55246
|
-
@keyframes acv-swing-v { 0%,100%{transform:translateY(-22%)} 50%{transform:translateY(22%)} }
|
|
55247
|
-
@keyframes acv-spin { to { transform:rotate(360deg) } }
|
|
55248
|
-
@keyframes acv-glow { 0%,100%{opacity:.35;transform:scale(1)} 50%{opacity:.6;transform:scale(1.06)} }
|
|
55249
|
-
@media (prefers-reduced-motion: reduce) {
|
|
55250
|
-
.acv-streak, .acv-fan, .acv-sway-wrap, .acv-glow, .acv-swing { animation: none !important; }
|
|
55729
|
+
/** Raised events the hero may name. Consumable events are dropped. */
|
|
55730
|
+
function visibleDishwasherEvents(events) {
|
|
55731
|
+
return events.filter((event) => !isConsumableDishwasherEvent(event));
|
|
55251
55732
|
}
|
|
55252
|
-
|
|
55253
|
-
function
|
|
55254
|
-
|
|
55255
|
-
const
|
|
55733
|
+
/** Whole seconds as a short duration: `1 h 24 min`, `42 min`, `30 s`. */
|
|
55734
|
+
function formatDuration(seconds, t = defaultUiText) {
|
|
55735
|
+
if (seconds < 60) return t("uiLibrary.dishwasher.durationSeconds", "{{n}} s", { n: seconds });
|
|
55736
|
+
const minutes = Math.round(seconds / 60);
|
|
55737
|
+
if (minutes < 60) return t("uiLibrary.dishwasher.durationMinutes", "{{n}} min", { n: minutes });
|
|
55738
|
+
const hours = Math.floor(minutes / 60);
|
|
55739
|
+
const rest = minutes % 60;
|
|
55740
|
+
return rest === 0 ? t("uiLibrary.dishwasher.durationHours", "{{n}} h", { n: hours }) : t("uiLibrary.dishwasher.durationHoursMinutes", "{{h}} h {{m}} min", {
|
|
55741
|
+
h: hours,
|
|
55742
|
+
m: rest
|
|
55743
|
+
});
|
|
55744
|
+
}
|
|
55745
|
+
function useDishwasher(trpc, deviceId) {
|
|
55746
|
+
return useDeviceCapability({
|
|
55256
55747
|
trpc,
|
|
55257
55748
|
deviceId,
|
|
55258
|
-
cap: (d) => d.
|
|
55259
|
-
slice: (d) => d.state.
|
|
55749
|
+
cap: (d) => d.dishwasher,
|
|
55750
|
+
slice: (d) => d.state.dishwasher
|
|
55260
55751
|
});
|
|
55261
|
-
|
|
55752
|
+
}
|
|
55753
|
+
function DishwasherIconButton({ label, busy, active, onClick, children }) {
|
|
55754
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
55755
|
+
type: "button",
|
|
55756
|
+
"aria-label": label,
|
|
55757
|
+
title: label,
|
|
55758
|
+
disabled: busy,
|
|
55759
|
+
onClick: (e) => {
|
|
55760
|
+
e.stopPropagation();
|
|
55761
|
+
onClick();
|
|
55762
|
+
},
|
|
55763
|
+
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"),
|
|
55764
|
+
children
|
|
55765
|
+
});
|
|
55766
|
+
}
|
|
55767
|
+
function DishwasherGraphic({ running, progressPercent }) {
|
|
55768
|
+
const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
|
|
55769
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55770
|
+
"aria-hidden": "true",
|
|
55771
|
+
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",
|
|
55772
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55773
|
+
className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
|
|
55774
|
+
style: { height: `${pct}%` }
|
|
55775
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55776
|
+
className: "absolute inset-0 flex items-center justify-center text-foreground-subtle",
|
|
55777
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waves, { className: "h-10 w-10 opacity-70" })
|
|
55778
|
+
})]
|
|
55779
|
+
});
|
|
55780
|
+
}
|
|
55781
|
+
function DishwasherPanel({ trpc, deviceId, layout }) {
|
|
55782
|
+
const t = useUiText();
|
|
55783
|
+
const { cap, slice, available } = useDishwasher(trpc, deviceId);
|
|
55262
55784
|
if (cap === void 0) return null;
|
|
55263
|
-
if (
|
|
55264
|
-
const
|
|
55785
|
+
if (slice === void 0) return null;
|
|
55786
|
+
const display = slice;
|
|
55787
|
+
const link = display.availability === "available";
|
|
55788
|
+
const busy = !available || !link;
|
|
55789
|
+
const optionTogglesLocked = display.childLock === true;
|
|
55790
|
+
const meta = dishwasherHeadline(display, t);
|
|
55791
|
+
const running = display.operationState === "Run";
|
|
55792
|
+
const paused = display.operationState === "Pause";
|
|
55793
|
+
const startable = display.remoteStartAllowed === true && display.selectedProgram !== null;
|
|
55794
|
+
const press = (fn) => {
|
|
55795
|
+
if (busy) return;
|
|
55796
|
+
fn();
|
|
55797
|
+
};
|
|
55265
55798
|
if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55266
|
-
"
|
|
55267
|
-
className: "inline-flex items-center gap-1",
|
|
55799
|
+
className: "inline-flex items-center gap-1.5 max-w-full",
|
|
55268
55800
|
children: [
|
|
55801
|
+
display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55802
|
+
className: "inline-flex text-warning shrink-0",
|
|
55803
|
+
title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
|
|
55804
|
+
"aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
|
|
55805
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
|
|
55806
|
+
}),
|
|
55269
55807
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55270
|
-
className: cn("text-xs tabular-nums
|
|
55271
|
-
children:
|
|
55272
|
-
display.target,
|
|
55273
|
-
"°",
|
|
55274
|
-
display.currentTemp !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55275
|
-
className: "text-foreground-subtle font-normal",
|
|
55276
|
-
children: [
|
|
55277
|
-
" ",
|
|
55278
|
-
"(",
|
|
55279
|
-
display.currentTemp,
|
|
55280
|
-
"°)"
|
|
55281
|
-
]
|
|
55282
|
-
})
|
|
55283
|
-
] }) : display.currentTemp !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55284
|
-
className: "text-foreground-subtle",
|
|
55285
|
-
children: [display.currentTemp, "°"]
|
|
55286
|
-
}) : null
|
|
55808
|
+
className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
|
|
55809
|
+
children: meta.label
|
|
55287
55810
|
}),
|
|
55288
|
-
display.
|
|
55289
|
-
|
|
55290
|
-
|
|
55291
|
-
|
|
55292
|
-
|
|
55293
|
-
|
|
55294
|
-
|
|
55295
|
-
|
|
55296
|
-
|
|
55297
|
-
|
|
55298
|
-
|
|
55299
|
-
|
|
55300
|
-
|
|
55301
|
-
|
|
55302
|
-
|
|
55303
|
-
|
|
55304
|
-
|
|
55305
|
-
|
|
55306
|
-
|
|
55307
|
-
|
|
55308
|
-
|
|
55309
|
-
|
|
55310
|
-
applyOptimistic({ target: next });
|
|
55311
|
-
cap.setTarget({ target: next });
|
|
55312
|
-
}
|
|
55313
|
-
},
|
|
55314
|
-
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"),
|
|
55315
|
-
children: "+"
|
|
55316
|
-
})] }),
|
|
55317
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
55318
|
-
"aria-label": t("uiLibrary.acPanel.mode", "Mode"),
|
|
55319
|
-
value: display.mode,
|
|
55320
|
-
disabled: busy,
|
|
55321
|
-
onChange: (e) => {
|
|
55322
|
-
e.stopPropagation();
|
|
55323
|
-
const mode = display.availableModes.find((m) => m === e.target.value);
|
|
55324
|
-
if (!busy && mode) {
|
|
55325
|
-
applyOptimistic({ mode });
|
|
55326
|
-
cap.setMode({ mode });
|
|
55327
|
-
}
|
|
55328
|
-
},
|
|
55329
|
-
className: cn("bg-transparent px-0.5 text-xs text-foreground-subtle hover:text-foreground cursor-pointer", busy && "opacity-40 cursor-not-allowed"),
|
|
55330
|
-
children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
55331
|
-
value: m,
|
|
55332
|
-
children: modeLabel(t, m)
|
|
55333
|
-
}, m))
|
|
55811
|
+
display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55812
|
+
className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
|
|
55813
|
+
title: display.program,
|
|
55814
|
+
children: featureLabel(display.program)
|
|
55815
|
+
}),
|
|
55816
|
+
display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55817
|
+
className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
|
|
55818
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remaining, t)]
|
|
55819
|
+
}),
|
|
55820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
55821
|
+
label: t("uiLibrary.dishwasher.start", "Start"),
|
|
55822
|
+
busy: busy || !startable,
|
|
55823
|
+
active: running,
|
|
55824
|
+
onClick: () => press(() => cap.start({})),
|
|
55825
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
|
|
55826
|
+
}),
|
|
55827
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
55828
|
+
label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
|
|
55829
|
+
busy: busy || !(running || paused),
|
|
55830
|
+
active: paused,
|
|
55831
|
+
onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
|
|
55832
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
|
|
55334
55833
|
})
|
|
55335
55834
|
]
|
|
55336
55835
|
});
|
|
55337
|
-
const
|
|
55338
|
-
const on = display.mode !== "off";
|
|
55339
|
-
const heatingOnly = !climateOffersCooling(display.availableModes);
|
|
55340
|
-
const setpointBounds = climateSetpointBounds(display);
|
|
55341
|
-
const CurrentPresetIcon = presetIcon(display.preset);
|
|
55342
|
-
const boost = display.preset === "turbo";
|
|
55343
|
-
const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
|
|
55344
|
-
const showVSwing = display.swingVertical !== null;
|
|
55345
|
-
const showHSwing = display.swingHorizontal !== null;
|
|
55346
|
-
const swaying = on && (display.swingVertical === true || display.swingHorizontal === true);
|
|
55836
|
+
const programs = display.availablePrograms;
|
|
55347
55837
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55348
|
-
"
|
|
55349
|
-
className: cn("relative flex flex-col items-stretch gap-2.5 overflow-hidden rounded-lg border p-3 transition-colors", MODE_COLOR[display.mode]),
|
|
55838
|
+
className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
|
|
55350
55839
|
children: [
|
|
55351
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES }),
|
|
55352
55840
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55353
|
-
className: "
|
|
55354
|
-
children: [
|
|
55355
|
-
|
|
55356
|
-
|
|
55357
|
-
|
|
55358
|
-
|
|
55359
|
-
|
|
55360
|
-
|
|
55361
|
-
|
|
55362
|
-
|
|
55363
|
-
|
|
55364
|
-
|
|
55365
|
-
|
|
55366
|
-
|
|
55367
|
-
|
|
55368
|
-
|
|
55369
|
-
|
|
55370
|
-
|
|
55371
|
-
|
|
55372
|
-
|
|
55373
|
-
|
|
55374
|
-
|
|
55375
|
-
|
|
55376
|
-
|
|
55377
|
-
|
|
55378
|
-
|
|
55379
|
-
|
|
55380
|
-
|
|
55381
|
-
|
|
55382
|
-
|
|
55383
|
-
|
|
55384
|
-
|
|
55385
|
-
|
|
55386
|
-
|
|
55387
|
-
|
|
55388
|
-
|
|
55389
|
-
|
|
55390
|
-
|
|
55391
|
-
|
|
55392
|
-
|
|
55393
|
-
|
|
55394
|
-
|
|
55395
|
-
|
|
55396
|
-
|
|
55397
|
-
|
|
55398
|
-
|
|
55399
|
-
|
|
55400
|
-
|
|
55401
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
55402
|
-
|
|
55403
|
-
|
|
55404
|
-
|
|
55405
|
-
|
|
55406
|
-
|
|
55407
|
-
|
|
55408
|
-
|
|
55409
|
-
|
|
55410
|
-
|
|
55411
|
-
|
|
55412
|
-
|
|
55413
|
-
|
|
55414
|
-
|
|
55415
|
-
|
|
55416
|
-
|
|
55417
|
-
|
|
55418
|
-
|
|
55419
|
-
className: "
|
|
55420
|
-
|
|
55421
|
-
|
|
55422
|
-
|
|
55423
|
-
|
|
55424
|
-
|
|
55425
|
-
|
|
55426
|
-
|
|
55427
|
-
|
|
55428
|
-
|
|
55429
|
-
|
|
55430
|
-
|
|
55431
|
-
|
|
55432
|
-
|
|
55433
|
-
|
|
55434
|
-
|
|
55435
|
-
|
|
55436
|
-
|
|
55437
|
-
|
|
55438
|
-
|
|
55439
|
-
|
|
55440
|
-
|
|
55441
|
-
|
|
55442
|
-
|
|
55443
|
-
|
|
55444
|
-
|
|
55445
|
-
|
|
55446
|
-
applyOptimistic({ target: next });
|
|
55447
|
-
},
|
|
55448
|
-
onCommit: (next) => {
|
|
55449
|
-
applyOptimistic({ target: next });
|
|
55450
|
-
cap.setTarget({ target: next });
|
|
55451
|
-
}
|
|
55841
|
+
className: "flex items-start gap-4",
|
|
55842
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
|
|
55843
|
+
running,
|
|
55844
|
+
progressPercent: display.progress
|
|
55845
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55846
|
+
className: "flex min-w-0 flex-1 flex-col gap-2",
|
|
55847
|
+
children: [
|
|
55848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55849
|
+
className: "flex flex-wrap items-center gap-2",
|
|
55850
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55851
|
+
className: cn("text-3xl font-semibold", meta.tone),
|
|
55852
|
+
children: meta.label
|
|
55853
|
+
}), display.powerOn === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55854
|
+
className: "text-xs font-medium text-foreground-subtle",
|
|
55855
|
+
children: t("uiLibrary.dishwasher.powerOff", "Power off")
|
|
55856
|
+
})]
|
|
55857
|
+
}),
|
|
55858
|
+
display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55859
|
+
className: "text-sm text-foreground-subtle",
|
|
55860
|
+
title: display.program,
|
|
55861
|
+
children: featureLabel(display.program)
|
|
55862
|
+
}),
|
|
55863
|
+
display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55864
|
+
className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
|
|
55865
|
+
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) })]
|
|
55866
|
+
}),
|
|
55867
|
+
display.delayedStart != null && display.delayedStart > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55868
|
+
className: "text-xs text-foreground-subtle tabular-nums",
|
|
55869
|
+
children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStart, t) })
|
|
55870
|
+
}),
|
|
55871
|
+
display.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55872
|
+
className: "flex items-center gap-2",
|
|
55873
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55874
|
+
className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
|
|
55875
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55876
|
+
className: "h-full bg-primary transition-[width] duration-500",
|
|
55877
|
+
style: { width: `${Math.max(0, Math.min(100, display.progress))}%` }
|
|
55878
|
+
})
|
|
55879
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55880
|
+
className: "tabular-nums text-xs text-foreground-subtle",
|
|
55881
|
+
children: [Math.round(display.progress), "%"]
|
|
55882
|
+
})]
|
|
55883
|
+
}),
|
|
55884
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55885
|
+
className: "flex flex-wrap items-center gap-x-3 gap-y-1.5",
|
|
55886
|
+
children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
55887
|
+
className: "inline-flex items-center gap-1 text-xs text-warning",
|
|
55888
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
|
|
55889
|
+
}), visibleDishwasherEvents(display.activeEvents).map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55890
|
+
className: "text-xs text-warning",
|
|
55891
|
+
title: event,
|
|
55892
|
+
children: featureLabel(event)
|
|
55893
|
+
}, event))]
|
|
55894
|
+
}),
|
|
55895
|
+
display.operationState === "Error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55896
|
+
className: "inline-flex items-center gap-1 text-xs text-danger",
|
|
55897
|
+
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") })]
|
|
55898
|
+
}),
|
|
55899
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55900
|
+
className: "flex flex-wrap items-center gap-2",
|
|
55901
|
+
children: [
|
|
55902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
55903
|
+
label: t("uiLibrary.dishwasher.start", "Start"),
|
|
55904
|
+
busy: busy || !startable,
|
|
55905
|
+
active: running,
|
|
55906
|
+
onClick: () => press(() => cap.start({})),
|
|
55907
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
|
|
55908
|
+
}),
|
|
55909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
55910
|
+
label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
|
|
55911
|
+
busy: busy || !(running || paused),
|
|
55912
|
+
active: paused,
|
|
55913
|
+
onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
|
|
55914
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
|
|
55915
|
+
}),
|
|
55916
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
55917
|
+
label: t("uiLibrary.dishwasher.stop", "Stop"),
|
|
55918
|
+
busy: busy || !(running || paused),
|
|
55919
|
+
active: false,
|
|
55920
|
+
onClick: () => press(() => cap.stop({})),
|
|
55921
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
|
|
55922
|
+
}),
|
|
55923
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
|
|
55924
|
+
label: t("uiLibrary.dishwasher.power", "Power"),
|
|
55925
|
+
busy: busy || display.powerOn === null,
|
|
55926
|
+
active: display.powerOn === true,
|
|
55927
|
+
onClick: () => {
|
|
55928
|
+
const next = display.powerOn !== true;
|
|
55929
|
+
press(() => cap.setPower({ on: next }));
|
|
55930
|
+
},
|
|
55931
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
|
|
55932
|
+
})
|
|
55933
|
+
]
|
|
55452
55934
|
})
|
|
55453
|
-
|
|
55454
|
-
|
|
55455
|
-
|
|
55456
|
-
|
|
55457
|
-
|
|
55458
|
-
|
|
55459
|
-
|
|
55460
|
-
|
|
55461
|
-
|
|
55462
|
-
|
|
55463
|
-
|
|
55935
|
+
]
|
|
55936
|
+
})]
|
|
55937
|
+
}),
|
|
55938
|
+
programs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
55939
|
+
className: "flex items-center gap-2 text-xs text-foreground-subtle",
|
|
55940
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55941
|
+
className: "w-16 shrink-0",
|
|
55942
|
+
children: t("uiLibrary.dishwasher.program", "Program")
|
|
55943
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
55944
|
+
"aria-label": t("uiLibrary.dishwasher.program", "Program"),
|
|
55945
|
+
value: display.selectedProgram ?? "",
|
|
55946
|
+
disabled: busy,
|
|
55947
|
+
onChange: (e) => {
|
|
55948
|
+
const program = e.target.value;
|
|
55949
|
+
if (!program) return;
|
|
55950
|
+
cap.setProgram({ program });
|
|
55951
|
+
},
|
|
55952
|
+
onClick: (e) => {
|
|
55953
|
+
e.stopPropagation();
|
|
55954
|
+
},
|
|
55955
|
+
className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
|
|
55956
|
+
children: [display.selectedProgram == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
55957
|
+
value: "",
|
|
55958
|
+
children: "—"
|
|
55959
|
+
}), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
55960
|
+
value: p,
|
|
55961
|
+
children: featureLabel(p)
|
|
55962
|
+
}, p))]
|
|
55963
|
+
})]
|
|
55464
55964
|
}),
|
|
55465
55965
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55466
|
-
className: "
|
|
55966
|
+
className: "flex flex-wrap gap-4 text-xs",
|
|
55467
55967
|
children: [
|
|
55468
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
55469
|
-
label: t("uiLibrary.
|
|
55470
|
-
|
|
55471
|
-
|
|
55472
|
-
|
|
55473
|
-
|
|
55474
|
-
|
|
55475
|
-
|
|
55476
|
-
|
|
55477
|
-
|
|
55478
|
-
|
|
55479
|
-
|
|
55480
|
-
|
|
55481
|
-
|
|
55482
|
-
|
|
55483
|
-
|
|
55484
|
-
|
|
55485
|
-
|
|
55486
|
-
|
|
55487
|
-
|
|
55488
|
-
|
|
55489
|
-
|
|
55490
|
-
|
|
55491
|
-
|
|
55968
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
|
|
55969
|
+
label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
|
|
55970
|
+
checked: display.halfLoad,
|
|
55971
|
+
shown: link,
|
|
55972
|
+
busy: busy || optionTogglesLocked,
|
|
55973
|
+
onChange: (on) => void cap.setHalfLoad({ on })
|
|
55974
|
+
}),
|
|
55975
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
|
|
55976
|
+
label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
|
|
55977
|
+
checked: display.hygienePlus,
|
|
55978
|
+
shown: link,
|
|
55979
|
+
busy: busy || optionTogglesLocked,
|
|
55980
|
+
onChange: (on) => void cap.setHygienePlus({ on })
|
|
55981
|
+
}),
|
|
55982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
|
|
55983
|
+
label: t("uiLibrary.dishwasher.extraDry", "Extra dry"),
|
|
55984
|
+
checked: display.extraDry,
|
|
55985
|
+
shown: link,
|
|
55986
|
+
busy: busy || optionTogglesLocked,
|
|
55987
|
+
onChange: (on) => void cap.setExtraDry({ on })
|
|
55988
|
+
}),
|
|
55989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
|
|
55990
|
+
label: t("uiLibrary.dishwasher.intensiveZone", "Intensive zone"),
|
|
55991
|
+
checked: display.intensiveZone,
|
|
55992
|
+
shown: link,
|
|
55993
|
+
busy: busy || optionTogglesLocked,
|
|
55994
|
+
onChange: (on) => void cap.setIntensiveZone({ on })
|
|
55492
55995
|
}),
|
|
55493
|
-
|
|
55494
|
-
label: t("uiLibrary.
|
|
55495
|
-
|
|
55496
|
-
|
|
55497
|
-
|
|
55498
|
-
|
|
55499
|
-
label: fanLabel(f)
|
|
55500
|
-
})),
|
|
55501
|
-
disabled: busy,
|
|
55502
|
-
onChange: (v) => {
|
|
55503
|
-
applyOptimistic({ fanMode: v });
|
|
55504
|
-
cap.setFanMode({ fanMode: v });
|
|
55505
|
-
}
|
|
55996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
|
|
55997
|
+
label: t("uiLibrary.dishwasher.varioSpeedPlus", "VarioSpeed+"),
|
|
55998
|
+
checked: display.varioSpeedPlus,
|
|
55999
|
+
shown: link,
|
|
56000
|
+
busy: busy || optionTogglesLocked,
|
|
56001
|
+
onChange: (on) => void cap.setVarioSpeedPlus({ on })
|
|
55506
56002
|
}),
|
|
55507
|
-
|
|
55508
|
-
label: t("uiLibrary.
|
|
55509
|
-
|
|
55510
|
-
|
|
55511
|
-
|
|
55512
|
-
|
|
55513
|
-
label: fanLabel(p)
|
|
55514
|
-
})),
|
|
55515
|
-
disabled: busy,
|
|
55516
|
-
onChange: (v) => {
|
|
55517
|
-
applyOptimistic({ preset: v });
|
|
55518
|
-
cap.setPreset({ preset: v });
|
|
55519
|
-
}
|
|
56003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
|
|
56004
|
+
label: t("uiLibrary.dishwasher.childLock", "Child lock"),
|
|
56005
|
+
checked: display.childLock,
|
|
56006
|
+
shown: link,
|
|
56007
|
+
busy,
|
|
56008
|
+
onChange: (on) => void cap.setChildLock({ on })
|
|
55520
56009
|
})
|
|
55521
56010
|
]
|
|
55522
|
-
}),
|
|
55523
|
-
(showVSwing || showHSwing) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
55524
|
-
label: t("uiLibrary.acPanel.swing", "Swing"),
|
|
55525
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55526
|
-
className: "grid grid-cols-2 gap-1",
|
|
55527
|
-
children: [showVSwing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwingToggle, {
|
|
55528
|
-
axis: "vertical",
|
|
55529
|
-
on: display.swingVertical ?? false,
|
|
55530
|
-
busy,
|
|
55531
|
-
onToggle: (next) => {
|
|
55532
|
-
applyOptimistic({ swingVertical: next });
|
|
55533
|
-
cap.setSwingVertical({ on: next });
|
|
55534
|
-
}
|
|
55535
|
-
}), showHSwing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwingToggle, {
|
|
55536
|
-
axis: "horizontal",
|
|
55537
|
-
on: display.swingHorizontal ?? false,
|
|
55538
|
-
busy,
|
|
55539
|
-
onToggle: (next) => {
|
|
55540
|
-
applyOptimistic({ swingHorizontal: next });
|
|
55541
|
-
cap.setSwingHorizontal({ on: next });
|
|
55542
|
-
}
|
|
55543
|
-
})]
|
|
55544
|
-
})
|
|
55545
56011
|
})
|
|
55546
56012
|
]
|
|
55547
56013
|
});
|
|
55548
56014
|
}
|
|
55549
|
-
/**
|
|
55550
|
-
|
|
55551
|
-
|
|
55552
|
-
|
|
55553
|
-
|
|
55554
|
-
|
|
55555
|
-
|
|
55556
|
-
|
|
56015
|
+
/**
|
|
56016
|
+
* One boolean setting. `checked === null` is "the appliance has not said": on a reachable
|
|
56017
|
+
* appliance that means it does not offer the setting (hidden); on an unreachable one the
|
|
56018
|
+
* setting is shown but disabled and marked unknown — never as an unchecked box.
|
|
56019
|
+
*/
|
|
56020
|
+
function ToggleRow({ label, checked, shown, busy, onChange }) {
|
|
56021
|
+
const t = useUiText();
|
|
56022
|
+
if (checked === null && shown) return null;
|
|
56023
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
56024
|
+
className: "inline-flex items-center gap-2 cursor-pointer",
|
|
56025
|
+
children: [
|
|
56026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
56027
|
+
type: "checkbox",
|
|
56028
|
+
checked: checked === true,
|
|
56029
|
+
"aria-checked": checked === null ? "mixed" : checked,
|
|
56030
|
+
disabled: busy || checked === null,
|
|
56031
|
+
onChange: (e) => {
|
|
56032
|
+
onChange(e.target.checked);
|
|
56033
|
+
},
|
|
56034
|
+
onClick: (e) => {
|
|
56035
|
+
e.stopPropagation();
|
|
56036
|
+
},
|
|
56037
|
+
className: "rounded border-border"
|
|
56038
|
+
}),
|
|
56039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
|
|
56040
|
+
checked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56041
|
+
className: "text-foreground-subtle",
|
|
56042
|
+
children: t("uiLibrary.dishwasher.unknownValue", "—")
|
|
56043
|
+
})
|
|
56044
|
+
]
|
|
55557
56045
|
});
|
|
55558
56046
|
}
|
|
55559
|
-
function
|
|
55560
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
55561
|
-
|
|
55562
|
-
"
|
|
56047
|
+
function DishwasherInlineControl(props) {
|
|
56048
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
|
|
56049
|
+
...props,
|
|
56050
|
+
layout: "compact"
|
|
55563
56051
|
});
|
|
55564
56052
|
}
|
|
55565
|
-
|
|
55566
|
-
|
|
55567
|
-
|
|
55568
|
-
|
|
55569
|
-
className: "flex min-w-0 flex-col gap-1",
|
|
55570
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55571
|
-
className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
|
|
55572
|
-
children: label
|
|
55573
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55574
|
-
className: cn("flex items-center gap-1 overflow-hidden rounded-lg border border-border/60 bg-surface-subtle px-1.5 py-1.5 has-[:focus-visible]:border-primary", disabled && "opacity-40"),
|
|
55575
|
-
children: [
|
|
55576
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55577
|
-
className: "flex shrink-0",
|
|
55578
|
-
children: icon
|
|
55579
|
-
}),
|
|
55580
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
55581
|
-
"aria-label": label,
|
|
55582
|
-
value,
|
|
55583
|
-
disabled,
|
|
55584
|
-
onChange: (e) => {
|
|
55585
|
-
e.stopPropagation();
|
|
55586
|
-
if (!disabled) onChange(e.target.value);
|
|
55587
|
-
},
|
|
55588
|
-
className: "min-w-0 flex-1 cursor-pointer appearance-none truncate bg-transparent text-xs font-medium text-foreground outline-none",
|
|
55589
|
-
children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
55590
|
-
value: o.value,
|
|
55591
|
-
children: o.label
|
|
55592
|
-
}, o.value))
|
|
55593
|
-
}),
|
|
55594
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 shrink-0 text-foreground-subtle" })
|
|
55595
|
-
]
|
|
55596
|
-
})]
|
|
56053
|
+
function DishwasherHeroCard(props) {
|
|
56054
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
|
|
56055
|
+
...props,
|
|
56056
|
+
layout: "full"
|
|
55597
56057
|
});
|
|
55598
56058
|
}
|
|
55599
|
-
|
|
55600
|
-
|
|
55601
|
-
|
|
55602
|
-
|
|
55603
|
-
|
|
55604
|
-
|
|
55605
|
-
|
|
55606
|
-
|
|
55607
|
-
|
|
55608
|
-
|
|
56059
|
+
//#endregion
|
|
56060
|
+
//#region src/composites/device-controls/appliance-control.tsx
|
|
56061
|
+
/**
|
|
56062
|
+
* Appliance card for a `DeviceType.Washer` / `DeviceType.Dryer` device.
|
|
56063
|
+
*
|
|
56064
|
+
* These appliances have no program surface: what is worth showing is whether
|
|
56065
|
+
* the cycle is running (the `running-state` cap), for how long, and what the
|
|
56066
|
+
* appliance draws right now (the `power-meter` cap). Both slices are read
|
|
56067
|
+
* straight from the device; neither is required.
|
|
56068
|
+
* - ApplianceInlineControl (device-list row): the running badge round the
|
|
56069
|
+
* drum icon, plus the live watts when there are any.
|
|
56070
|
+
* - ApplianceHeroCard (device-detail hero): the drum (turning only while a
|
|
56071
|
+
* cycle runs), the state, the phase, the dwell and the power.
|
|
56072
|
+
*
|
|
56073
|
+
* UNKNOWN IS NOT OFF (D393). `running: null` is drawn as "Unknown" and the
|
|
56074
|
+
* drum stays still and unbadged; a missing watt reading is a dash, never
|
|
56075
|
+
* "0 W". A slice that has not landed hides its row instead of faking a value.
|
|
56076
|
+
*/
|
|
56077
|
+
var KEYFRAMES = `
|
|
56078
|
+
@keyframes apl-tumble { to { transform: rotate(360deg) } }
|
|
56079
|
+
.apl-drum-run { animation: apl-tumble 3s linear infinite; }
|
|
56080
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56081
|
+
.apl-drum-run { animation: none !important; }
|
|
55609
56082
|
}
|
|
55610
|
-
|
|
55611
|
-
|
|
55612
|
-
|
|
55613
|
-
|
|
55614
|
-
|
|
55615
|
-
|
|
55616
|
-
|
|
55617
|
-
|
|
55618
|
-
|
|
55619
|
-
|
|
55620
|
-
const rect = el.getBoundingClientRect();
|
|
55621
|
-
if (rect.width <= 0) return value;
|
|
55622
|
-
return climateTargetFromFraction((clientX - rect.left) / rect.width, {
|
|
55623
|
-
min,
|
|
55624
|
-
max
|
|
55625
|
-
}, step);
|
|
55626
|
-
};
|
|
55627
|
-
const onPointerDown = (event) => {
|
|
55628
|
-
if (disabled) return;
|
|
55629
|
-
event.stopPropagation();
|
|
55630
|
-
event.preventDefault();
|
|
55631
|
-
draggingRef.current = true;
|
|
55632
|
-
if (typeof event.currentTarget.setPointerCapture === "function") event.currentTarget.setPointerCapture(event.pointerId);
|
|
55633
|
-
const next = tempAt(event.clientX);
|
|
55634
|
-
setDraft(next);
|
|
55635
|
-
onPreview(next);
|
|
56083
|
+
`;
|
|
56084
|
+
/** Whole watts, or a dash when the appliance has not reported a reading. */
|
|
56085
|
+
function formatWatts(power) {
|
|
56086
|
+
return power === void 0 || !Number.isFinite(power) ? "—" : `${Math.round(power)} W`;
|
|
56087
|
+
}
|
|
56088
|
+
/** The state to a person: running / idle, or why there is none (unknown). */
|
|
56089
|
+
function applianceStateLabel(running, t) {
|
|
56090
|
+
if (running === true) return {
|
|
56091
|
+
label: t("uiLibrary.appliance.running", "Running"),
|
|
56092
|
+
tone: "text-primary"
|
|
55636
56093
|
};
|
|
55637
|
-
|
|
55638
|
-
|
|
55639
|
-
|
|
55640
|
-
const next = tempAt(event.clientX);
|
|
55641
|
-
setDraft(next);
|
|
55642
|
-
onPreview(next);
|
|
56094
|
+
if (running === false) return {
|
|
56095
|
+
label: t("uiLibrary.appliance.idle", "Idle"),
|
|
56096
|
+
tone: "text-foreground"
|
|
55643
56097
|
};
|
|
55644
|
-
|
|
55645
|
-
|
|
55646
|
-
|
|
55647
|
-
draggingRef.current = false;
|
|
55648
|
-
const next = tempAt(event.clientX);
|
|
55649
|
-
setDraft(null);
|
|
55650
|
-
if (event.currentTarget.hasPointerCapture?.(event.pointerId) === true) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
55651
|
-
onCommit(next);
|
|
56098
|
+
return {
|
|
56099
|
+
label: t("uiLibrary.appliance.unknown", "Unknown"),
|
|
56100
|
+
tone: "text-foreground-subtle"
|
|
55652
56101
|
};
|
|
55653
|
-
|
|
55654
|
-
|
|
55655
|
-
|
|
55656
|
-
|
|
55657
|
-
|
|
55658
|
-
|
|
55659
|
-
|
|
55660
|
-
|
|
55661
|
-
|
|
55662
|
-
|
|
55663
|
-
|
|
55664
|
-
event.preventDefault();
|
|
55665
|
-
onCommit(nextClimateTarget(current, -1, {
|
|
55666
|
-
tempStep: step,
|
|
55667
|
-
minTemp: min,
|
|
55668
|
-
maxTemp: max
|
|
55669
|
-
}));
|
|
55670
|
-
} else if (event.key === "Home") {
|
|
55671
|
-
event.preventDefault();
|
|
55672
|
-
onCommit(min);
|
|
55673
|
-
} else if (event.key === "End") {
|
|
55674
|
-
event.preventDefault();
|
|
55675
|
-
onCommit(max);
|
|
55676
|
-
}
|
|
56102
|
+
}
|
|
56103
|
+
function dwellText(running, since, now, t) {
|
|
56104
|
+
if (running !== true) return null;
|
|
56105
|
+
const { duration } = runningStatusParts(running, since, null, now);
|
|
56106
|
+
if (duration === void 0) return null;
|
|
56107
|
+
return "minutes" in duration ? t("uiLibrary.appliance.forMinutes", "for {{count}} min", { count: duration.minutes }) : t("uiLibrary.appliance.forHours", "for {{count}} h", { count: duration.hours });
|
|
56108
|
+
}
|
|
56109
|
+
function useApplianceState(props) {
|
|
56110
|
+
return {
|
|
56111
|
+
running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState),
|
|
56112
|
+
power: useDeviceState(props.trpc, props.deviceId, (s) => s.powerMeter)
|
|
55677
56113
|
};
|
|
55678
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55679
|
-
ref: trackRef,
|
|
55680
|
-
role: "slider",
|
|
55681
|
-
"aria-label": label,
|
|
55682
|
-
"aria-valuemin": min,
|
|
55683
|
-
"aria-valuemax": max,
|
|
55684
|
-
"aria-valuenow": shown,
|
|
55685
|
-
"aria-disabled": disabled,
|
|
55686
|
-
tabIndex: disabled ? -1 : 0,
|
|
55687
|
-
onPointerDown,
|
|
55688
|
-
onPointerMove,
|
|
55689
|
-
onPointerUp: endDrag,
|
|
55690
|
-
onPointerCancel: endDrag,
|
|
55691
|
-
onKeyDown,
|
|
55692
|
-
className: cn("mt-1 w-full touch-none select-none py-2", disabled ? "cursor-not-allowed opacity-40" : "cursor-ew-resize"),
|
|
55693
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55694
|
-
className: "relative h-1.5 w-full rounded-full bg-surface-subtle",
|
|
55695
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55696
|
-
className: "absolute inset-y-0 left-0 rounded-full",
|
|
55697
|
-
style: {
|
|
55698
|
-
width: pct,
|
|
55699
|
-
background: accent
|
|
55700
|
-
}
|
|
55701
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55702
|
-
className: "absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-surface shadow",
|
|
55703
|
-
style: {
|
|
55704
|
-
left: pct,
|
|
55705
|
-
background: accent
|
|
55706
|
-
}
|
|
55707
|
-
})]
|
|
55708
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55709
|
-
className: "mt-0.5 flex justify-between text-[9px] tabular-nums text-foreground-subtle",
|
|
55710
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [min, "°"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [max, "°"] })]
|
|
55711
|
-
})]
|
|
55712
|
-
});
|
|
55713
56114
|
}
|
|
55714
|
-
/**
|
|
55715
|
-
|
|
55716
|
-
* vent; every active mode renders drifting particles that FALL (cool/dry/auto)
|
|
55717
|
-
* or RISE (heat), with a horizontal sweep overlaid when a swing axis is active.
|
|
55718
|
-
*/
|
|
55719
|
-
function AirflowViz({ mode, accent, periodSec, swaying, forceRising = false }) {
|
|
55720
|
-
const columns = [
|
|
55721
|
-
0,
|
|
55722
|
-
1,
|
|
55723
|
-
2,
|
|
55724
|
-
3,
|
|
55725
|
-
4,
|
|
55726
|
-
5
|
|
55727
|
-
];
|
|
55728
|
-
const rising = forceRising || mode === "heat";
|
|
55729
|
-
const isDroplet = mode === "dry";
|
|
56115
|
+
/** The drum: a ring with a few items in it, turning only while a cycle runs. */
|
|
56116
|
+
function Drum({ running }) {
|
|
55730
56117
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55731
|
-
"aria-hidden": true,
|
|
55732
|
-
|
|
55733
|
-
|
|
55734
|
-
|
|
55735
|
-
|
|
55736
|
-
|
|
55737
|
-
|
|
55738
|
-
children:
|
|
55739
|
-
|
|
55740
|
-
|
|
55741
|
-
|
|
55742
|
-
|
|
55743
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55744
|
-
"data-testid": forceRising ? "climate-heat-rise" : void 0,
|
|
55745
|
-
className: cn("acv-sway-wrap absolute inset-0", swaying && "acv-sway"),
|
|
55746
|
-
style: swaying ? { animation: "acv-sway 2.6s ease-in-out infinite" } : void 0,
|
|
55747
|
-
children: columns.map((i) => {
|
|
55748
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
55749
|
-
className: "acv-streak absolute rounded-full",
|
|
55750
|
-
style: {
|
|
55751
|
-
left: `${8 + i * 15}%`,
|
|
55752
|
-
width: isDroplet ? "0.4rem" : "0.3rem",
|
|
55753
|
-
height: isDroplet ? "0.4rem" : "1.9rem",
|
|
55754
|
-
background: accent,
|
|
55755
|
-
boxShadow: `0 0 6px ${accent}`,
|
|
55756
|
-
animationName: rising ? "acv-rise" : "acv-fall",
|
|
55757
|
-
animationTimingFunction: "linear",
|
|
55758
|
-
animationIterationCount: "infinite",
|
|
55759
|
-
animationDuration: `${periodSec}s`,
|
|
55760
|
-
animationDelay: `${i % 3 * (periodSec / 3) + i * .05}s`
|
|
55761
|
-
}
|
|
55762
|
-
}, i);
|
|
55763
|
-
})
|
|
56118
|
+
"aria-hidden": "true",
|
|
56119
|
+
"data-testid": "appliance-drum",
|
|
56120
|
+
"data-running": running === true ? "running" : running === false ? "idle" : "unknown",
|
|
56121
|
+
className: "relative flex h-28 w-28 shrink-0 items-center justify-center rounded-full border-2 border-border bg-surface-hover",
|
|
56122
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56123
|
+
className: cn("relative h-20 w-20 rounded-full border-2", running === true ? "apl-drum-run border-primary" : "border-border"),
|
|
56124
|
+
"data-testid": "appliance-drum-ring",
|
|
56125
|
+
children: [
|
|
56126
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "absolute left-2 top-4 h-3 w-3 rounded-full bg-primary/40" }),
|
|
56127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "absolute bottom-3 right-3 h-4 w-4 rounded-full bg-info/40" }),
|
|
56128
|
+
/* @__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" })
|
|
56129
|
+
]
|
|
55764
56130
|
})
|
|
55765
56131
|
});
|
|
55766
56132
|
}
|
|
55767
|
-
|
|
55768
|
-
|
|
55769
|
-
|
|
55770
|
-
|
|
55771
|
-
|
|
55772
|
-
|
|
56133
|
+
function ApplianceHeroCard(props) {
|
|
56134
|
+
const t = useUiText();
|
|
56135
|
+
const { running, power } = useApplianceState(props);
|
|
56136
|
+
const state = running?.running ?? null;
|
|
56137
|
+
const meta = applianceStateLabel(state, t);
|
|
56138
|
+
const phase = running?.phase?.trim() ?? "";
|
|
56139
|
+
const dwell = running === void 0 ? null : dwellText(state, running.since, Date.now(), t);
|
|
56140
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56141
|
+
className: "flex items-center gap-4 rounded-lg border border-border bg-surface p-4",
|
|
55773
56142
|
children: [
|
|
55774
|
-
0,
|
|
55775
|
-
|
|
55776
|
-
|
|
55777
|
-
|
|
55778
|
-
|
|
55779
|
-
|
|
55780
|
-
|
|
55781
|
-
|
|
56143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: KEYFRAMES }),
|
|
56144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Drum, { running: state }),
|
|
56145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56146
|
+
className: "flex min-w-0 flex-1 flex-col gap-2",
|
|
56147
|
+
children: [
|
|
56148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56149
|
+
className: "flex flex-wrap items-baseline gap-x-2",
|
|
56150
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56151
|
+
"data-testid": "appliance-state",
|
|
56152
|
+
className: cn("text-3xl font-semibold", meta.tone),
|
|
56153
|
+
children: meta.label
|
|
56154
|
+
}), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56155
|
+
className: "text-sm tabular-nums text-foreground-subtle",
|
|
56156
|
+
children: dwell
|
|
56157
|
+
})]
|
|
56158
|
+
}),
|
|
56159
|
+
phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56160
|
+
className: "text-sm text-foreground-subtle",
|
|
56161
|
+
children: phase
|
|
56162
|
+
}),
|
|
56163
|
+
power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
56164
|
+
"data-testid": "appliance-power",
|
|
56165
|
+
className: "inline-flex items-center gap-1 text-sm tabular-nums text-foreground-muted",
|
|
56166
|
+
title: power.power === void 0 ? t("uiLibrary.appliance.powerUnknown", "Power unknown") : void 0,
|
|
56167
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
|
|
56168
|
+
className: "h-3.5 w-3.5",
|
|
56169
|
+
"aria-hidden": true
|
|
56170
|
+
}), formatWatts(power.power)]
|
|
56171
|
+
})
|
|
56172
|
+
]
|
|
56173
|
+
})
|
|
56174
|
+
]
|
|
55782
56175
|
});
|
|
55783
56176
|
}
|
|
55784
|
-
|
|
55785
|
-
|
|
55786
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
55787
|
-
|
|
55788
|
-
|
|
55789
|
-
|
|
55790
|
-
|
|
55791
|
-
|
|
55792
|
-
|
|
55793
|
-
|
|
55794
|
-
},
|
|
55795
|
-
|
|
55796
|
-
|
|
55797
|
-
0,
|
|
55798
|
-
120,
|
|
55799
|
-
240
|
|
55800
|
-
].map((deg) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
55801
|
-
d: "M50 50 C 58 30, 78 26, 84 12 C 66 20, 54 34, 50 50 Z",
|
|
55802
|
-
fill: color,
|
|
55803
|
-
opacity: .85,
|
|
55804
|
-
transform: `rotate(${deg} 50 50)`
|
|
55805
|
-
}, deg)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
55806
|
-
cx: "50",
|
|
55807
|
-
cy: "50",
|
|
55808
|
-
r: "7",
|
|
55809
|
-
fill: color
|
|
56177
|
+
function ApplianceInlineControl(props) {
|
|
56178
|
+
const { running, power } = useApplianceState(props);
|
|
56179
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56180
|
+
className: "inline-flex items-center gap-1.5 max-w-full",
|
|
56181
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
|
|
56182
|
+
running: running?.running ?? null,
|
|
56183
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WashingMachine, {
|
|
56184
|
+
className: "h-4 w-4 text-foreground-muted",
|
|
56185
|
+
"aria-hidden": true
|
|
56186
|
+
})
|
|
56187
|
+
}), power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56188
|
+
className: "text-xs tabular-nums text-foreground-subtle",
|
|
56189
|
+
children: formatWatts(power.power)
|
|
55810
56190
|
})]
|
|
55811
56191
|
});
|
|
55812
56192
|
}
|
|
55813
|
-
|
|
55814
|
-
|
|
55815
|
-
function
|
|
56193
|
+
//#endregion
|
|
56194
|
+
//#region src/composites/device-controls/heating-valve-readout.tsx
|
|
56195
|
+
function formatHeatingValvePosition(position) {
|
|
56196
|
+
return position !== null ? `${position}%` : "—";
|
|
56197
|
+
}
|
|
56198
|
+
function HeatingValveReadout({ trpc, deviceId, layout }) {
|
|
55816
56199
|
const t = useUiText();
|
|
55817
|
-
const
|
|
55818
|
-
|
|
55819
|
-
|
|
55820
|
-
|
|
55821
|
-
|
|
55822
|
-
|
|
55823
|
-
|
|
55824
|
-
|
|
55825
|
-
|
|
55826
|
-
|
|
55827
|
-
|
|
55828
|
-
|
|
55829
|
-
children:
|
|
55830
|
-
|
|
55831
|
-
|
|
55832
|
-
|
|
55833
|
-
}),
|
|
56200
|
+
const { cap, slice } = useDeviceCapability({
|
|
56201
|
+
trpc,
|
|
56202
|
+
deviceId,
|
|
56203
|
+
cap: (d) => d.heatingValve,
|
|
56204
|
+
slice: (d) => d.state.heatingValve
|
|
56205
|
+
});
|
|
56206
|
+
if (cap === void 0 || slice === void 0) return null;
|
|
56207
|
+
const label = t("uiLibrary.heatingValveReadout.valve", "Valve");
|
|
56208
|
+
const value = formatHeatingValvePosition(slice.position);
|
|
56209
|
+
if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56210
|
+
className: "inline-flex items-center text-xs tabular-nums text-foreground-subtle",
|
|
56211
|
+
"aria-label": `${label} ${value}`,
|
|
56212
|
+
children: value
|
|
56213
|
+
});
|
|
56214
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56215
|
+
className: "flex items-center gap-2 text-xs tabular-nums text-foreground-subtle",
|
|
56216
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56217
|
+
className: "font-medium text-foreground",
|
|
56218
|
+
children: value
|
|
56219
|
+
})]
|
|
55834
56220
|
});
|
|
55835
56221
|
}
|
|
55836
56222
|
//#endregion
|
|
@@ -57922,6 +58308,10 @@ var CONTROL_KIND_COMPONENTS = {
|
|
|
57922
58308
|
InlineControl: PetFountainInlineControl,
|
|
57923
58309
|
HeroCard: PetFountainHeroCard
|
|
57924
58310
|
},
|
|
58311
|
+
heater: {
|
|
58312
|
+
InlineControl: HeaterInlineControl,
|
|
58313
|
+
HeroCard: HeaterHeroCard
|
|
58314
|
+
},
|
|
57925
58315
|
dishwasher: {
|
|
57926
58316
|
InlineControl: DishwasherInlineControl,
|
|
57927
58317
|
HeroCard: DishwasherHeroCard
|
|
@@ -58009,6 +58399,7 @@ var CONTROL_FILLS = {
|
|
|
58009
58399
|
[_camstack_types.DeviceType.Image]: false,
|
|
58010
58400
|
[_camstack_types.DeviceType.PetFeeder]: false,
|
|
58011
58401
|
[_camstack_types.DeviceType.PetFountain]: false,
|
|
58402
|
+
[_camstack_types.DeviceType.Heater]: false,
|
|
58012
58403
|
[_camstack_types.DeviceType.Dishwasher]: false,
|
|
58013
58404
|
[_camstack_types.DeviceType.Washer]: false,
|
|
58014
58405
|
[_camstack_types.DeviceType.Dryer]: false
|
|
@@ -72481,6 +72872,9 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
|
|
|
72481
72872
|
});
|
|
72482
72873
|
}
|
|
72483
72874
|
//#endregion
|
|
72875
|
+
//#region src/ui-library-version.ts
|
|
72876
|
+
var uiLibraryVersion = "1.2.370";
|
|
72877
|
+
//#endregion
|
|
72484
72878
|
//#region src/hooks/use-account-unit-preferences.ts
|
|
72485
72879
|
/**
|
|
72486
72880
|
* Unit preferences in the signed-in user's ACCOUNT preferences (D894): one
|
|
@@ -74016,6 +74410,7 @@ exports.ThermostatInlineControl = ThermostatInlineControl;
|
|
|
74016
74410
|
exports.TimeSeriesChart = TimeSeriesChart;
|
|
74017
74411
|
exports.TimelineSlider = TimelineSlider;
|
|
74018
74412
|
exports.Timer = Timer;
|
|
74413
|
+
exports.TimerOff = TimerOff;
|
|
74019
74414
|
exports.TimezoneSelector = TimezoneSelector;
|
|
74020
74415
|
exports.ToggleLeft = ToggleLeft;
|
|
74021
74416
|
exports.ToggleRight = ToggleRight;
|
|
@@ -74423,6 +74818,7 @@ exports.translateAddonKey = translateAddonKey;
|
|
|
74423
74818
|
exports.trpc = trpc;
|
|
74424
74819
|
exports.typeLabel = typeLabel;
|
|
74425
74820
|
exports.typedToStored = typedToStored;
|
|
74821
|
+
exports.uiLibraryVersion = uiLibraryVersion;
|
|
74426
74822
|
exports.unitAxes = unitAxes;
|
|
74427
74823
|
exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
|
|
74428
74824
|
exports.useAccessoriesSetChildHidden = useAccessoriesSetChildHidden;
|
|
@@ -74882,6 +75278,8 @@ exports.useDisplayPaletteGetPalette = useDisplayPaletteGetPalette;
|
|
|
74882
75278
|
exports.useDisplayPaletteListPresets = useDisplayPaletteListPresets;
|
|
74883
75279
|
exports.useDisplayPaletteSetPalette = useDisplayPaletteSetPalette;
|
|
74884
75280
|
exports.useDisplayPaletteView = useDisplayPaletteView;
|
|
75281
|
+
exports.useDoNotDisturbGetStatus = useDoNotDisturbGetStatus;
|
|
75282
|
+
exports.useDoNotDisturbSetEnabled = useDoNotDisturbSetEnabled;
|
|
74885
75283
|
exports.useDoorbellEvents = useDoorbellEvents;
|
|
74886
75284
|
exports.useDoorbellGetStatus = useDoorbellGetStatus;
|
|
74887
75285
|
exports.useEnumSensorGetStatus = useEnumSensorGetStatus;
|
|
@@ -74910,6 +75308,10 @@ exports.useFanControlSetDirection = useFanControlSetDirection;
|
|
|
74910
75308
|
exports.useFanControlSetOscillating = useFanControlSetOscillating;
|
|
74911
75309
|
exports.useFanControlSetPercentage = useFanControlSetPercentage;
|
|
74912
75310
|
exports.useFanControlSetPreset = useFanControlSetPreset;
|
|
75311
|
+
exports.useFanPresetsApplyPreset = useFanPresetsApplyPreset;
|
|
75312
|
+
exports.useFanPresetsCapturePreset = useFanPresetsCapturePreset;
|
|
75313
|
+
exports.useFanPresetsGetPresets = useFanPresetsGetPresets;
|
|
75314
|
+
exports.useFanPresetsSetPresets = useFanPresetsSetPresets;
|
|
74913
75315
|
exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
|
|
74914
75316
|
exports.useFloodGetStatus = useFloodGetStatus;
|
|
74915
75317
|
exports.useGasGetStatus = useGasGetStatus;
|
|
@@ -75519,6 +75921,7 @@ exports.useRecordingOnboardGetOptions = useRecordingOnboardGetOptions;
|
|
|
75519
75921
|
exports.useRecordingOnboardGetStatus = useRecordingOnboardGetStatus;
|
|
75520
75922
|
exports.useRecordingOnboardSetSettings = useRecordingOnboardSetSettings;
|
|
75521
75923
|
exports.useRecordingSignalGetStatus = useRecordingSignalGetStatus;
|
|
75924
|
+
exports.useRegisteredHotkeys = useRegisteredHotkeys;
|
|
75522
75925
|
exports.useRemoteComponent = useRemoteComponent;
|
|
75523
75926
|
exports.useRemoteGetConfig = useRemoteGetConfig;
|
|
75524
75927
|
exports.useRemoteLayoutComponentsListComponents = useRemoteLayoutComponentsListComponents;
|
|
@@ -75711,6 +76114,9 @@ exports.useTerminalSessionSetInstanceEnabled = useTerminalSessionSetInstanceEnab
|
|
|
75711
76114
|
exports.useTerminalSessionUpdateInstance = useTerminalSessionUpdateInstance;
|
|
75712
76115
|
exports.useTerminalSessionWriteInput = useTerminalSessionWriteInput;
|
|
75713
76116
|
exports.useThemeMode = require_theme_index.useThemeMode$1;
|
|
76117
|
+
exports.useTimedRunCancel = useTimedRunCancel;
|
|
76118
|
+
exports.useTimedRunGetStatus = useTimedRunGetStatus;
|
|
76119
|
+
exports.useTimedRunRunFor = useTimedRunRunFor;
|
|
75714
76120
|
exports.useToastOnToast = useToastOnToast;
|
|
75715
76121
|
exports.useTrainingCanaryResult = useTrainingCanaryResult;
|
|
75716
76122
|
exports.useTrainingCancelJob = useTrainingCancelJob;
|