@camstack/ui-library 1.2.299 → 1.2.301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2185,6 +2185,54 @@ var Hash = require_createLucideIcon.createLucideIcon("hash", [
2185
2185
  * This source code is licensed under the ISC license.
2186
2186
  * See the LICENSE file in the root directory of this source tree.
2187
2187
  */
2188
+ var Heater = require_createLucideIcon.createLucideIcon("heater", [
2189
+ ["path", {
2190
+ d: "M11 8c2-3-2-3 0-6",
2191
+ key: "1ldv5m"
2192
+ }],
2193
+ ["path", {
2194
+ d: "M15.5 8c2-3-2-3 0-6",
2195
+ key: "1otqoz"
2196
+ }],
2197
+ ["path", {
2198
+ d: "M6 10h.01",
2199
+ key: "1lbq93"
2200
+ }],
2201
+ ["path", {
2202
+ d: "M6 14h.01",
2203
+ key: "zudwn7"
2204
+ }],
2205
+ ["path", {
2206
+ d: "M10 16v-4",
2207
+ key: "1c25yv"
2208
+ }],
2209
+ ["path", {
2210
+ d: "M14 16v-4",
2211
+ key: "1dkbt8"
2212
+ }],
2213
+ ["path", {
2214
+ d: "M18 16v-4",
2215
+ key: "1yg9me"
2216
+ }],
2217
+ ["path", {
2218
+ d: "M20 6a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3",
2219
+ key: "1ubg90"
2220
+ }],
2221
+ ["path", {
2222
+ d: "M5 20v2",
2223
+ key: "1abpe8"
2224
+ }],
2225
+ ["path", {
2226
+ d: "M19 20v2",
2227
+ key: "kqn6ft"
2228
+ }]
2229
+ ]);
2230
+ /**
2231
+ * @license lucide-react v0.576.0 - ISC
2232
+ *
2233
+ * This source code is licensed under the ISC license.
2234
+ * See the LICENSE file in the root directory of this source tree.
2235
+ */
2188
2236
  var House = require_createLucideIcon.createLucideIcon("house", [["path", {
2189
2237
  d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",
2190
2238
  key: "5wwlr5"
@@ -4173,6 +4221,34 @@ var Thermometer = require_createLucideIcon.createLucideIcon("thermometer", [["pa
4173
4221
  * This source code is licensed under the ISC license.
4174
4222
  * See the LICENSE file in the root directory of this source tree.
4175
4223
  */
4224
+ var Timer = require_createLucideIcon.createLucideIcon("timer", [
4225
+ ["line", {
4226
+ x1: "10",
4227
+ x2: "14",
4228
+ y1: "2",
4229
+ y2: "2",
4230
+ key: "14vaq8"
4231
+ }],
4232
+ ["line", {
4233
+ x1: "12",
4234
+ x2: "15",
4235
+ y1: "14",
4236
+ y2: "11",
4237
+ key: "17fdiu"
4238
+ }],
4239
+ ["circle", {
4240
+ cx: "12",
4241
+ cy: "14",
4242
+ r: "8",
4243
+ key: "1e1u0o"
4244
+ }]
4245
+ ]);
4246
+ /**
4247
+ * @license lucide-react v0.576.0 - ISC
4248
+ *
4249
+ * This source code is licensed under the ISC license.
4250
+ * See the LICENSE file in the root directory of this source tree.
4251
+ */
4176
4252
  var ToggleLeft = require_createLucideIcon.createLucideIcon("toggle-left", [["circle", {
4177
4253
  cx: "9",
4178
4254
  cy: "12",
@@ -5275,6 +5351,11 @@ var DEVICE_TYPE_META = {
5275
5351
  icon: Bone,
5276
5352
  labelKey: "uiLibrary.deviceMeta.type.petFeeder",
5277
5353
  labelFallback: "Pet feeder"
5354
+ },
5355
+ [_camstack_types.DeviceType.Dishwasher]: {
5356
+ icon: Waves,
5357
+ labelKey: "uiLibrary.deviceMeta.type.dishwasher",
5358
+ labelFallback: "Dishwasher"
5278
5359
  }
5279
5360
  };
5280
5361
  var DEVICE_ROLE_META = {
@@ -5756,7 +5837,7 @@ function lucideExportName(key) {
5756
5837
  function loadIconGlyphs() {
5757
5838
  if (fullTable !== void 0) return Promise.resolve();
5758
5839
  if (failure !== void 0) return Promise.reject(failure);
5759
- pending ??= Promise.resolve().then(() => require("./icons-CLC60C-_.cjs")).then((module) => {
5840
+ pending ??= Promise.resolve().then(() => require("./icons-B1AFXUaZ.cjs")).then((module) => {
5760
5841
  fullTable = module;
5761
5842
  publish();
5762
5843
  }, (error) => {
@@ -10931,6 +11012,32 @@ var useDeviceStateListClaims = trpc.deviceState.listClaims.useQuery;
10931
11012
  var useDeviceStateGetNativeCapSlice = trpc.deviceState.getNativeCapSlice.useQuery;
10932
11013
  /** Generated alias around `trpc.deviceStatus.getStatus.useQuery`. */
10933
11014
  var useDeviceStatusGetStatus = trpc.deviceStatus.getStatus.useQuery;
11015
+ /** Generated alias around `trpc.dishwasher.start.useMutation`. */
11016
+ var useDishwasherStart = trpc.dishwasher.start.useMutation;
11017
+ /** Generated alias around `trpc.dishwasher.pause.useMutation`. */
11018
+ var useDishwasherPause = trpc.dishwasher.pause.useMutation;
11019
+ /** Generated alias around `trpc.dishwasher.stop.useMutation`. */
11020
+ var useDishwasherStop = trpc.dishwasher.stop.useMutation;
11021
+ /** Generated alias around `trpc.dishwasher.setProgram.useMutation`. */
11022
+ var useDishwasherSetProgram = trpc.dishwasher.setProgram.useMutation;
11023
+ /** Generated alias around `trpc.dishwasher.setPower.useMutation`. */
11024
+ var useDishwasherSetPower = trpc.dishwasher.setPower.useMutation;
11025
+ /** Generated alias around `trpc.dishwasher.setHalfLoad.useMutation`. */
11026
+ var useDishwasherSetHalfLoad = trpc.dishwasher.setHalfLoad.useMutation;
11027
+ /** Generated alias around `trpc.dishwasher.setHygienePlus.useMutation`. */
11028
+ var useDishwasherSetHygienePlus = trpc.dishwasher.setHygienePlus.useMutation;
11029
+ /** Generated alias around `trpc.dishwasher.setExtraDry.useMutation`. */
11030
+ var useDishwasherSetExtraDry = trpc.dishwasher.setExtraDry.useMutation;
11031
+ /** Generated alias around `trpc.dishwasher.setIntensiveZone.useMutation`. */
11032
+ var useDishwasherSetIntensiveZone = trpc.dishwasher.setIntensiveZone.useMutation;
11033
+ /** Generated alias around `trpc.dishwasher.setVarioSpeedPlus.useMutation`. */
11034
+ var useDishwasherSetVarioSpeedPlus = trpc.dishwasher.setVarioSpeedPlus.useMutation;
11035
+ /** Generated alias around `trpc.dishwasher.setChildLock.useMutation`. */
11036
+ var useDishwasherSetChildLock = trpc.dishwasher.setChildLock.useMutation;
11037
+ /** Generated alias around `trpc.dishwasher.setOption.useMutation`. */
11038
+ var useDishwasherSetOption = trpc.dishwasher.setOption.useMutation;
11039
+ /** Generated alias around `trpc.dishwasher.getStatus.useQuery`. */
11040
+ var useDishwasherGetStatus = trpc.dishwasher.getStatus.useQuery;
10934
11041
  /** Generated alias around `trpc.doorbell.getStatus.useQuery`. */
10935
11042
  var useDoorbellGetStatus = trpc.doorbell.getStatus.useQuery;
10936
11043
  /** Generated alias around `trpc.enumSensor.getStatus.useQuery`. */
@@ -10985,6 +11092,8 @@ var useFeatureProbeGetStatus = trpc.featureProbe.getStatus.useQuery;
10985
11092
  var useFloodGetStatus = trpc.flood.getStatus.useQuery;
10986
11093
  /** Generated alias around `trpc.gas.getStatus.useQuery`. */
10987
11094
  var useGasGetStatus = trpc.gas.getStatus.useQuery;
11095
+ /** Generated alias around `trpc.heatingValve.getStatus.useQuery`. */
11096
+ var useHeatingValveGetStatus = trpc.heatingValve.getStatus.useQuery;
10988
11097
  /** Generated alias around `trpc.humidifier.setOn.useMutation`. */
10989
11098
  var useHumidifierSetOn = trpc.humidifier.setOn.useMutation;
10990
11099
  /** Generated alias around `trpc.humidifier.setTargetHumidity.useMutation`. */
@@ -28815,7 +28924,7 @@ function AddonActionSelectField({ field, value, values, onChange, disabled, onAc
28815
28924
  })()
28816
28925
  });
28817
28926
  }
28818
- function AddonActionButtonField({ field, values, disabled, onAction }) {
28927
+ function AddonActionButtonField({ field, values, disabled, onAction, onChange, onChangeMany }) {
28819
28928
  const t = useUiText();
28820
28929
  const [loading, setLoading] = (0, react$1.useState)(false);
28821
28930
  const [status, setStatus] = (0, react$1.useState)(null);
@@ -28847,6 +28956,17 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
28847
28956
  try {
28848
28957
  const result = await onAction(field.action, field.key, params);
28849
28958
  const resultRecord = typeof result === "object" && result !== null ? result : null;
28959
+ if (resultRecord !== null && field.assignResult && (onChange || onChangeMany)) {
28960
+ const patch = {};
28961
+ for (const [formKey, resultKey] of Object.entries(field.assignResult)) {
28962
+ const assigned = resultRecord[resultKey];
28963
+ if (typeof assigned === "string") patch[formKey] = assigned;
28964
+ }
28965
+ if (Object.keys(patch).length > 0) {
28966
+ if (onChangeMany) onChangeMany(patch);
28967
+ else if (onChange) for (const [formKey, assigned] of Object.entries(patch)) onChange(formKey, assigned);
28968
+ }
28969
+ }
28850
28970
  setStatus({
28851
28971
  kind: "ok",
28852
28972
  text: field.successMessage ? field.successMessage.replace(/\{(\w+)\}/g, (_, k) => {
@@ -29195,7 +29315,7 @@ function isFieldVisible(field, values, allFields) {
29195
29315
  if (condition.notIncludes !== void 0) return !Array.isArray(condVal) || !condVal.includes(condition.notIncludes);
29196
29316
  return true;
29197
29317
  }
29198
- function FormField({ field, values, onChange, disabled, translationFn, onTestField, externalProbe, allFields, onAction }) {
29318
+ function FormField({ field, values, onChange, onChangeMany, disabled, translationFn, onTestField, externalProbe, allFields, onAction }) {
29199
29319
  if (hasMultiple(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultipleFieldWrapper, {
29200
29320
  field,
29201
29321
  values,
@@ -29431,7 +29551,9 @@ function FormField({ field, values, onChange, disabled, translationFn, onTestFie
29431
29551
  field,
29432
29552
  values,
29433
29553
  disabled,
29434
- onAction
29554
+ onAction,
29555
+ onChange,
29556
+ onChangeMany
29435
29557
  });
29436
29558
  case "device-action-button": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceActionButtonField, {
29437
29559
  field,
@@ -42914,6 +43036,19 @@ function FanPanel({ trpc, deviceId, features, layout, optimistic }) {
42914
43036
  }
42915
43037
  //#endregion
42916
43038
  //#region src/composites/device-controls/climate-panel.tsx
43039
+ /** Step size for target ± buttons; defaults to 1 °C when unset or invalid. */
43040
+ function resolveClimateTempStep(tempStep) {
43041
+ if (typeof tempStep === "number" && Number.isFinite(tempStep) && tempStep > 0) return tempStep;
43042
+ return 1;
43043
+ }
43044
+ function nextClimateTarget(current, deltaSteps, display) {
43045
+ let next = current + deltaSteps * resolveClimateTempStep(display.tempStep);
43046
+ const min = display.minTemp;
43047
+ const max = display.maxTemp;
43048
+ if (typeof min === "number" && Number.isFinite(min)) next = Math.max(next, min);
43049
+ if (typeof max === "number" && Number.isFinite(max)) next = Math.min(next, max);
43050
+ return next;
43051
+ }
42917
43052
  /**
42918
43053
  * Exhaustive colour map — one entry per HvacMode. Build will fail if a
42919
43054
  * mode is added to the enum and not reflected here (Record<HvacMode, ...>
@@ -42994,7 +43129,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
42994
43129
  onClick: (e) => {
42995
43130
  e.stopPropagation();
42996
43131
  if (!busy && display.target !== null) {
42997
- const next = display.target - 1;
43132
+ const next = nextClimateTarget(display.target, -1, display);
42998
43133
  applyOptimistic({ target: next });
42999
43134
  cap.setTarget({ target: next });
43000
43135
  }
@@ -43008,7 +43143,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43008
43143
  onClick: (e) => {
43009
43144
  e.stopPropagation();
43010
43145
  if (!busy && display.target !== null) {
43011
- const next = display.target + 1;
43146
+ const next = nextClimateTarget(display.target, 1, display);
43012
43147
  applyOptimistic({ target: next });
43013
43148
  cap.setTarget({ target: next });
43014
43149
  }
@@ -43063,7 +43198,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43063
43198
  onClick: (e) => {
43064
43199
  e.stopPropagation();
43065
43200
  if (!busy && display.target !== null) {
43066
- const next = display.target - 1;
43201
+ const next = nextClimateTarget(display.target, -1, display);
43067
43202
  applyOptimistic({ target: next });
43068
43203
  cap.setTarget({ target: next });
43069
43204
  }
@@ -43078,7 +43213,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43078
43213
  onClick: (e) => {
43079
43214
  e.stopPropagation();
43080
43215
  if (!busy && display.target !== null) {
43081
- const next = display.target + 1;
43216
+ const next = nextClimateTarget(display.target, 1, display);
43082
43217
  applyOptimistic({ target: next });
43083
43218
  cap.setTarget({ target: next });
43084
43219
  }
@@ -43736,7 +43871,8 @@ var CONTROL_CAP_NAMES = [
43736
43871
  "climate-control",
43737
43872
  "media-player",
43738
43873
  "control",
43739
- "pet-feeder"
43874
+ "pet-feeder",
43875
+ "dishwasher"
43740
43876
  ];
43741
43877
  //#endregion
43742
43878
  //#region src/composites/device-controls/popover-row-action.tsx
@@ -48547,7 +48683,462 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
48547
48683
  });
48548
48684
  }
48549
48685
  //#endregion
48686
+ //#region src/composites/device-controls/dishwasher-control.tsx
48687
+ /**
48688
+ * Dishwasher controls for a `DeviceType.Dishwasher` device.
48689
+ *
48690
+ * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
48691
+ * mirroring vacuum-control / pet-feeder patterns.
48692
+ */
48693
+ function dishwasherStateMeta(state, t = defaultUiText) {
48694
+ switch (state) {
48695
+ case "running": return {
48696
+ label: t("uiLibrary.dishwasher.running", "Running"),
48697
+ tone: "text-primary"
48698
+ };
48699
+ case "paused": return {
48700
+ label: t("uiLibrary.dishwasher.paused", "Paused"),
48701
+ tone: "text-warning"
48702
+ };
48703
+ case "finished": return {
48704
+ label: t("uiLibrary.dishwasher.finished", "Finished"),
48705
+ tone: "text-success"
48706
+ };
48707
+ case "error": return {
48708
+ label: t("uiLibrary.dishwasher.error", "Error"),
48709
+ tone: "text-destructive"
48710
+ };
48711
+ case "unavailable": return {
48712
+ label: t("uiLibrary.dishwasher.unavailable", "Unavailable"),
48713
+ tone: "text-foreground-subtle"
48714
+ };
48715
+ case "idle": return {
48716
+ label: t("uiLibrary.dishwasher.idle", "Idle"),
48717
+ tone: "text-foreground-subtle"
48718
+ };
48719
+ case void 0: return {
48720
+ label: t("uiLibrary.dishwasher.unknown", "Unknown"),
48721
+ tone: "text-foreground-subtle"
48722
+ };
48723
+ }
48724
+ }
48725
+ function consumableTone(level) {
48726
+ switch (level) {
48727
+ case "ok": return "text-success";
48728
+ case "low": return "text-warning";
48729
+ case "empty": return "text-destructive";
48730
+ case null: return "text-foreground-subtle";
48731
+ }
48732
+ }
48733
+ function useDishwasher(trpc, deviceId) {
48734
+ return useDeviceCapability({
48735
+ trpc,
48736
+ deviceId,
48737
+ cap: (d) => d.dishwasher,
48738
+ slice: (d) => d.state.dishwasher
48739
+ });
48740
+ }
48741
+ function DishwasherIconButton({ label, busy, active, onClick, children }) {
48742
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
48743
+ type: "button",
48744
+ "aria-label": label,
48745
+ title: label,
48746
+ disabled: busy,
48747
+ onClick: (e) => {
48748
+ e.stopPropagation();
48749
+ onClick();
48750
+ },
48751
+ 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"),
48752
+ children
48753
+ });
48754
+ }
48755
+ function ConsumableBadge({ label, level }) {
48756
+ if (level === null) return null;
48757
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48758
+ className: cn("text-xs font-medium capitalize", consumableTone(level)),
48759
+ children: [
48760
+ label,
48761
+ ": ",
48762
+ level
48763
+ ]
48764
+ });
48765
+ }
48766
+ function DishwasherGraphic({ state, progressPercent }) {
48767
+ const running = state === "running";
48768
+ const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
48769
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48770
+ "aria-hidden": "true",
48771
+ 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",
48772
+ style: { boxShadow: "inset 0 0 0 2px rgba(255,255,255,.06)" },
48773
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48774
+ className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
48775
+ style: { height: `${pct}%` }
48776
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48777
+ className: "absolute inset-0 flex items-center justify-center text-foreground-subtle",
48778
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waves, { className: "h-10 w-10 opacity-70" })
48779
+ })]
48780
+ });
48781
+ }
48782
+ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48783
+ const t = useUiText();
48784
+ const { cap, slice, available } = useDishwasher(trpc, deviceId);
48785
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48786
+ if (cap === void 0) return null;
48787
+ if (display === void 0) return null;
48788
+ const busy = !available;
48789
+ const optionTogglesLocked = display.childLock === true;
48790
+ const meta = dishwasherStateMeta(display.state, t);
48791
+ const running = display.state === "running";
48792
+ const paused = display.state === "paused";
48793
+ const press = (fn, patch) => {
48794
+ if (busy) return;
48795
+ applyOptimistic(patch);
48796
+ fn();
48797
+ };
48798
+ if (layout === "compact") {
48799
+ const saltWarn = display.saltLevel === "low" || display.saltLevel === "empty";
48800
+ const rinseWarn = display.rinseAidLevel === "low" || display.rinseAidLevel === "empty";
48801
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48802
+ className: "inline-flex items-center gap-1.5 max-w-full",
48803
+ children: [
48804
+ display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48805
+ className: "inline-flex text-warning shrink-0",
48806
+ title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
48807
+ "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
48808
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
48809
+ }),
48810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48811
+ className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
48812
+ children: meta.label
48813
+ }),
48814
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48815
+ className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
48816
+ title: display.program,
48817
+ children: display.program
48818
+ }),
48819
+ display.remainingMinutes != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48820
+ className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
48821
+ children: [
48822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }),
48823
+ display.remainingMinutes,
48824
+ "m"
48825
+ ]
48826
+ }),
48827
+ saltWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48828
+ className: cn("text-[10px] font-medium shrink-0", consumableTone(display.saltLevel)),
48829
+ children: t("uiLibrary.dishwasher.salt", "Salt")
48830
+ }),
48831
+ rinseWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48832
+ className: cn("text-[10px] font-medium shrink-0", consumableTone(display.rinseAidLevel)),
48833
+ children: t("uiLibrary.dishwasher.rinseAid", "Rinse")
48834
+ }),
48835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48836
+ label: t("uiLibrary.dishwasher.start", "Start"),
48837
+ busy: busy || !display.remoteStartAllowed,
48838
+ active: running,
48839
+ onClick: () => press(() => cap.start({}), {
48840
+ state: "running",
48841
+ remoteStartAllowed: false
48842
+ }),
48843
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
48844
+ }),
48845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48846
+ label: t("uiLibrary.dishwasher.pause", "Pause"),
48847
+ busy: busy || !running,
48848
+ active: paused,
48849
+ onClick: () => press(() => cap.pause({}), { state: "paused" }),
48850
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
48851
+ })
48852
+ ]
48853
+ });
48854
+ }
48855
+ const programs = display.availablePrograms;
48856
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48857
+ className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
48858
+ children: [
48859
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48860
+ className: "flex items-start gap-4",
48861
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
48862
+ state: display.state,
48863
+ progressPercent: display.progressPercent
48864
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48865
+ className: "flex min-w-0 flex-1 flex-col gap-2",
48866
+ children: [
48867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48868
+ className: "flex flex-wrap items-center gap-2",
48869
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48870
+ className: cn("text-3xl font-semibold", meta.tone),
48871
+ children: meta.label
48872
+ }), !display.powerOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48873
+ className: "text-xs font-medium text-foreground-subtle",
48874
+ children: t("uiLibrary.dishwasher.powerOff", "Power off")
48875
+ })]
48876
+ }),
48877
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48878
+ className: "text-sm text-foreground-subtle",
48879
+ children: display.program
48880
+ }),
48881
+ display.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48882
+ className: "flex items-center gap-2",
48883
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48884
+ className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
48885
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48886
+ className: "h-full bg-primary transition-[width] duration-500",
48887
+ style: { width: `${Math.max(0, Math.min(100, display.progressPercent))}%` }
48888
+ })
48889
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48890
+ className: "tabular-nums text-xs text-foreground-subtle",
48891
+ children: [Math.round(display.progressPercent), "%"]
48892
+ })]
48893
+ }),
48894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48895
+ className: "flex flex-wrap gap-3",
48896
+ children: [
48897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableBadge, {
48898
+ label: t("uiLibrary.dishwasher.salt", "Salt"),
48899
+ level: display.saltLevel
48900
+ }),
48901
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableBadge, {
48902
+ label: t("uiLibrary.dishwasher.rinseAid", "Rinse aid"),
48903
+ level: display.rinseAidLevel
48904
+ }),
48905
+ display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48906
+ className: "inline-flex items-center gap-1 text-xs text-warning",
48907
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
48908
+ })
48909
+ ]
48910
+ }),
48911
+ display.errorCode != null && display.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48912
+ className: "inline-flex items-center gap-1 text-xs text-destructive",
48913
+ 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 ?? t("uiLibrary.dishwasher.errorCode", "Error {{code}}", { code: display.errorCode }) })]
48914
+ }),
48915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48916
+ className: "flex flex-wrap items-center gap-2",
48917
+ children: [
48918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48919
+ label: t("uiLibrary.dishwasher.start", "Start"),
48920
+ busy: busy || !display.remoteStartAllowed,
48921
+ active: running,
48922
+ onClick: () => press(() => cap.start({}), {
48923
+ state: "running",
48924
+ remoteStartAllowed: false
48925
+ }),
48926
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
48927
+ }),
48928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48929
+ label: t("uiLibrary.dishwasher.pause", "Pause"),
48930
+ busy: busy || !running,
48931
+ active: paused,
48932
+ onClick: () => press(() => cap.pause({}), { state: "paused" }),
48933
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
48934
+ }),
48935
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48936
+ label: t("uiLibrary.dishwasher.stop", "Stop"),
48937
+ busy,
48938
+ active: false,
48939
+ onClick: () => press(() => cap.stop({}), {
48940
+ state: "idle",
48941
+ remainingMinutes: null
48942
+ }),
48943
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
48944
+ }),
48945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48946
+ label: t("uiLibrary.dishwasher.power", "Power"),
48947
+ busy,
48948
+ active: display.powerOn,
48949
+ onClick: () => {
48950
+ const next = !display.powerOn;
48951
+ press(() => cap.setPower({ on: next }), { powerOn: next });
48952
+ },
48953
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
48954
+ })
48955
+ ]
48956
+ })
48957
+ ]
48958
+ })]
48959
+ }),
48960
+ programs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48961
+ className: "flex items-center gap-2 text-xs text-foreground-subtle",
48962
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48963
+ className: "w-16 shrink-0",
48964
+ children: t("uiLibrary.dishwasher.program", "Program")
48965
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
48966
+ "aria-label": t("uiLibrary.dishwasher.program", "Program"),
48967
+ value: display.program ?? "",
48968
+ disabled: busy,
48969
+ onChange: (e) => {
48970
+ const program = e.target.value;
48971
+ if (!program) return;
48972
+ applyOptimistic({ program });
48973
+ cap.setProgram({ program });
48974
+ },
48975
+ onClick: (e) => {
48976
+ e.stopPropagation();
48977
+ },
48978
+ className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
48979
+ children: [display.program == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48980
+ value: "",
48981
+ children: "—"
48982
+ }), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48983
+ value: p,
48984
+ children: p
48985
+ }, p))]
48986
+ })]
48987
+ }),
48988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48989
+ className: "flex flex-wrap gap-4 text-xs",
48990
+ children: [
48991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
48992
+ label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
48993
+ checked: display.halfLoad,
48994
+ busy: busy || optionTogglesLocked,
48995
+ onChange: (on) => {
48996
+ applyOptimistic({ halfLoad: on });
48997
+ cap.setHalfLoad({ on });
48998
+ }
48999
+ }),
49000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49001
+ label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
49002
+ checked: display.hygienePlus,
49003
+ busy: busy || optionTogglesLocked,
49004
+ onChange: (on) => {
49005
+ applyOptimistic({ hygienePlus: on });
49006
+ cap.setHygienePlus({ on });
49007
+ }
49008
+ }),
49009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49010
+ label: t("uiLibrary.dishwasher.extraDry", "Extra dry"),
49011
+ checked: display.extraDry,
49012
+ busy: busy || optionTogglesLocked,
49013
+ onChange: (on) => {
49014
+ applyOptimistic({ extraDry: on });
49015
+ cap.setExtraDry({ on });
49016
+ }
49017
+ }),
49018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49019
+ label: t("uiLibrary.dishwasher.intensiveZone", "Intensive zone"),
49020
+ checked: display.intensiveZone,
49021
+ busy: busy || optionTogglesLocked,
49022
+ onChange: (on) => {
49023
+ applyOptimistic({ intensiveZone: on });
49024
+ cap.setIntensiveZone({ on });
49025
+ }
49026
+ }),
49027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49028
+ label: t("uiLibrary.dishwasher.varioSpeedPlus", "VarioSpeed+"),
49029
+ checked: display.varioSpeedPlus,
49030
+ busy: busy || optionTogglesLocked,
49031
+ onChange: (on) => {
49032
+ applyOptimistic({ varioSpeedPlus: on });
49033
+ cap.setVarioSpeedPlus({ on });
49034
+ }
49035
+ }),
49036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49037
+ label: t("uiLibrary.dishwasher.childLock", "Child lock"),
49038
+ checked: display.childLock,
49039
+ busy,
49040
+ onChange: (on) => {
49041
+ applyOptimistic({ childLock: on });
49042
+ cap.setChildLock({ on });
49043
+ }
49044
+ })
49045
+ ]
49046
+ })
49047
+ ]
49048
+ });
49049
+ }
49050
+ function ToggleRow({ label, checked, busy, onChange }) {
49051
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
49052
+ className: "inline-flex items-center gap-2 cursor-pointer",
49053
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
49054
+ type: "checkbox",
49055
+ checked,
49056
+ disabled: busy,
49057
+ onChange: (e) => {
49058
+ onChange(e.target.checked);
49059
+ },
49060
+ onClick: (e) => {
49061
+ e.stopPropagation();
49062
+ },
49063
+ className: "rounded border-border"
49064
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
49065
+ });
49066
+ }
49067
+ function DishwasherInlineControl(props) {
49068
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
49069
+ ...props,
49070
+ layout: "compact"
49071
+ });
49072
+ }
49073
+ function DishwasherHeroCard(props) {
49074
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
49075
+ ...props,
49076
+ layout: "full"
49077
+ });
49078
+ }
49079
+ //#endregion
49080
+ //#region src/composites/device-controls/heating-valve-readout.tsx
49081
+ function formatHeatingValvePosition(position) {
49082
+ return position !== null ? `${position}%` : "—";
49083
+ }
49084
+ function HeatingValveReadout({ trpc, deviceId, layout }) {
49085
+ const t = useUiText();
49086
+ const { cap, slice } = useDeviceCapability({
49087
+ trpc,
49088
+ deviceId,
49089
+ cap: (d) => d.heatingValve,
49090
+ slice: (d) => d.state.heatingValve
49091
+ });
49092
+ if (cap === void 0 || slice === void 0) return null;
49093
+ const label = t("uiLibrary.heatingValveReadout.valve", "Valve");
49094
+ const value = formatHeatingValvePosition(slice.position);
49095
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49096
+ className: "inline-flex items-center text-xs tabular-nums text-foreground-subtle",
49097
+ "aria-label": `${label} ${value}`,
49098
+ children: value
49099
+ });
49100
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49101
+ className: "flex items-center gap-2 text-xs tabular-nums text-foreground-subtle",
49102
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49103
+ className: "font-medium text-foreground",
49104
+ children: value
49105
+ })]
49106
+ });
49107
+ }
49108
+ //#endregion
48550
49109
  //#region src/composites/device-controls/ac-panel.tsx
49110
+ /** Modes that mean the device can cool, dry, or move air — an air conditioner. */
49111
+ var COOLING_MODES = new Set([
49112
+ "cool",
49113
+ "heat_cool",
49114
+ "dry",
49115
+ "fan_only"
49116
+ ]);
49117
+ /** A heater offers heat (and maybe auto/off) and none of the cooling modes. */
49118
+ function climateOffersCooling(modes) {
49119
+ return modes.some((mode) => COOLING_MODES.has(mode));
49120
+ }
49121
+ /** Setpoint track bounds. Absent when the integration did not report both ends. */
49122
+ function climateSetpointBounds(status) {
49123
+ const min = status.minTemp;
49124
+ const max = status.maxTemp;
49125
+ if (typeof min !== "number" || typeof max !== "number") return null;
49126
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
49127
+ return {
49128
+ min,
49129
+ max
49130
+ };
49131
+ }
49132
+ /** Map a 0–1 drag fraction onto the setpoint, snapped to `tempStep`. */
49133
+ function climateTargetFromFraction(fraction, bounds, tempStep) {
49134
+ const step = resolveClimateTempStep(tempStep);
49135
+ const clamped = Math.min(1, Math.max(0, fraction));
49136
+ const raw = bounds.min + clamped * (bounds.max - bounds.min);
49137
+ const steps = Math.round((raw - bounds.min) / step);
49138
+ const snapped = bounds.min + steps * step;
49139
+ const rounded = Math.round(snapped * 1e3) / 1e3;
49140
+ return Math.min(bounds.max, Math.max(bounds.min, rounded));
49141
+ }
48551
49142
  /** AC-appropriate short mode labels. Exhaustive over HvacMode. */
48552
49143
  function modeLabel(t, mode) {
48553
49144
  switch (mode) {
@@ -48581,9 +49172,6 @@ var MODE_ACCENT = {
48581
49172
  dry: "#22d3ee",
48582
49173
  off: "#a1a1aa"
48583
49174
  };
48584
- /** Practical AC setpoint range (Celsius) for the target range track. */
48585
- var TARGET_MIN = 16;
48586
- var TARGET_MAX = 30;
48587
49175
  /** Title-case a snake/lower token for a selector option ('medium_low' → 'Medium
48588
49176
  * low', 'turbo' → 'Turbo'). Shared by the fan + preset selectors. */
48589
49177
  function fanLabel(token) {
@@ -48670,7 +49258,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48670
49258
  onClick: (e) => {
48671
49259
  e.stopPropagation();
48672
49260
  if (!busy && display.target !== null) {
48673
- const next = display.target - 1;
49261
+ const next = nextClimateTarget(display.target, -1, display);
48674
49262
  applyOptimistic({ target: next });
48675
49263
  cap.setTarget({ target: next });
48676
49264
  }
@@ -48684,7 +49272,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48684
49272
  onClick: (e) => {
48685
49273
  e.stopPropagation();
48686
49274
  if (!busy && display.target !== null) {
48687
- const next = display.target + 1;
49275
+ const next = nextClimateTarget(display.target, 1, display);
48688
49276
  applyOptimistic({ target: next });
48689
49277
  cap.setTarget({ target: next });
48690
49278
  }
@@ -48714,6 +49302,8 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48714
49302
  });
48715
49303
  const accent = MODE_ACCENT[display.mode];
48716
49304
  const on = display.mode !== "off";
49305
+ const heatingOnly = !climateOffersCooling(display.availableModes);
49306
+ const setpointBounds = climateSetpointBounds(display);
48717
49307
  const CurrentPresetIcon = presetIcon(display.preset);
48718
49308
  const boost = display.preset === "turbo";
48719
49309
  const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
@@ -48730,11 +49320,12 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48730
49320
  children: [
48731
49321
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48732
49322
  className: "pointer-events-none absolute inset-0 opacity-60",
48733
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
49323
+ children: heatingOnly && !on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RadiatorFins, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
48734
49324
  mode: display.mode,
48735
49325
  accent,
48736
49326
  periodSec: period,
48737
- swaying
49327
+ swaying,
49328
+ forceRising: heatingOnly
48738
49329
  })
48739
49330
  }),
48740
49331
  /* @__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" }),
@@ -48752,6 +49343,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48752
49343
  children: [
48753
49344
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
48754
49345
  mode: display.mode,
49346
+ heatingOnly,
48755
49347
  className: "h-3.5 w-3.5"
48756
49348
  }),
48757
49349
  modeLabel(t, display.mode),
@@ -48778,7 +49370,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48778
49370
  disabled: busy,
48779
49371
  onClick: () => {
48780
49372
  if (busy || display.target === null) return;
48781
- const next = display.target - 1;
49373
+ const next = nextClimateTarget(display.target, -1, display);
48782
49374
  applyOptimistic({ target: next });
48783
49375
  cap.setTarget({ target: next });
48784
49376
  }
@@ -48800,19 +49392,29 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48800
49392
  disabled: busy,
48801
49393
  onClick: () => {
48802
49394
  if (busy || display.target === null) return;
48803
- const next = display.target + 1;
49395
+ const next = nextClimateTarget(display.target, 1, display);
48804
49396
  applyOptimistic({ target: next });
48805
49397
  cap.setTarget({ target: next });
48806
49398
  }
48807
49399
  })
48808
49400
  ]
48809
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48810
- className: "relative z-10 w-36",
49401
+ }), setpointBounds !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49402
+ className: "relative z-10 w-full px-1",
48811
49403
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeTrack, {
48812
49404
  value: display.target,
48813
- min: TARGET_MIN,
48814
- max: TARGET_MAX,
48815
- accent
49405
+ min: setpointBounds.min,
49406
+ max: setpointBounds.max,
49407
+ step: display.tempStep,
49408
+ accent,
49409
+ disabled: busy,
49410
+ label: t("uiLibrary.acPanel.setpoint", "Setpoint"),
49411
+ onPreview: (next) => {
49412
+ applyOptimistic({ target: next });
49413
+ },
49414
+ onCommit: (next) => {
49415
+ applyOptimistic({ target: next });
49416
+ cap.setTarget({ target: next });
49417
+ }
48816
49418
  })
48817
49419
  })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48818
49420
  className: "relative z-10 flex items-baseline gap-0.5",
@@ -48836,6 +49438,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48836
49438
  className: "flex",
48837
49439
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
48838
49440
  mode: display.mode,
49441
+ heatingOnly,
48839
49442
  className: "h-4 w-4"
48840
49443
  })
48841
49444
  }),
@@ -48919,9 +49522,8 @@ function Field({ label, children }) {
48919
49522
  }), children]
48920
49523
  });
48921
49524
  }
48922
- function ModeIcon({ mode, className }) {
48923
- const Icon = MODE_ICON[mode];
48924
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
49525
+ function ModeIcon({ mode, heatingOnly = false, className }) {
49526
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(heatingOnly && mode === "heat" ? Heater : MODE_ICON[mode], {
48925
49527
  className,
48926
49528
  "aria-hidden": true
48927
49529
  });
@@ -48971,11 +49573,89 @@ function RoundStep({ label, symbol, disabled, onClick }) {
48971
49573
  children: symbol
48972
49574
  });
48973
49575
  }
48974
- /** Thin gradient range track with a knob at the setpoint fraction. */
48975
- function RangeTrack({ value, min, max, accent }) {
48976
- const pct = `${Math.min(1, Math.max(0, (value - min) / (max - min))) * 100}%`;
49576
+ /** Setpoint track. Drag previews; the command is sent when the pointer is released. */
49577
+ function RangeTrack({ value, min, max, step, accent, disabled, label, onPreview, onCommit }) {
49578
+ const trackRef = (0, react$1.useRef)(null);
49579
+ const draggingRef = (0, react$1.useRef)(false);
49580
+ const [draft, setDraft] = (0, react$1.useState)(null);
49581
+ const shown = draft ?? value;
49582
+ const pct = `${Math.min(1, Math.max(0, (shown - min) / (max - min))) * 100}%`;
49583
+ const tempAt = (clientX) => {
49584
+ const el = trackRef.current;
49585
+ if (el === null) return value;
49586
+ const rect = el.getBoundingClientRect();
49587
+ if (rect.width <= 0) return value;
49588
+ return climateTargetFromFraction((clientX - rect.left) / rect.width, {
49589
+ min,
49590
+ max
49591
+ }, step);
49592
+ };
49593
+ const onPointerDown = (event) => {
49594
+ if (disabled) return;
49595
+ event.stopPropagation();
49596
+ event.preventDefault();
49597
+ draggingRef.current = true;
49598
+ if (typeof event.currentTarget.setPointerCapture === "function") event.currentTarget.setPointerCapture(event.pointerId);
49599
+ const next = tempAt(event.clientX);
49600
+ setDraft(next);
49601
+ onPreview(next);
49602
+ };
49603
+ const onPointerMove = (event) => {
49604
+ if (disabled || !draggingRef.current) return;
49605
+ event.stopPropagation();
49606
+ const next = tempAt(event.clientX);
49607
+ setDraft(next);
49608
+ onPreview(next);
49609
+ };
49610
+ const endDrag = (event) => {
49611
+ if (!draggingRef.current) return;
49612
+ event.stopPropagation();
49613
+ draggingRef.current = false;
49614
+ const next = tempAt(event.clientX);
49615
+ setDraft(null);
49616
+ if (event.currentTarget.hasPointerCapture?.(event.pointerId) === true) event.currentTarget.releasePointerCapture(event.pointerId);
49617
+ onCommit(next);
49618
+ };
49619
+ const onKeyDown = (event) => {
49620
+ if (disabled) return;
49621
+ const current = draft ?? value;
49622
+ if (event.key === "ArrowRight" || event.key === "ArrowUp") {
49623
+ event.preventDefault();
49624
+ onCommit(nextClimateTarget(current, 1, {
49625
+ tempStep: step,
49626
+ minTemp: min,
49627
+ maxTemp: max
49628
+ }));
49629
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
49630
+ event.preventDefault();
49631
+ onCommit(nextClimateTarget(current, -1, {
49632
+ tempStep: step,
49633
+ minTemp: min,
49634
+ maxTemp: max
49635
+ }));
49636
+ } else if (event.key === "Home") {
49637
+ event.preventDefault();
49638
+ onCommit(min);
49639
+ } else if (event.key === "End") {
49640
+ event.preventDefault();
49641
+ onCommit(max);
49642
+ }
49643
+ };
48977
49644
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48978
- className: "mt-2 w-full",
49645
+ ref: trackRef,
49646
+ role: "slider",
49647
+ "aria-label": label,
49648
+ "aria-valuemin": min,
49649
+ "aria-valuemax": max,
49650
+ "aria-valuenow": shown,
49651
+ "aria-disabled": disabled,
49652
+ tabIndex: disabled ? -1 : 0,
49653
+ onPointerDown,
49654
+ onPointerMove,
49655
+ onPointerUp: endDrag,
49656
+ onPointerCancel: endDrag,
49657
+ onKeyDown,
49658
+ className: cn("mt-1 w-full touch-none select-none py-2", disabled ? "cursor-not-allowed opacity-40" : "cursor-ew-resize"),
48979
49659
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48980
49660
  className: "relative h-1.5 w-full rounded-full bg-surface-subtle",
48981
49661
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -48985,7 +49665,7 @@ function RangeTrack({ value, min, max, accent }) {
48985
49665
  background: accent
48986
49666
  }
48987
49667
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48988
- className: "absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-surface shadow",
49668
+ 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",
48989
49669
  style: {
48990
49670
  left: pct,
48991
49671
  background: accent
@@ -49002,7 +49682,7 @@ function RangeTrack({ value, min, max, accent }) {
49002
49682
  * vent; every active mode renders drifting particles that FALL (cool/dry/auto)
49003
49683
  * or RISE (heat), with a horizontal sweep overlaid when a swing axis is active.
49004
49684
  */
49005
- function AirflowViz({ mode, accent, periodSec, swaying }) {
49685
+ function AirflowViz({ mode, accent, periodSec, swaying, forceRising = false }) {
49006
49686
  const columns = [
49007
49687
  0,
49008
49688
  1,
@@ -49011,7 +49691,7 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49011
49691
  4,
49012
49692
  5
49013
49693
  ];
49014
- const rising = mode === "heat";
49694
+ const rising = forceRising || mode === "heat";
49015
49695
  const isDroplet = mode === "dry";
49016
49696
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49017
49697
  "aria-hidden": true,
@@ -49027,6 +49707,7 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49027
49707
  size: 56
49028
49708
  })
49029
49709
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49710
+ "data-testid": forceRising ? "climate-heat-rise" : void 0,
49030
49711
  className: cn("acv-sway-wrap absolute inset-0", swaying && "acv-sway"),
49031
49712
  style: swaying ? { animation: "acv-sway 2.6s ease-in-out infinite" } : void 0,
49032
49713
  children: columns.map((i) => {
@@ -49049,6 +49730,23 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49049
49730
  })
49050
49731
  });
49051
49732
  }
49733
+ /** Still radiator fins — the off state of a heating-only climate. */
49734
+ function RadiatorFins() {
49735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49736
+ "data-testid": "climate-radiator",
49737
+ "aria-hidden": true,
49738
+ className: "flex h-full items-end justify-center gap-1.5 px-8 pb-3 opacity-40",
49739
+ children: [
49740
+ 0,
49741
+ 1,
49742
+ 2,
49743
+ 3,
49744
+ 4,
49745
+ 5,
49746
+ 6
49747
+ ].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-10 w-1.5 rounded-t-full bg-foreground-subtle" }, i))
49748
+ });
49749
+ }
49052
49750
  /** Big spinning fan for `fan_only` mode. */
49053
49751
  function FanBlades({ color, periodSec, size }) {
49054
49752
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
@@ -49215,16 +49913,33 @@ var MODE_ARC_COLOR = {
49215
49913
  */
49216
49914
  function ThermostatHeroCard({ trpc, deviceId, features: _features, optimistic }) {
49217
49915
  const t = useUiText();
49218
- const { cap, slice, available } = useDeviceCapability({
49916
+ const climate = useDeviceCapability({
49219
49917
  trpc,
49220
49918
  deviceId,
49221
49919
  cap: (d) => d.climateControl,
49222
49920
  slice: (d) => d.state.climateControl
49223
49921
  });
49224
- const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
49225
- if (cap === void 0) return null;
49226
- if (display === void 0) return null;
49227
- const busy = !available;
49922
+ const valve = useDeviceCapability({
49923
+ trpc,
49924
+ deviceId,
49925
+ cap: (d) => d.heatingValve,
49926
+ slice: (d) => d.state.heatingValve
49927
+ });
49928
+ const { display, applyOptimistic } = useOptimisticSlice(climate.slice, { enabled: optimistic ?? true });
49929
+ const hasClimate = climate.cap !== void 0 && display !== void 0;
49930
+ const hasValve = valve.cap !== void 0;
49931
+ if (!hasClimate && !hasValve) return null;
49932
+ if (!hasClimate && hasValve) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49933
+ className: "flex flex-col items-stretch gap-3 rounded-lg border border-border bg-surface p-4",
49934
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatingValveReadout, {
49935
+ trpc,
49936
+ deviceId,
49937
+ layout: "full"
49938
+ })
49939
+ });
49940
+ if (climate.cap === void 0 || display === void 0) return null;
49941
+ const cap = climate.cap;
49942
+ const busy = !climate.available;
49228
49943
  const color = MODE_ARC_COLOR[display.mode];
49229
49944
  const isRange = display.mode === "heat_cool" || display.targetLow !== null && display.targetHigh !== null;
49230
49945
  const min = DEFAULT_TEMP_MIN;
@@ -49235,132 +49950,140 @@ function ThermostatHeroCard({ trpc, deviceId, features: _features, optimistic })
49235
49950
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49236
49951
  "data-mode": display.mode,
49237
49952
  className: cn("flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4 transition-colors", MODE_COLOR[display.mode]),
49238
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49239
- className: "flex flex-col items-center gap-3",
49240
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49241
- className: "relative",
49242
- style: {
49243
- width: GAUGE_SIZE,
49244
- height: GAUGE_SIZE
49245
- },
49246
- children: [
49247
- isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49248
- mode: "band",
49249
- band: {
49250
- from: fractionLow,
49251
- to: fractionHigh
49252
- },
49253
- color
49254
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49255
- mode: "fill",
49256
- fraction: singleFraction,
49257
- color
49258
- }),
49259
- isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49260
- "data-testid": "arc-knob",
49261
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49262
- fraction: fractionLow,
49263
- color,
49264
- knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49265
- onDragFraction: (f) => {
49266
- if (busy || display.targetHigh === null) return;
49267
- const next = Math.min(fractionToTemp(f, min, max), display.targetHigh);
49268
- applyOptimistic({ targetLow: next });
49269
- cap.setTargetRange({
49270
- targetLow: next,
49271
- targetHigh: display.targetHigh
49272
- });
49273
- }
49274
- })
49275
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49276
- "data-testid": "arc-knob",
49277
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49278
- fraction: fractionHigh,
49279
- color,
49280
- knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49281
- onDragFraction: (f) => {
49282
- if (busy || display.targetLow === null) return;
49283
- const next = Math.max(fractionToTemp(f, min, max), display.targetLow);
49284
- applyOptimistic({ targetHigh: next });
49285
- cap.setTargetRange({
49286
- targetLow: display.targetLow,
49287
- targetHigh: next
49288
- });
49289
- }
49290
- })
49291
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49292
- "data-testid": "arc-knob",
49293
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49953
+ children: [
49954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49955
+ className: "flex flex-col items-center gap-3",
49956
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49957
+ className: "relative",
49958
+ style: {
49959
+ width: GAUGE_SIZE,
49960
+ height: GAUGE_SIZE
49961
+ },
49962
+ children: [
49963
+ isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49964
+ mode: "band",
49965
+ band: {
49966
+ from: fractionLow,
49967
+ to: fractionHigh
49968
+ },
49969
+ color
49970
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49971
+ mode: "fill",
49294
49972
  fraction: singleFraction,
49295
- color,
49296
- knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49297
- onDragFraction: (f) => {
49298
- if (busy) return;
49299
- const next = fractionToTemp(f, min, max);
49300
- applyOptimistic({ target: next });
49301
- cap.setTarget({ target: next });
49302
- }
49303
- })
49304
- }),
49305
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49306
- className: "absolute inset-0 flex flex-col items-center justify-center gap-0.5 pointer-events-none",
49307
- children: [
49308
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49309
- "data-testid": "current-temp",
49310
- className: "text-4xl font-bold tabular-nums text-foreground leading-none",
49311
- children: [display.currentTemp !== null ? display.currentTemp : "—", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49312
- className: "text-lg text-foreground-subtle font-normal ml-0.5",
49313
- children: "°"
49314
- })]
49315
- }),
49316
- !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49317
- className: "text-xs font-semibold",
49318
- style: { color },
49319
- children: t("uiLibrary.thermostatHeroCard.setTo", "set {{value}}°", { value: display.target })
49320
- }),
49321
- isRange && display.targetLow !== null && display.targetHigh !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49322
- className: "text-xs font-semibold",
49323
- style: { color },
49324
- children: [
49325
- display.targetLow,
49326
- "° – ",
49327
- display.targetHigh,
49328
- "°"
49329
- ]
49973
+ color
49974
+ }),
49975
+ isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49976
+ "data-testid": "arc-knob",
49977
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49978
+ fraction: fractionLow,
49979
+ color,
49980
+ knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49981
+ onDragFraction: (f) => {
49982
+ if (busy || display.targetHigh === null) return;
49983
+ const next = Math.min(fractionToTemp(f, min, max), display.targetHigh);
49984
+ applyOptimistic({ targetLow: next });
49985
+ cap.setTargetRange({
49986
+ targetLow: next,
49987
+ targetHigh: display.targetHigh
49988
+ });
49989
+ }
49330
49990
  })
49331
- ]
49991
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49992
+ "data-testid": "arc-knob",
49993
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49994
+ fraction: fractionHigh,
49995
+ color,
49996
+ knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49997
+ onDragFraction: (f) => {
49998
+ if (busy || display.targetLow === null) return;
49999
+ const next = Math.max(fractionToTemp(f, min, max), display.targetLow);
50000
+ applyOptimistic({ targetHigh: next });
50001
+ cap.setTargetRange({
50002
+ targetLow: display.targetLow,
50003
+ targetHigh: next
50004
+ });
50005
+ }
50006
+ })
50007
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50008
+ "data-testid": "arc-knob",
50009
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
50010
+ fraction: singleFraction,
50011
+ color,
50012
+ knobTopPx: THERMOSTAT_KNOB_TOP_PX,
50013
+ onDragFraction: (f) => {
50014
+ if (busy) return;
50015
+ const next = fractionToTemp(f, min, max);
50016
+ applyOptimistic({ target: next });
50017
+ cap.setTarget({ target: next });
50018
+ }
50019
+ })
50020
+ }),
50021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50022
+ className: "absolute inset-0 flex flex-col items-center justify-center gap-0.5 pointer-events-none",
50023
+ children: [
50024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50025
+ "data-testid": "current-temp",
50026
+ className: "text-4xl font-bold tabular-nums text-foreground leading-none",
50027
+ children: [display.currentTemp !== null ? display.currentTemp : "—", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50028
+ className: "text-lg text-foreground-subtle font-normal ml-0.5",
50029
+ children: "°"
50030
+ })]
50031
+ }),
50032
+ !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50033
+ className: "text-xs font-semibold",
50034
+ style: { color },
50035
+ children: t("uiLibrary.thermostatHeroCard.setTo", "set {{value}}°", { value: display.target })
50036
+ }),
50037
+ isRange && display.targetLow !== null && display.targetHigh !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50038
+ className: "text-xs font-semibold",
50039
+ style: { color },
50040
+ children: [
50041
+ display.targetLow,
50042
+ "° – ",
50043
+ display.targetHigh,
50044
+ "°"
50045
+ ]
50046
+ })
50047
+ ]
50048
+ })
50049
+ ]
50050
+ }), !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50051
+ className: "w-[200px]",
50052
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stepper, {
50053
+ value: `${display.target}°`,
50054
+ onDelta: (d) => {
50055
+ if (busy || display.target === null) return;
50056
+ const next = display.target + d;
50057
+ applyOptimistic({ target: next });
50058
+ cap.setTarget({ target: next });
50059
+ }
49332
50060
  })
49333
- ]
49334
- }), !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49335
- className: "w-[200px]",
49336
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stepper, {
49337
- value: `${display.target}°`,
49338
- onDelta: (d) => {
49339
- if (busy || display.target === null) return;
49340
- const next = display.target + d;
49341
- applyOptimistic({ target: next });
49342
- cap.setTarget({ target: next });
50061
+ })]
50062
+ }),
50063
+ hasValve ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatingValveReadout, {
50064
+ trpc,
50065
+ deviceId,
50066
+ layout: "full"
50067
+ }) : null,
50068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
50069
+ "aria-label": t("uiLibrary.thermostatHeroCard.mode", "Mode"),
50070
+ value: display.mode,
50071
+ disabled: busy,
50072
+ onChange: (e) => {
50073
+ e.stopPropagation();
50074
+ const mode = e.target.value;
50075
+ if (!busy && mode) {
50076
+ applyOptimistic({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
50077
+ cap.setMode({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
49343
50078
  }
49344
- })
49345
- })]
49346
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
49347
- "aria-label": t("uiLibrary.thermostatHeroCard.mode", "Mode"),
49348
- value: display.mode,
49349
- disabled: busy,
49350
- onChange: (e) => {
49351
- e.stopPropagation();
49352
- const mode = e.target.value;
49353
- if (!busy && mode) {
49354
- applyOptimistic({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
49355
- cap.setMode({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
49356
- }
49357
- },
49358
- className: cn("w-full rounded border border-border bg-surface px-3 py-1.5 text-sm", busy && "opacity-40 cursor-not-allowed"),
49359
- children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
49360
- value: m,
49361
- children: m
49362
- }, m))
49363
- })]
50079
+ },
50080
+ className: cn("w-full rounded border border-border bg-surface px-3 py-1.5 text-sm", busy && "opacity-40 cursor-not-allowed"),
50081
+ children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
50082
+ value: m,
50083
+ children: m
50084
+ }, m))
50085
+ })
50086
+ ]
49364
50087
  });
49365
50088
  }
49366
50089
  //#endregion
@@ -50868,14 +51591,32 @@ function useClimateCapGuard(trpc, deviceId) {
50868
51591
  });
50869
51592
  return cap !== void 0;
50870
51593
  }
50871
- function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
50872
- if (!useClimateCapGuard(trpc, deviceId)) return null;
50873
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
51594
+ function useHeatingValveCapGuard(trpc, deviceId) {
51595
+ const { cap } = useDeviceCapability({
50874
51596
  trpc,
50875
51597
  deviceId,
50876
- features,
50877
- layout: "compact",
50878
- optimistic
51598
+ cap: (d) => d.heatingValve,
51599
+ slice: (d) => d.state.heatingValve
51600
+ });
51601
+ return cap !== void 0;
51602
+ }
51603
+ function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
51604
+ const hasClimate = useClimateCapGuard(trpc, deviceId);
51605
+ const hasValve = useHeatingValveCapGuard(trpc, deviceId);
51606
+ if (!hasClimate && !hasValve) return null;
51607
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
51608
+ className: "inline-flex items-center gap-2",
51609
+ children: [hasClimate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
51610
+ trpc,
51611
+ deviceId,
51612
+ features,
51613
+ layout: "compact",
51614
+ optimistic
51615
+ }) : null, hasValve ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatingValveReadout, {
51616
+ trpc,
51617
+ deviceId,
51618
+ layout: "compact"
51619
+ }) : null]
50879
51620
  });
50880
51621
  }
50881
51622
  function AcInlineControl({ trpc, deviceId, features, optimistic }) {
@@ -51040,6 +51781,10 @@ var CONTROL_KIND_COMPONENTS = {
51040
51781
  InlineControl: PetFeederInlineControl,
51041
51782
  HeroCard: PetFeederHeroCard
51042
51783
  },
51784
+ dishwasher: {
51785
+ InlineControl: DishwasherInlineControl,
51786
+ HeroCard: DishwasherHeroCard
51787
+ },
51043
51788
  script: {
51044
51789
  InlineControl: ScriptInlineControl,
51045
51790
  HeroCard: ScriptHeroCard
@@ -51116,7 +51861,8 @@ var CONTROL_FILLS = {
51116
51861
  [_camstack_types.DeviceType.Weather]: false,
51117
51862
  [_camstack_types.DeviceType.Container]: false,
51118
51863
  [_camstack_types.DeviceType.Image]: false,
51119
- [_camstack_types.DeviceType.PetFeeder]: false
51864
+ [_camstack_types.DeviceType.PetFeeder]: false,
51865
+ [_camstack_types.DeviceType.Dishwasher]: false
51120
51866
  };
51121
51867
  var DEVICE_TYPE_VALUES = new Set(Object.values(_camstack_types.DeviceType));
51122
51868
  /**
@@ -63964,6 +64710,7 @@ exports.Hammer = Hammer;
63964
64710
  exports.Hand = Hand;
63965
64711
  exports.HardDrive = HardDrive;
63966
64712
  exports.Hash = Hash;
64713
+ exports.Heater = Heater;
63967
64714
  exports.HlsVideo = HlsVideo;
63968
64715
  exports.House = House;
63969
64716
  exports.HoverZoomImage = HoverZoomImage;
@@ -64239,6 +64986,7 @@ exports.ThemeProvider = require_theme_index.ThemeProvider$1;
64239
64986
  exports.Thermometer = Thermometer;
64240
64987
  exports.ThermostatHeroCard = ThermostatHeroCard;
64241
64988
  exports.ThermostatInlineControl = ThermostatInlineControl;
64989
+ exports.Timer = Timer;
64242
64990
  exports.TimezoneSelector = TimezoneSelector;
64243
64991
  exports.ToggleLeft = ToggleLeft;
64244
64992
  exports.ToggleRight = ToggleRight;
@@ -64919,6 +65667,19 @@ exports.useDeviceStateSlice$1 = useDeviceStateSlice;
64919
65667
  exports.useDeviceStatusGetStatus = useDeviceStatusGetStatus;
64920
65668
  exports.useDeviceWebrtc = useDeviceWebrtc;
64921
65669
  exports.useDevices = useDevices;
65670
+ exports.useDishwasherGetStatus = useDishwasherGetStatus;
65671
+ exports.useDishwasherPause = useDishwasherPause;
65672
+ exports.useDishwasherSetChildLock = useDishwasherSetChildLock;
65673
+ exports.useDishwasherSetExtraDry = useDishwasherSetExtraDry;
65674
+ exports.useDishwasherSetHalfLoad = useDishwasherSetHalfLoad;
65675
+ exports.useDishwasherSetHygienePlus = useDishwasherSetHygienePlus;
65676
+ exports.useDishwasherSetIntensiveZone = useDishwasherSetIntensiveZone;
65677
+ exports.useDishwasherSetOption = useDishwasherSetOption;
65678
+ exports.useDishwasherSetPower = useDishwasherSetPower;
65679
+ exports.useDishwasherSetProgram = useDishwasherSetProgram;
65680
+ exports.useDishwasherSetVarioSpeedPlus = useDishwasherSetVarioSpeedPlus;
65681
+ exports.useDishwasherStart = useDishwasherStart;
65682
+ exports.useDishwasherStop = useDishwasherStop;
64922
65683
  exports.useDisplayIconResolver = useDisplayIconResolver;
64923
65684
  exports.useDoorbellEvents = useDoorbellEvents;
64924
65685
  exports.useDoorbellGetStatus = useDoorbellGetStatus;
@@ -64951,6 +65712,7 @@ exports.useFanControlSetPreset = useFanControlSetPreset;
64951
65712
  exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
64952
65713
  exports.useFloodGetStatus = useFloodGetStatus;
64953
65714
  exports.useGasGetStatus = useGasGetStatus;
65715
+ exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
64954
65716
  exports.useHumidifierGetStatus = useHumidifierGetStatus;
64955
65717
  exports.useHumidifierSetMode = useHumidifierSetMode;
64956
65718
  exports.useHumidifierSetOn = useHumidifierSetOn;