@camstack/ui-library 1.2.299 → 1.2.300

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,24 @@ 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.setOption.useMutation`. */
11030
+ var useDishwasherSetOption = trpc.dishwasher.setOption.useMutation;
11031
+ /** Generated alias around `trpc.dishwasher.getStatus.useQuery`. */
11032
+ var useDishwasherGetStatus = trpc.dishwasher.getStatus.useQuery;
10934
11033
  /** Generated alias around `trpc.doorbell.getStatus.useQuery`. */
10935
11034
  var useDoorbellGetStatus = trpc.doorbell.getStatus.useQuery;
10936
11035
  /** Generated alias around `trpc.enumSensor.getStatus.useQuery`. */
@@ -10985,6 +11084,8 @@ var useFeatureProbeGetStatus = trpc.featureProbe.getStatus.useQuery;
10985
11084
  var useFloodGetStatus = trpc.flood.getStatus.useQuery;
10986
11085
  /** Generated alias around `trpc.gas.getStatus.useQuery`. */
10987
11086
  var useGasGetStatus = trpc.gas.getStatus.useQuery;
11087
+ /** Generated alias around `trpc.heatingValve.getStatus.useQuery`. */
11088
+ var useHeatingValveGetStatus = trpc.heatingValve.getStatus.useQuery;
10988
11089
  /** Generated alias around `trpc.humidifier.setOn.useMutation`. */
10989
11090
  var useHumidifierSetOn = trpc.humidifier.setOn.useMutation;
10990
11091
  /** Generated alias around `trpc.humidifier.setTargetHumidity.useMutation`. */
@@ -28815,7 +28916,7 @@ function AddonActionSelectField({ field, value, values, onChange, disabled, onAc
28815
28916
  })()
28816
28917
  });
28817
28918
  }
28818
- function AddonActionButtonField({ field, values, disabled, onAction }) {
28919
+ function AddonActionButtonField({ field, values, disabled, onAction, onChange, onChangeMany }) {
28819
28920
  const t = useUiText();
28820
28921
  const [loading, setLoading] = (0, react$1.useState)(false);
28821
28922
  const [status, setStatus] = (0, react$1.useState)(null);
@@ -28847,6 +28948,17 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
28847
28948
  try {
28848
28949
  const result = await onAction(field.action, field.key, params);
28849
28950
  const resultRecord = typeof result === "object" && result !== null ? result : null;
28951
+ if (resultRecord !== null && field.assignResult && (onChange || onChangeMany)) {
28952
+ const patch = {};
28953
+ for (const [formKey, resultKey] of Object.entries(field.assignResult)) {
28954
+ const assigned = resultRecord[resultKey];
28955
+ if (typeof assigned === "string") patch[formKey] = assigned;
28956
+ }
28957
+ if (Object.keys(patch).length > 0) {
28958
+ if (onChangeMany) onChangeMany(patch);
28959
+ else if (onChange) for (const [formKey, assigned] of Object.entries(patch)) onChange(formKey, assigned);
28960
+ }
28961
+ }
28850
28962
  setStatus({
28851
28963
  kind: "ok",
28852
28964
  text: field.successMessage ? field.successMessage.replace(/\{(\w+)\}/g, (_, k) => {
@@ -29195,7 +29307,7 @@ function isFieldVisible(field, values, allFields) {
29195
29307
  if (condition.notIncludes !== void 0) return !Array.isArray(condVal) || !condVal.includes(condition.notIncludes);
29196
29308
  return true;
29197
29309
  }
29198
- function FormField({ field, values, onChange, disabled, translationFn, onTestField, externalProbe, allFields, onAction }) {
29310
+ function FormField({ field, values, onChange, onChangeMany, disabled, translationFn, onTestField, externalProbe, allFields, onAction }) {
29199
29311
  if (hasMultiple(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultipleFieldWrapper, {
29200
29312
  field,
29201
29313
  values,
@@ -29431,7 +29543,9 @@ function FormField({ field, values, onChange, disabled, translationFn, onTestFie
29431
29543
  field,
29432
29544
  values,
29433
29545
  disabled,
29434
- onAction
29546
+ onAction,
29547
+ onChange,
29548
+ onChangeMany
29435
29549
  });
29436
29550
  case "device-action-button": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceActionButtonField, {
29437
29551
  field,
@@ -42914,6 +43028,19 @@ function FanPanel({ trpc, deviceId, features, layout, optimistic }) {
42914
43028
  }
42915
43029
  //#endregion
42916
43030
  //#region src/composites/device-controls/climate-panel.tsx
43031
+ /** Step size for target ± buttons; defaults to 1 °C when unset or invalid. */
43032
+ function resolveClimateTempStep(tempStep) {
43033
+ if (typeof tempStep === "number" && Number.isFinite(tempStep) && tempStep > 0) return tempStep;
43034
+ return 1;
43035
+ }
43036
+ function nextClimateTarget(current, deltaSteps, display) {
43037
+ let next = current + deltaSteps * resolveClimateTempStep(display.tempStep);
43038
+ const min = display.minTemp;
43039
+ const max = display.maxTemp;
43040
+ if (typeof min === "number" && Number.isFinite(min)) next = Math.max(next, min);
43041
+ if (typeof max === "number" && Number.isFinite(max)) next = Math.min(next, max);
43042
+ return next;
43043
+ }
42917
43044
  /**
42918
43045
  * Exhaustive colour map — one entry per HvacMode. Build will fail if a
42919
43046
  * mode is added to the enum and not reflected here (Record<HvacMode, ...>
@@ -42994,7 +43121,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
42994
43121
  onClick: (e) => {
42995
43122
  e.stopPropagation();
42996
43123
  if (!busy && display.target !== null) {
42997
- const next = display.target - 1;
43124
+ const next = nextClimateTarget(display.target, -1, display);
42998
43125
  applyOptimistic({ target: next });
42999
43126
  cap.setTarget({ target: next });
43000
43127
  }
@@ -43008,7 +43135,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43008
43135
  onClick: (e) => {
43009
43136
  e.stopPropagation();
43010
43137
  if (!busy && display.target !== null) {
43011
- const next = display.target + 1;
43138
+ const next = nextClimateTarget(display.target, 1, display);
43012
43139
  applyOptimistic({ target: next });
43013
43140
  cap.setTarget({ target: next });
43014
43141
  }
@@ -43063,7 +43190,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43063
43190
  onClick: (e) => {
43064
43191
  e.stopPropagation();
43065
43192
  if (!busy && display.target !== null) {
43066
- const next = display.target - 1;
43193
+ const next = nextClimateTarget(display.target, -1, display);
43067
43194
  applyOptimistic({ target: next });
43068
43195
  cap.setTarget({ target: next });
43069
43196
  }
@@ -43078,7 +43205,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43078
43205
  onClick: (e) => {
43079
43206
  e.stopPropagation();
43080
43207
  if (!busy && display.target !== null) {
43081
- const next = display.target + 1;
43208
+ const next = nextClimateTarget(display.target, 1, display);
43082
43209
  applyOptimistic({ target: next });
43083
43210
  cap.setTarget({ target: next });
43084
43211
  }
@@ -43736,7 +43863,8 @@ var CONTROL_CAP_NAMES = [
43736
43863
  "climate-control",
43737
43864
  "media-player",
43738
43865
  "control",
43739
- "pet-feeder"
43866
+ "pet-feeder",
43867
+ "dishwasher"
43740
43868
  ];
43741
43869
  //#endregion
43742
43870
  //#region src/composites/device-controls/popover-row-action.tsx
@@ -48547,7 +48675,405 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
48547
48675
  });
48548
48676
  }
48549
48677
  //#endregion
48678
+ //#region src/composites/device-controls/dishwasher-control.tsx
48679
+ /**
48680
+ * Dishwasher controls for a `DeviceType.Dishwasher` device.
48681
+ *
48682
+ * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
48683
+ * mirroring vacuum-control / pet-feeder patterns.
48684
+ */
48685
+ function dishwasherStateMeta(state, t = defaultUiText) {
48686
+ switch (state) {
48687
+ case "running": return {
48688
+ label: t("uiLibrary.dishwasher.running", "Running"),
48689
+ tone: "text-primary"
48690
+ };
48691
+ case "paused": return {
48692
+ label: t("uiLibrary.dishwasher.paused", "Paused"),
48693
+ tone: "text-warning"
48694
+ };
48695
+ case "finished": return {
48696
+ label: t("uiLibrary.dishwasher.finished", "Finished"),
48697
+ tone: "text-success"
48698
+ };
48699
+ case "error": return {
48700
+ label: t("uiLibrary.dishwasher.error", "Error"),
48701
+ tone: "text-destructive"
48702
+ };
48703
+ case "unavailable": return {
48704
+ label: t("uiLibrary.dishwasher.unavailable", "Unavailable"),
48705
+ tone: "text-foreground-subtle"
48706
+ };
48707
+ case "idle": return {
48708
+ label: t("uiLibrary.dishwasher.idle", "Idle"),
48709
+ tone: "text-foreground-subtle"
48710
+ };
48711
+ case void 0: return {
48712
+ label: t("uiLibrary.dishwasher.unknown", "Unknown"),
48713
+ tone: "text-foreground-subtle"
48714
+ };
48715
+ }
48716
+ }
48717
+ function consumableTone(level) {
48718
+ switch (level) {
48719
+ case "ok": return "text-success";
48720
+ case "low": return "text-warning";
48721
+ case "empty": return "text-destructive";
48722
+ case null: return "text-foreground-subtle";
48723
+ }
48724
+ }
48725
+ function useDishwasher(trpc, deviceId) {
48726
+ return useDeviceCapability({
48727
+ trpc,
48728
+ deviceId,
48729
+ cap: (d) => d.dishwasher,
48730
+ slice: (d) => d.state.dishwasher
48731
+ });
48732
+ }
48733
+ function DishwasherIconButton({ label, busy, active, onClick, children }) {
48734
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
48735
+ type: "button",
48736
+ "aria-label": label,
48737
+ title: label,
48738
+ disabled: busy,
48739
+ onClick: (e) => {
48740
+ e.stopPropagation();
48741
+ onClick();
48742
+ },
48743
+ 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"),
48744
+ children
48745
+ });
48746
+ }
48747
+ function ConsumableBadge({ label, level }) {
48748
+ if (level === null) return null;
48749
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48750
+ className: cn("text-xs font-medium capitalize", consumableTone(level)),
48751
+ children: [
48752
+ label,
48753
+ ": ",
48754
+ level
48755
+ ]
48756
+ });
48757
+ }
48758
+ function DishwasherGraphic({ state, progressPercent }) {
48759
+ const running = state === "running";
48760
+ const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
48761
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48762
+ "aria-hidden": "true",
48763
+ 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",
48764
+ style: { boxShadow: "inset 0 0 0 2px rgba(255,255,255,.06)" },
48765
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48766
+ className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
48767
+ style: { height: `${pct}%` }
48768
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48769
+ className: "absolute inset-0 flex items-center justify-center text-foreground-subtle",
48770
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Waves, { className: "h-10 w-10 opacity-70" })
48771
+ })]
48772
+ });
48773
+ }
48774
+ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48775
+ const t = useUiText();
48776
+ const { cap, slice, available } = useDishwasher(trpc, deviceId);
48777
+ const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48778
+ if (cap === void 0) return null;
48779
+ if (display === void 0) return null;
48780
+ const busy = !available;
48781
+ const meta = dishwasherStateMeta(display.state, t);
48782
+ const running = display.state === "running";
48783
+ const paused = display.state === "paused";
48784
+ const press = (fn, patch) => {
48785
+ if (busy) return;
48786
+ applyOptimistic(patch);
48787
+ fn();
48788
+ };
48789
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48790
+ className: "inline-flex items-center gap-1",
48791
+ children: [
48792
+ display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48793
+ className: "inline-flex text-warning",
48794
+ title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
48795
+ "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
48796
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
48797
+ }),
48798
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48799
+ className: cn("text-xs font-medium tabular-nums", meta.tone),
48800
+ children: meta.label
48801
+ }),
48802
+ display.remainingMinutes != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48803
+ className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums",
48804
+ children: [
48805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }),
48806
+ display.remainingMinutes,
48807
+ "m"
48808
+ ]
48809
+ }),
48810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48811
+ label: t("uiLibrary.dishwasher.start", "Start"),
48812
+ busy: busy || !display.remoteStartAllowed,
48813
+ active: running,
48814
+ onClick: () => press(() => cap.start({}), {
48815
+ state: "running",
48816
+ remoteStartAllowed: false
48817
+ }),
48818
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
48819
+ }),
48820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48821
+ label: t("uiLibrary.dishwasher.pause", "Pause"),
48822
+ busy: busy || !running,
48823
+ active: paused,
48824
+ onClick: () => press(() => cap.pause({}), { state: "paused" }),
48825
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
48826
+ })
48827
+ ]
48828
+ });
48829
+ const programs = display.availablePrograms;
48830
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48831
+ className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
48832
+ children: [
48833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48834
+ className: "flex items-start gap-4",
48835
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
48836
+ state: display.state,
48837
+ progressPercent: display.progressPercent
48838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48839
+ className: "flex min-w-0 flex-1 flex-col gap-2",
48840
+ children: [
48841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48842
+ className: "flex flex-wrap items-center gap-2",
48843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48844
+ className: cn("text-3xl font-semibold", meta.tone),
48845
+ children: meta.label
48846
+ }), !display.powerOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48847
+ className: "text-xs font-medium text-foreground-subtle",
48848
+ children: t("uiLibrary.dishwasher.powerOff", "Power off")
48849
+ })]
48850
+ }),
48851
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48852
+ className: "text-sm text-foreground-subtle",
48853
+ children: display.program
48854
+ }),
48855
+ display.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48856
+ className: "flex items-center gap-2",
48857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48858
+ className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
48859
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48860
+ className: "h-full bg-primary transition-[width] duration-500",
48861
+ style: { width: `${Math.max(0, Math.min(100, display.progressPercent))}%` }
48862
+ })
48863
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48864
+ className: "tabular-nums text-xs text-foreground-subtle",
48865
+ children: [Math.round(display.progressPercent), "%"]
48866
+ })]
48867
+ }),
48868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48869
+ className: "flex flex-wrap gap-3",
48870
+ children: [
48871
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableBadge, {
48872
+ label: t("uiLibrary.dishwasher.salt", "Salt"),
48873
+ level: display.saltLevel
48874
+ }),
48875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableBadge, {
48876
+ label: t("uiLibrary.dishwasher.rinseAid", "Rinse aid"),
48877
+ level: display.rinseAidLevel
48878
+ }),
48879
+ display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48880
+ className: "inline-flex items-center gap-1 text-xs text-warning",
48881
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
48882
+ })
48883
+ ]
48884
+ }),
48885
+ display.errorCode != null && display.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48886
+ className: "inline-flex items-center gap-1 text-xs text-destructive",
48887
+ 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 }) })]
48888
+ }),
48889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48890
+ className: "flex flex-wrap items-center gap-2",
48891
+ children: [
48892
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48893
+ label: t("uiLibrary.dishwasher.start", "Start"),
48894
+ busy: busy || !display.remoteStartAllowed,
48895
+ active: running,
48896
+ onClick: () => press(() => cap.start({}), {
48897
+ state: "running",
48898
+ remoteStartAllowed: false
48899
+ }),
48900
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
48901
+ }),
48902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48903
+ label: t("uiLibrary.dishwasher.pause", "Pause"),
48904
+ busy: busy || !running,
48905
+ active: paused,
48906
+ onClick: () => press(() => cap.pause({}), { state: "paused" }),
48907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
48908
+ }),
48909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48910
+ label: t("uiLibrary.dishwasher.stop", "Stop"),
48911
+ busy,
48912
+ active: false,
48913
+ onClick: () => press(() => cap.stop({}), {
48914
+ state: "idle",
48915
+ remainingMinutes: null
48916
+ }),
48917
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
48918
+ }),
48919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48920
+ label: t("uiLibrary.dishwasher.power", "Power"),
48921
+ busy,
48922
+ active: display.powerOn,
48923
+ onClick: () => {
48924
+ const next = !display.powerOn;
48925
+ press(() => cap.setPower({ on: next }), { powerOn: next });
48926
+ },
48927
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
48928
+ })
48929
+ ]
48930
+ })
48931
+ ]
48932
+ })]
48933
+ }),
48934
+ programs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48935
+ className: "flex items-center gap-2 text-xs text-foreground-subtle",
48936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48937
+ className: "w-16 shrink-0",
48938
+ children: t("uiLibrary.dishwasher.program", "Program")
48939
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
48940
+ "aria-label": t("uiLibrary.dishwasher.program", "Program"),
48941
+ value: display.program ?? "",
48942
+ disabled: busy,
48943
+ onChange: (e) => {
48944
+ const program = e.target.value;
48945
+ if (!program) return;
48946
+ applyOptimistic({ program });
48947
+ cap.setProgram({ program });
48948
+ },
48949
+ onClick: (e) => {
48950
+ e.stopPropagation();
48951
+ },
48952
+ className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
48953
+ children: [display.program == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48954
+ value: "",
48955
+ children: "—"
48956
+ }), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48957
+ value: p,
48958
+ children: p
48959
+ }, p))]
48960
+ })]
48961
+ }),
48962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48963
+ className: "flex flex-wrap gap-4 text-xs",
48964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
48965
+ label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
48966
+ checked: display.halfLoad,
48967
+ busy,
48968
+ onChange: (on) => {
48969
+ applyOptimistic({ halfLoad: on });
48970
+ cap.setHalfLoad({ on });
48971
+ }
48972
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
48973
+ label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
48974
+ checked: display.hygienePlus,
48975
+ busy,
48976
+ onChange: (on) => {
48977
+ applyOptimistic({ hygienePlus: on });
48978
+ cap.setHygienePlus({ on });
48979
+ }
48980
+ })]
48981
+ })
48982
+ ]
48983
+ });
48984
+ }
48985
+ function ToggleRow({ label, checked, busy, onChange }) {
48986
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48987
+ className: "inline-flex items-center gap-2 cursor-pointer",
48988
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
48989
+ type: "checkbox",
48990
+ checked,
48991
+ disabled: busy,
48992
+ onChange: (e) => {
48993
+ onChange(e.target.checked);
48994
+ },
48995
+ onClick: (e) => {
48996
+ e.stopPropagation();
48997
+ },
48998
+ className: "rounded border-border"
48999
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
49000
+ });
49001
+ }
49002
+ function DishwasherInlineControl(props) {
49003
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
49004
+ ...props,
49005
+ layout: "compact"
49006
+ });
49007
+ }
49008
+ function DishwasherHeroCard(props) {
49009
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherPanel, {
49010
+ ...props,
49011
+ layout: "full"
49012
+ });
49013
+ }
49014
+ //#endregion
49015
+ //#region src/composites/device-controls/heating-valve-readout.tsx
49016
+ function formatHeatingValvePosition(position) {
49017
+ return position !== null ? `${position}%` : "—";
49018
+ }
49019
+ function HeatingValveReadout({ trpc, deviceId, layout }) {
49020
+ const t = useUiText();
49021
+ const { cap, slice } = useDeviceCapability({
49022
+ trpc,
49023
+ deviceId,
49024
+ cap: (d) => d.heatingValve,
49025
+ slice: (d) => d.state.heatingValve
49026
+ });
49027
+ if (cap === void 0 || slice === void 0) return null;
49028
+ const label = t("uiLibrary.heatingValveReadout.valve", "Valve");
49029
+ const value = formatHeatingValvePosition(slice.position);
49030
+ if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49031
+ className: "inline-flex items-center text-xs tabular-nums text-foreground-subtle",
49032
+ "aria-label": `${label} ${value}`,
49033
+ children: value
49034
+ });
49035
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49036
+ className: "flex items-center gap-2 text-xs tabular-nums text-foreground-subtle",
49037
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49038
+ className: "font-medium text-foreground",
49039
+ children: value
49040
+ })]
49041
+ });
49042
+ }
49043
+ //#endregion
48550
49044
  //#region src/composites/device-controls/ac-panel.tsx
49045
+ /** Modes that mean the device can cool, dry, or move air — an air conditioner. */
49046
+ var COOLING_MODES = new Set([
49047
+ "cool",
49048
+ "heat_cool",
49049
+ "dry",
49050
+ "fan_only"
49051
+ ]);
49052
+ /** A heater offers heat (and maybe auto/off) and none of the cooling modes. */
49053
+ function climateOffersCooling(modes) {
49054
+ return modes.some((mode) => COOLING_MODES.has(mode));
49055
+ }
49056
+ /** Setpoint track bounds. Absent when the integration did not report both ends. */
49057
+ function climateSetpointBounds(status) {
49058
+ const min = status.minTemp;
49059
+ const max = status.maxTemp;
49060
+ if (typeof min !== "number" || typeof max !== "number") return null;
49061
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return null;
49062
+ return {
49063
+ min,
49064
+ max
49065
+ };
49066
+ }
49067
+ /** Map a 0–1 drag fraction onto the setpoint, snapped to `tempStep`. */
49068
+ function climateTargetFromFraction(fraction, bounds, tempStep) {
49069
+ const step = resolveClimateTempStep(tempStep);
49070
+ const clamped = Math.min(1, Math.max(0, fraction));
49071
+ const raw = bounds.min + clamped * (bounds.max - bounds.min);
49072
+ const steps = Math.round((raw - bounds.min) / step);
49073
+ const snapped = bounds.min + steps * step;
49074
+ const rounded = Math.round(snapped * 1e3) / 1e3;
49075
+ return Math.min(bounds.max, Math.max(bounds.min, rounded));
49076
+ }
48551
49077
  /** AC-appropriate short mode labels. Exhaustive over HvacMode. */
48552
49078
  function modeLabel(t, mode) {
48553
49079
  switch (mode) {
@@ -48581,9 +49107,6 @@ var MODE_ACCENT = {
48581
49107
  dry: "#22d3ee",
48582
49108
  off: "#a1a1aa"
48583
49109
  };
48584
- /** Practical AC setpoint range (Celsius) for the target range track. */
48585
- var TARGET_MIN = 16;
48586
- var TARGET_MAX = 30;
48587
49110
  /** Title-case a snake/lower token for a selector option ('medium_low' → 'Medium
48588
49111
  * low', 'turbo' → 'Turbo'). Shared by the fan + preset selectors. */
48589
49112
  function fanLabel(token) {
@@ -48670,7 +49193,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48670
49193
  onClick: (e) => {
48671
49194
  e.stopPropagation();
48672
49195
  if (!busy && display.target !== null) {
48673
- const next = display.target - 1;
49196
+ const next = nextClimateTarget(display.target, -1, display);
48674
49197
  applyOptimistic({ target: next });
48675
49198
  cap.setTarget({ target: next });
48676
49199
  }
@@ -48684,7 +49207,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48684
49207
  onClick: (e) => {
48685
49208
  e.stopPropagation();
48686
49209
  if (!busy && display.target !== null) {
48687
- const next = display.target + 1;
49210
+ const next = nextClimateTarget(display.target, 1, display);
48688
49211
  applyOptimistic({ target: next });
48689
49212
  cap.setTarget({ target: next });
48690
49213
  }
@@ -48714,6 +49237,8 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48714
49237
  });
48715
49238
  const accent = MODE_ACCENT[display.mode];
48716
49239
  const on = display.mode !== "off";
49240
+ const heatingOnly = !climateOffersCooling(display.availableModes);
49241
+ const setpointBounds = climateSetpointBounds(display);
48717
49242
  const CurrentPresetIcon = presetIcon(display.preset);
48718
49243
  const boost = display.preset === "turbo";
48719
49244
  const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
@@ -48730,11 +49255,12 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48730
49255
  children: [
48731
49256
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48732
49257
  className: "pointer-events-none absolute inset-0 opacity-60",
48733
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
49258
+ children: heatingOnly && !on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RadiatorFins, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AirflowViz, {
48734
49259
  mode: display.mode,
48735
49260
  accent,
48736
49261
  periodSec: period,
48737
- swaying
49262
+ swaying,
49263
+ forceRising: heatingOnly
48738
49264
  })
48739
49265
  }),
48740
49266
  /* @__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 +49278,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48752
49278
  children: [
48753
49279
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
48754
49280
  mode: display.mode,
49281
+ heatingOnly,
48755
49282
  className: "h-3.5 w-3.5"
48756
49283
  }),
48757
49284
  modeLabel(t, display.mode),
@@ -48778,7 +49305,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48778
49305
  disabled: busy,
48779
49306
  onClick: () => {
48780
49307
  if (busy || display.target === null) return;
48781
- const next = display.target - 1;
49308
+ const next = nextClimateTarget(display.target, -1, display);
48782
49309
  applyOptimistic({ target: next });
48783
49310
  cap.setTarget({ target: next });
48784
49311
  }
@@ -48800,19 +49327,29 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48800
49327
  disabled: busy,
48801
49328
  onClick: () => {
48802
49329
  if (busy || display.target === null) return;
48803
- const next = display.target + 1;
49330
+ const next = nextClimateTarget(display.target, 1, display);
48804
49331
  applyOptimistic({ target: next });
48805
49332
  cap.setTarget({ target: next });
48806
49333
  }
48807
49334
  })
48808
49335
  ]
48809
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48810
- className: "relative z-10 w-36",
49336
+ }), setpointBounds !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49337
+ className: "relative z-10 w-full px-1",
48811
49338
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeTrack, {
48812
49339
  value: display.target,
48813
- min: TARGET_MIN,
48814
- max: TARGET_MAX,
48815
- accent
49340
+ min: setpointBounds.min,
49341
+ max: setpointBounds.max,
49342
+ step: display.tempStep,
49343
+ accent,
49344
+ disabled: busy,
49345
+ label: t("uiLibrary.acPanel.setpoint", "Setpoint"),
49346
+ onPreview: (next) => {
49347
+ applyOptimistic({ target: next });
49348
+ },
49349
+ onCommit: (next) => {
49350
+ applyOptimistic({ target: next });
49351
+ cap.setTarget({ target: next });
49352
+ }
48816
49353
  })
48817
49354
  })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48818
49355
  className: "relative z-10 flex items-baseline gap-0.5",
@@ -48836,6 +49373,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48836
49373
  className: "flex",
48837
49374
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
48838
49375
  mode: display.mode,
49376
+ heatingOnly,
48839
49377
  className: "h-4 w-4"
48840
49378
  })
48841
49379
  }),
@@ -48919,9 +49457,8 @@ function Field({ label, children }) {
48919
49457
  }), children]
48920
49458
  });
48921
49459
  }
48922
- function ModeIcon({ mode, className }) {
48923
- const Icon = MODE_ICON[mode];
48924
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
49460
+ function ModeIcon({ mode, heatingOnly = false, className }) {
49461
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(heatingOnly && mode === "heat" ? Heater : MODE_ICON[mode], {
48925
49462
  className,
48926
49463
  "aria-hidden": true
48927
49464
  });
@@ -48971,11 +49508,89 @@ function RoundStep({ label, symbol, disabled, onClick }) {
48971
49508
  children: symbol
48972
49509
  });
48973
49510
  }
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}%`;
49511
+ /** Setpoint track. Drag previews; the command is sent when the pointer is released. */
49512
+ function RangeTrack({ value, min, max, step, accent, disabled, label, onPreview, onCommit }) {
49513
+ const trackRef = (0, react$1.useRef)(null);
49514
+ const draggingRef = (0, react$1.useRef)(false);
49515
+ const [draft, setDraft] = (0, react$1.useState)(null);
49516
+ const shown = draft ?? value;
49517
+ const pct = `${Math.min(1, Math.max(0, (shown - min) / (max - min))) * 100}%`;
49518
+ const tempAt = (clientX) => {
49519
+ const el = trackRef.current;
49520
+ if (el === null) return value;
49521
+ const rect = el.getBoundingClientRect();
49522
+ if (rect.width <= 0) return value;
49523
+ return climateTargetFromFraction((clientX - rect.left) / rect.width, {
49524
+ min,
49525
+ max
49526
+ }, step);
49527
+ };
49528
+ const onPointerDown = (event) => {
49529
+ if (disabled) return;
49530
+ event.stopPropagation();
49531
+ event.preventDefault();
49532
+ draggingRef.current = true;
49533
+ if (typeof event.currentTarget.setPointerCapture === "function") event.currentTarget.setPointerCapture(event.pointerId);
49534
+ const next = tempAt(event.clientX);
49535
+ setDraft(next);
49536
+ onPreview(next);
49537
+ };
49538
+ const onPointerMove = (event) => {
49539
+ if (disabled || !draggingRef.current) return;
49540
+ event.stopPropagation();
49541
+ const next = tempAt(event.clientX);
49542
+ setDraft(next);
49543
+ onPreview(next);
49544
+ };
49545
+ const endDrag = (event) => {
49546
+ if (!draggingRef.current) return;
49547
+ event.stopPropagation();
49548
+ draggingRef.current = false;
49549
+ const next = tempAt(event.clientX);
49550
+ setDraft(null);
49551
+ if (event.currentTarget.hasPointerCapture?.(event.pointerId) === true) event.currentTarget.releasePointerCapture(event.pointerId);
49552
+ onCommit(next);
49553
+ };
49554
+ const onKeyDown = (event) => {
49555
+ if (disabled) return;
49556
+ const current = draft ?? value;
49557
+ if (event.key === "ArrowRight" || event.key === "ArrowUp") {
49558
+ event.preventDefault();
49559
+ onCommit(nextClimateTarget(current, 1, {
49560
+ tempStep: step,
49561
+ minTemp: min,
49562
+ maxTemp: max
49563
+ }));
49564
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
49565
+ event.preventDefault();
49566
+ onCommit(nextClimateTarget(current, -1, {
49567
+ tempStep: step,
49568
+ minTemp: min,
49569
+ maxTemp: max
49570
+ }));
49571
+ } else if (event.key === "Home") {
49572
+ event.preventDefault();
49573
+ onCommit(min);
49574
+ } else if (event.key === "End") {
49575
+ event.preventDefault();
49576
+ onCommit(max);
49577
+ }
49578
+ };
48977
49579
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48978
- className: "mt-2 w-full",
49580
+ ref: trackRef,
49581
+ role: "slider",
49582
+ "aria-label": label,
49583
+ "aria-valuemin": min,
49584
+ "aria-valuemax": max,
49585
+ "aria-valuenow": shown,
49586
+ "aria-disabled": disabled,
49587
+ tabIndex: disabled ? -1 : 0,
49588
+ onPointerDown,
49589
+ onPointerMove,
49590
+ onPointerUp: endDrag,
49591
+ onPointerCancel: endDrag,
49592
+ onKeyDown,
49593
+ className: cn("mt-1 w-full touch-none select-none py-2", disabled ? "cursor-not-allowed opacity-40" : "cursor-ew-resize"),
48979
49594
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48980
49595
  className: "relative h-1.5 w-full rounded-full bg-surface-subtle",
48981
49596
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -48985,7 +49600,7 @@ function RangeTrack({ value, min, max, accent }) {
48985
49600
  background: accent
48986
49601
  }
48987
49602
  }), /* @__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",
49603
+ 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
49604
  style: {
48990
49605
  left: pct,
48991
49606
  background: accent
@@ -49002,7 +49617,7 @@ function RangeTrack({ value, min, max, accent }) {
49002
49617
  * vent; every active mode renders drifting particles that FALL (cool/dry/auto)
49003
49618
  * or RISE (heat), with a horizontal sweep overlaid when a swing axis is active.
49004
49619
  */
49005
- function AirflowViz({ mode, accent, periodSec, swaying }) {
49620
+ function AirflowViz({ mode, accent, periodSec, swaying, forceRising = false }) {
49006
49621
  const columns = [
49007
49622
  0,
49008
49623
  1,
@@ -49011,7 +49626,7 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49011
49626
  4,
49012
49627
  5
49013
49628
  ];
49014
- const rising = mode === "heat";
49629
+ const rising = forceRising || mode === "heat";
49015
49630
  const isDroplet = mode === "dry";
49016
49631
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49017
49632
  "aria-hidden": true,
@@ -49027,6 +49642,7 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49027
49642
  size: 56
49028
49643
  })
49029
49644
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49645
+ "data-testid": forceRising ? "climate-heat-rise" : void 0,
49030
49646
  className: cn("acv-sway-wrap absolute inset-0", swaying && "acv-sway"),
49031
49647
  style: swaying ? { animation: "acv-sway 2.6s ease-in-out infinite" } : void 0,
49032
49648
  children: columns.map((i) => {
@@ -49049,6 +49665,23 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49049
49665
  })
49050
49666
  });
49051
49667
  }
49668
+ /** Still radiator fins — the off state of a heating-only climate. */
49669
+ function RadiatorFins() {
49670
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49671
+ "data-testid": "climate-radiator",
49672
+ "aria-hidden": true,
49673
+ className: "flex h-full items-end justify-center gap-1.5 px-8 pb-3 opacity-40",
49674
+ children: [
49675
+ 0,
49676
+ 1,
49677
+ 2,
49678
+ 3,
49679
+ 4,
49680
+ 5,
49681
+ 6
49682
+ ].map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-10 w-1.5 rounded-t-full bg-foreground-subtle" }, i))
49683
+ });
49684
+ }
49052
49685
  /** Big spinning fan for `fan_only` mode. */
49053
49686
  function FanBlades({ color, periodSec, size }) {
49054
49687
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
@@ -49215,16 +49848,33 @@ var MODE_ARC_COLOR = {
49215
49848
  */
49216
49849
  function ThermostatHeroCard({ trpc, deviceId, features: _features, optimistic }) {
49217
49850
  const t = useUiText();
49218
- const { cap, slice, available } = useDeviceCapability({
49851
+ const climate = useDeviceCapability({
49219
49852
  trpc,
49220
49853
  deviceId,
49221
49854
  cap: (d) => d.climateControl,
49222
49855
  slice: (d) => d.state.climateControl
49223
49856
  });
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;
49857
+ const valve = useDeviceCapability({
49858
+ trpc,
49859
+ deviceId,
49860
+ cap: (d) => d.heatingValve,
49861
+ slice: (d) => d.state.heatingValve
49862
+ });
49863
+ const { display, applyOptimistic } = useOptimisticSlice(climate.slice, { enabled: optimistic ?? true });
49864
+ const hasClimate = climate.cap !== void 0 && display !== void 0;
49865
+ const hasValve = valve.cap !== void 0;
49866
+ if (!hasClimate && !hasValve) return null;
49867
+ if (!hasClimate && hasValve) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49868
+ className: "flex flex-col items-stretch gap-3 rounded-lg border border-border bg-surface p-4",
49869
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatingValveReadout, {
49870
+ trpc,
49871
+ deviceId,
49872
+ layout: "full"
49873
+ })
49874
+ });
49875
+ if (climate.cap === void 0 || display === void 0) return null;
49876
+ const cap = climate.cap;
49877
+ const busy = !climate.available;
49228
49878
  const color = MODE_ARC_COLOR[display.mode];
49229
49879
  const isRange = display.mode === "heat_cool" || display.targetLow !== null && display.targetHigh !== null;
49230
49880
  const min = DEFAULT_TEMP_MIN;
@@ -49235,132 +49885,140 @@ function ThermostatHeroCard({ trpc, deviceId, features: _features, optimistic })
49235
49885
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49236
49886
  "data-mode": display.mode,
49237
49887
  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, {
49888
+ children: [
49889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49890
+ className: "flex flex-col items-center gap-3",
49891
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49892
+ className: "relative",
49893
+ style: {
49894
+ width: GAUGE_SIZE,
49895
+ height: GAUGE_SIZE
49896
+ },
49897
+ children: [
49898
+ isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49899
+ mode: "band",
49900
+ band: {
49901
+ from: fractionLow,
49902
+ to: fractionHigh
49903
+ },
49904
+ color
49905
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49906
+ mode: "fill",
49294
49907
  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
- ]
49908
+ color
49909
+ }),
49910
+ isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49911
+ "data-testid": "arc-knob",
49912
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49913
+ fraction: fractionLow,
49914
+ color,
49915
+ knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49916
+ onDragFraction: (f) => {
49917
+ if (busy || display.targetHigh === null) return;
49918
+ const next = Math.min(fractionToTemp(f, min, max), display.targetHigh);
49919
+ applyOptimistic({ targetLow: next });
49920
+ cap.setTargetRange({
49921
+ targetLow: next,
49922
+ targetHigh: display.targetHigh
49923
+ });
49924
+ }
49330
49925
  })
49331
- ]
49926
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49927
+ "data-testid": "arc-knob",
49928
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49929
+ fraction: fractionHigh,
49930
+ color,
49931
+ knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49932
+ onDragFraction: (f) => {
49933
+ if (busy || display.targetLow === null) return;
49934
+ const next = Math.max(fractionToTemp(f, min, max), display.targetLow);
49935
+ applyOptimistic({ targetHigh: next });
49936
+ cap.setTargetRange({
49937
+ targetLow: display.targetLow,
49938
+ targetHigh: next
49939
+ });
49940
+ }
49941
+ })
49942
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49943
+ "data-testid": "arc-knob",
49944
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49945
+ fraction: singleFraction,
49946
+ color,
49947
+ knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49948
+ onDragFraction: (f) => {
49949
+ if (busy) return;
49950
+ const next = fractionToTemp(f, min, max);
49951
+ applyOptimistic({ target: next });
49952
+ cap.setTarget({ target: next });
49953
+ }
49954
+ })
49955
+ }),
49956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49957
+ className: "absolute inset-0 flex flex-col items-center justify-center gap-0.5 pointer-events-none",
49958
+ children: [
49959
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49960
+ "data-testid": "current-temp",
49961
+ className: "text-4xl font-bold tabular-nums text-foreground leading-none",
49962
+ children: [display.currentTemp !== null ? display.currentTemp : "—", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49963
+ className: "text-lg text-foreground-subtle font-normal ml-0.5",
49964
+ children: "°"
49965
+ })]
49966
+ }),
49967
+ !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49968
+ className: "text-xs font-semibold",
49969
+ style: { color },
49970
+ children: t("uiLibrary.thermostatHeroCard.setTo", "set {{value}}°", { value: display.target })
49971
+ }),
49972
+ isRange && display.targetLow !== null && display.targetHigh !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49973
+ className: "text-xs font-semibold",
49974
+ style: { color },
49975
+ children: [
49976
+ display.targetLow,
49977
+ "° – ",
49978
+ display.targetHigh,
49979
+ "°"
49980
+ ]
49981
+ })
49982
+ ]
49983
+ })
49984
+ ]
49985
+ }), !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49986
+ className: "w-[200px]",
49987
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stepper, {
49988
+ value: `${display.target}°`,
49989
+ onDelta: (d) => {
49990
+ if (busy || display.target === null) return;
49991
+ const next = display.target + d;
49992
+ applyOptimistic({ target: next });
49993
+ cap.setTarget({ target: next });
49994
+ }
49332
49995
  })
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 });
49996
+ })]
49997
+ }),
49998
+ hasValve ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatingValveReadout, {
49999
+ trpc,
50000
+ deviceId,
50001
+ layout: "full"
50002
+ }) : null,
50003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
50004
+ "aria-label": t("uiLibrary.thermostatHeroCard.mode", "Mode"),
50005
+ value: display.mode,
50006
+ disabled: busy,
50007
+ onChange: (e) => {
50008
+ e.stopPropagation();
50009
+ const mode = e.target.value;
50010
+ if (!busy && mode) {
50011
+ applyOptimistic({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
50012
+ cap.setMode({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
49343
50013
  }
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
- })]
50014
+ },
50015
+ className: cn("w-full rounded border border-border bg-surface px-3 py-1.5 text-sm", busy && "opacity-40 cursor-not-allowed"),
50016
+ children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
50017
+ value: m,
50018
+ children: m
50019
+ }, m))
50020
+ })
50021
+ ]
49364
50022
  });
49365
50023
  }
49366
50024
  //#endregion
@@ -50868,14 +51526,32 @@ function useClimateCapGuard(trpc, deviceId) {
50868
51526
  });
50869
51527
  return cap !== void 0;
50870
51528
  }
50871
- function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
50872
- if (!useClimateCapGuard(trpc, deviceId)) return null;
50873
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
51529
+ function useHeatingValveCapGuard(trpc, deviceId) {
51530
+ const { cap } = useDeviceCapability({
50874
51531
  trpc,
50875
51532
  deviceId,
50876
- features,
50877
- layout: "compact",
50878
- optimistic
51533
+ cap: (d) => d.heatingValve,
51534
+ slice: (d) => d.state.heatingValve
51535
+ });
51536
+ return cap !== void 0;
51537
+ }
51538
+ function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
51539
+ const hasClimate = useClimateCapGuard(trpc, deviceId);
51540
+ const hasValve = useHeatingValveCapGuard(trpc, deviceId);
51541
+ if (!hasClimate && !hasValve) return null;
51542
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
51543
+ className: "inline-flex items-center gap-2",
51544
+ children: [hasClimate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
51545
+ trpc,
51546
+ deviceId,
51547
+ features,
51548
+ layout: "compact",
51549
+ optimistic
51550
+ }) : null, hasValve ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeatingValveReadout, {
51551
+ trpc,
51552
+ deviceId,
51553
+ layout: "compact"
51554
+ }) : null]
50879
51555
  });
50880
51556
  }
50881
51557
  function AcInlineControl({ trpc, deviceId, features, optimistic }) {
@@ -51040,6 +51716,10 @@ var CONTROL_KIND_COMPONENTS = {
51040
51716
  InlineControl: PetFeederInlineControl,
51041
51717
  HeroCard: PetFeederHeroCard
51042
51718
  },
51719
+ dishwasher: {
51720
+ InlineControl: DishwasherInlineControl,
51721
+ HeroCard: DishwasherHeroCard
51722
+ },
51043
51723
  script: {
51044
51724
  InlineControl: ScriptInlineControl,
51045
51725
  HeroCard: ScriptHeroCard
@@ -51116,7 +51796,8 @@ var CONTROL_FILLS = {
51116
51796
  [_camstack_types.DeviceType.Weather]: false,
51117
51797
  [_camstack_types.DeviceType.Container]: false,
51118
51798
  [_camstack_types.DeviceType.Image]: false,
51119
- [_camstack_types.DeviceType.PetFeeder]: false
51799
+ [_camstack_types.DeviceType.PetFeeder]: false,
51800
+ [_camstack_types.DeviceType.Dishwasher]: false
51120
51801
  };
51121
51802
  var DEVICE_TYPE_VALUES = new Set(Object.values(_camstack_types.DeviceType));
51122
51803
  /**
@@ -63964,6 +64645,7 @@ exports.Hammer = Hammer;
63964
64645
  exports.Hand = Hand;
63965
64646
  exports.HardDrive = HardDrive;
63966
64647
  exports.Hash = Hash;
64648
+ exports.Heater = Heater;
63967
64649
  exports.HlsVideo = HlsVideo;
63968
64650
  exports.House = House;
63969
64651
  exports.HoverZoomImage = HoverZoomImage;
@@ -64239,6 +64921,7 @@ exports.ThemeProvider = require_theme_index.ThemeProvider$1;
64239
64921
  exports.Thermometer = Thermometer;
64240
64922
  exports.ThermostatHeroCard = ThermostatHeroCard;
64241
64923
  exports.ThermostatInlineControl = ThermostatInlineControl;
64924
+ exports.Timer = Timer;
64242
64925
  exports.TimezoneSelector = TimezoneSelector;
64243
64926
  exports.ToggleLeft = ToggleLeft;
64244
64927
  exports.ToggleRight = ToggleRight;
@@ -64919,6 +65602,15 @@ exports.useDeviceStateSlice$1 = useDeviceStateSlice;
64919
65602
  exports.useDeviceStatusGetStatus = useDeviceStatusGetStatus;
64920
65603
  exports.useDeviceWebrtc = useDeviceWebrtc;
64921
65604
  exports.useDevices = useDevices;
65605
+ exports.useDishwasherGetStatus = useDishwasherGetStatus;
65606
+ exports.useDishwasherPause = useDishwasherPause;
65607
+ exports.useDishwasherSetHalfLoad = useDishwasherSetHalfLoad;
65608
+ exports.useDishwasherSetHygienePlus = useDishwasherSetHygienePlus;
65609
+ exports.useDishwasherSetOption = useDishwasherSetOption;
65610
+ exports.useDishwasherSetPower = useDishwasherSetPower;
65611
+ exports.useDishwasherSetProgram = useDishwasherSetProgram;
65612
+ exports.useDishwasherStart = useDishwasherStart;
65613
+ exports.useDishwasherStop = useDishwasherStop;
64922
65614
  exports.useDisplayIconResolver = useDisplayIconResolver;
64923
65615
  exports.useDoorbellEvents = useDoorbellEvents;
64924
65616
  exports.useDoorbellGetStatus = useDoorbellGetStatus;
@@ -64951,6 +65643,7 @@ exports.useFanControlSetPreset = useFanControlSetPreset;
64951
65643
  exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
64952
65644
  exports.useFloodGetStatus = useFloodGetStatus;
64953
65645
  exports.useGasGetStatus = useGasGetStatus;
65646
+ exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
64954
65647
  exports.useHumidifierGetStatus = useHumidifierGetStatus;
64955
65648
  exports.useHumidifierSetMode = useHumidifierSetMode;
64956
65649
  exports.useHumidifierSetOn = useHumidifierSetOn;