@camstack/ui-library 1.2.298 → 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.
Files changed (25) hide show
  1. package/dist/{MotionZonesSettings-Bv8XzCa3.js → MotionZonesSettings-C3Tj12yc.js} +1 -1
  2. package/dist/{PrivacyMaskSettings-BlkbESYj.js → PrivacyMaskSettings-C9wfHkiB.js} +1 -1
  3. package/dist/{SceneMonitorEditor-i3-nXAEU.js → SceneMonitorEditor-30H29vN4.js} +1 -1
  4. package/dist/composites/addon-global-settings-form.d.ts +1 -1
  5. package/dist/composites/config-form-field.d.ts +3 -1
  6. package/dist/composites/device-controls/ac-panel.d.ts +14 -1
  7. package/dist/composites/device-controls/climate-panel.d.ts +4 -1
  8. package/dist/composites/device-controls/dishwasher-control.d.ts +17 -0
  9. package/dist/composites/device-controls/heating-valve-readout.d.ts +4 -0
  10. package/dist/composites/device-controls/types.d.ts +1 -1
  11. package/dist/composites/index.d.ts +2 -0
  12. package/dist/composites/record-entries/entry-json-schema-form.d.ts +11 -0
  13. package/dist/composites/record-entries/index.d.ts +4 -0
  14. package/dist/composites/record-entries/record-entries-hub.d.ts +9 -0
  15. package/dist/composites/record-entries/record-entries-panel.d.ts +6 -0
  16. package/dist/composites/record-entries/record-entry-secrets.d.ts +9 -0
  17. package/dist/composites/storage-records/index.d.ts +2 -0
  18. package/dist/composites/storage-records/storage-records-hub.d.ts +4 -0
  19. package/dist/composites/storage-records/storage-records-panel.d.ts +6 -0
  20. package/dist/generated/system-hooks.d.ts +20 -0
  21. package/dist/{icons-CLC60C-_.cjs → icons-B1AFXUaZ.cjs} +2 -78
  22. package/dist/{icons-BXXScOuy.js → icons-BT8c2hxp.js} +1 -77
  23. package/dist/index.cjs +1656 -186
  24. package/dist/index.js +1638 -193
  25. package/package.json +1 -1
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`. */
@@ -14135,7 +14236,7 @@ function ConfigNotificationTargetMultiSelectFieldView({ field, value, onChange,
14135
14236
  const kindsQuery = useNotificationOutputListTargetKinds({});
14136
14237
  const targets = (0, react$1.useMemo)(() => toTargetOptions(targetsQuery.data), [targetsQuery.data]);
14137
14238
  const kindIconUrls = (0, react$1.useMemo)(() => buildKindIconMap(kindsQuery.data), [kindsQuery.data]);
14138
- const selected = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" && value.length > 0 ? [value] : [];
14239
+ const selected = Array.isArray(value) ? value.filter((v) => typeof v === "string") : typeof value === "string" && value.length > 0 ? value.includes(",") ? value.split(",").map((id) => id.trim()).filter((id) => id.length > 0) : [value] : [];
14139
14240
  const pending = targetsQuery.isPending || kindsQuery.isPending;
14140
14241
  const error = targetsQuery.isError ? targetsQuery.error : kindsQuery.isError ? kindsQuery.error : null;
14141
14242
  if (pending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldShell, {
@@ -19498,11 +19599,11 @@ function useDeviceSnapshotImage(deviceId, options = {}) {
19498
19599
  //#region src/composites/cap-settings/camera-grid-layout.ts
19499
19600
  /** Smallest rectangle the editor will produce — ffmpeg refuses a zero-area crop. */
19500
19601
  var MIN_EXTENT = .02;
19501
- function isRecord(value) {
19602
+ function isRecord$2(value) {
19502
19603
  return value !== null && typeof value === "object" && !Array.isArray(value);
19503
19604
  }
19504
19605
  function readRect(value) {
19505
- if (!isRecord(value)) return null;
19606
+ if (!isRecord$2(value)) return null;
19506
19607
  const { x, y, width, height } = value;
19507
19608
  if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number") return null;
19508
19609
  if (x < 0 || y < 0 || width <= 0 || height <= 0) return null;
@@ -19515,7 +19616,7 @@ function readRect(value) {
19515
19616
  };
19516
19617
  }
19517
19618
  function readCell(value) {
19518
- if (!isRecord(value)) return null;
19619
+ if (!isRecord$2(value)) return null;
19519
19620
  const deviceId = value["deviceId"];
19520
19621
  if (typeof deviceId !== "number" || !Number.isInteger(deviceId)) return null;
19521
19622
  const source = readRect(value["source"]);
@@ -19528,7 +19629,7 @@ function readCell(value) {
19528
19629
  };
19529
19630
  }
19530
19631
  function readProfileOffer(value) {
19531
- if (!isRecord(value)) return null;
19632
+ if (!isRecord$2(value)) return null;
19532
19633
  const profile = value["profile"];
19533
19634
  const offered = value["offered"];
19534
19635
  const missing = value["missingSources"];
@@ -19557,7 +19658,7 @@ function readProfileOffer(value) {
19557
19658
  */
19558
19659
  function parseGridView(raw) {
19559
19660
  if (raw === null) return null;
19560
- if (!isRecord(raw)) return "unreadable";
19661
+ if (!isRecord$2(raw)) return "unreadable";
19561
19662
  const { instanceId, deviceId, name, fps, cells, profiles } = raw;
19562
19663
  if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
19563
19664
  const parsed = [];
@@ -25714,7 +25815,7 @@ function mergeAvailabilitySpans(ranges) {
25714
25815
  * significant digits (binary prefixes, matching recording sizes elsewhere).
25715
25816
  * KB and up render one decimal under 100, none above; bytes never get a decimal.
25716
25817
  */
25717
- function formatBytes(bytes) {
25818
+ function formatBytes$1(bytes) {
25718
25819
  if (!Number.isFinite(bytes) || bytes < 0) return "—";
25719
25820
  if (bytes < 1024) return `${Math.round(bytes)} B`;
25720
25821
  const units = [
@@ -26247,7 +26348,7 @@ function RecordingPanel({ deviceId }) {
26247
26348
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMetricCard, {
26248
26349
  label: t("uiLibrary.recordingPanel.storage", "Storage"),
26249
26350
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HardDrive, { className: "h-3.5 w-3.5" }),
26250
- children: status ? formatBytes(status.storageBytes) : "—"
26351
+ children: status ? formatBytes$1(status.storageBytes) : "—"
26251
26352
  }),
26252
26353
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMetricCard, {
26253
26354
  label: t("uiLibrary.recordingPanel.recorded24h", "Recorded (24h)"),
@@ -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,
@@ -29608,17 +29722,7 @@ function applyValuesToSchema(schema, nextValues) {
29608
29722
  };
29609
29723
  }
29610
29724
  function stripValues(schema) {
29611
- return {
29612
- ...schema,
29613
- sections: schema.sections.map((s) => ({
29614
- ...s,
29615
- fields: s.fields.map((f) => {
29616
- if (!isValueBearingField(f)) return f;
29617
- const { value: _drop, ...rest } = f;
29618
- return rest;
29619
- })
29620
- }))
29621
- };
29725
+ return (0, _camstack_types.dehydrateSchema)(schema);
29622
29726
  }
29623
29727
  function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAfterChange, refreshToken, sectionIds, bare = false, collapsible, defaultOpen, mode, onOverrideChange }) {
29624
29728
  const t = useUiText();
@@ -42924,6 +43028,19 @@ function FanPanel({ trpc, deviceId, features, layout, optimistic }) {
42924
43028
  }
42925
43029
  //#endregion
42926
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
+ }
42927
43044
  /**
42928
43045
  * Exhaustive colour map — one entry per HvacMode. Build will fail if a
42929
43046
  * mode is added to the enum and not reflected here (Record<HvacMode, ...>
@@ -43004,7 +43121,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43004
43121
  onClick: (e) => {
43005
43122
  e.stopPropagation();
43006
43123
  if (!busy && display.target !== null) {
43007
- const next = display.target - 1;
43124
+ const next = nextClimateTarget(display.target, -1, display);
43008
43125
  applyOptimistic({ target: next });
43009
43126
  cap.setTarget({ target: next });
43010
43127
  }
@@ -43018,7 +43135,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43018
43135
  onClick: (e) => {
43019
43136
  e.stopPropagation();
43020
43137
  if (!busy && display.target !== null) {
43021
- const next = display.target + 1;
43138
+ const next = nextClimateTarget(display.target, 1, display);
43022
43139
  applyOptimistic({ target: next });
43023
43140
  cap.setTarget({ target: next });
43024
43141
  }
@@ -43073,7 +43190,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43073
43190
  onClick: (e) => {
43074
43191
  e.stopPropagation();
43075
43192
  if (!busy && display.target !== null) {
43076
- const next = display.target - 1;
43193
+ const next = nextClimateTarget(display.target, -1, display);
43077
43194
  applyOptimistic({ target: next });
43078
43195
  cap.setTarget({ target: next });
43079
43196
  }
@@ -43088,7 +43205,7 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
43088
43205
  onClick: (e) => {
43089
43206
  e.stopPropagation();
43090
43207
  if (!busy && display.target !== null) {
43091
- const next = display.target + 1;
43208
+ const next = nextClimateTarget(display.target, 1, display);
43092
43209
  applyOptimistic({ target: next });
43093
43210
  cap.setTarget({ target: next });
43094
43211
  }
@@ -43746,7 +43863,8 @@ var CONTROL_CAP_NAMES = [
43746
43863
  "climate-control",
43747
43864
  "media-player",
43748
43865
  "control",
43749
- "pet-feeder"
43866
+ "pet-feeder",
43867
+ "dishwasher"
43750
43868
  ];
43751
43869
  //#endregion
43752
43870
  //#region src/composites/device-controls/popover-row-action.tsx
@@ -48557,7 +48675,405 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
48557
48675
  });
48558
48676
  }
48559
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
48560
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
+ }
48561
49077
  /** AC-appropriate short mode labels. Exhaustive over HvacMode. */
48562
49078
  function modeLabel(t, mode) {
48563
49079
  switch (mode) {
@@ -48591,9 +49107,6 @@ var MODE_ACCENT = {
48591
49107
  dry: "#22d3ee",
48592
49108
  off: "#a1a1aa"
48593
49109
  };
48594
- /** Practical AC setpoint range (Celsius) for the target range track. */
48595
- var TARGET_MIN = 16;
48596
- var TARGET_MAX = 30;
48597
49110
  /** Title-case a snake/lower token for a selector option ('medium_low' → 'Medium
48598
49111
  * low', 'turbo' → 'Turbo'). Shared by the fan + preset selectors. */
48599
49112
  function fanLabel(token) {
@@ -48680,7 +49193,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48680
49193
  onClick: (e) => {
48681
49194
  e.stopPropagation();
48682
49195
  if (!busy && display.target !== null) {
48683
- const next = display.target - 1;
49196
+ const next = nextClimateTarget(display.target, -1, display);
48684
49197
  applyOptimistic({ target: next });
48685
49198
  cap.setTarget({ target: next });
48686
49199
  }
@@ -48694,7 +49207,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48694
49207
  onClick: (e) => {
48695
49208
  e.stopPropagation();
48696
49209
  if (!busy && display.target !== null) {
48697
- const next = display.target + 1;
49210
+ const next = nextClimateTarget(display.target, 1, display);
48698
49211
  applyOptimistic({ target: next });
48699
49212
  cap.setTarget({ target: next });
48700
49213
  }
@@ -48724,6 +49237,8 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48724
49237
  });
48725
49238
  const accent = MODE_ACCENT[display.mode];
48726
49239
  const on = display.mode !== "off";
49240
+ const heatingOnly = !climateOffersCooling(display.availableModes);
49241
+ const setpointBounds = climateSetpointBounds(display);
48727
49242
  const CurrentPresetIcon = presetIcon(display.preset);
48728
49243
  const boost = display.preset === "turbo";
48729
49244
  const period = Math.max(.55, airflowPeriodSec(display.fanMode) * (boost ? .6 : 1));
@@ -48740,11 +49255,12 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48740
49255
  children: [
48741
49256
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48742
49257
  className: "pointer-events-none absolute inset-0 opacity-60",
48743
- 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, {
48744
49259
  mode: display.mode,
48745
49260
  accent,
48746
49261
  periodSec: period,
48747
- swaying
49262
+ swaying,
49263
+ forceRising: heatingOnly
48748
49264
  })
48749
49265
  }),
48750
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" }),
@@ -48762,6 +49278,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48762
49278
  children: [
48763
49279
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
48764
49280
  mode: display.mode,
49281
+ heatingOnly,
48765
49282
  className: "h-3.5 w-3.5"
48766
49283
  }),
48767
49284
  modeLabel(t, display.mode),
@@ -48788,7 +49305,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48788
49305
  disabled: busy,
48789
49306
  onClick: () => {
48790
49307
  if (busy || display.target === null) return;
48791
- const next = display.target - 1;
49308
+ const next = nextClimateTarget(display.target, -1, display);
48792
49309
  applyOptimistic({ target: next });
48793
49310
  cap.setTarget({ target: next });
48794
49311
  }
@@ -48810,19 +49327,29 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48810
49327
  disabled: busy,
48811
49328
  onClick: () => {
48812
49329
  if (busy || display.target === null) return;
48813
- const next = display.target + 1;
49330
+ const next = nextClimateTarget(display.target, 1, display);
48814
49331
  applyOptimistic({ target: next });
48815
49332
  cap.setTarget({ target: next });
48816
49333
  }
48817
49334
  })
48818
49335
  ]
48819
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48820
- 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",
48821
49338
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeTrack, {
48822
49339
  value: display.target,
48823
- min: TARGET_MIN,
48824
- max: TARGET_MAX,
48825
- 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
+ }
48826
49353
  })
48827
49354
  })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48828
49355
  className: "relative z-10 flex items-baseline gap-0.5",
@@ -48846,6 +49373,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
48846
49373
  className: "flex",
48847
49374
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeIcon, {
48848
49375
  mode: display.mode,
49376
+ heatingOnly,
48849
49377
  className: "h-4 w-4"
48850
49378
  })
48851
49379
  }),
@@ -48929,9 +49457,8 @@ function Field({ label, children }) {
48929
49457
  }), children]
48930
49458
  });
48931
49459
  }
48932
- function ModeIcon({ mode, className }) {
48933
- const Icon = MODE_ICON[mode];
48934
- 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], {
48935
49462
  className,
48936
49463
  "aria-hidden": true
48937
49464
  });
@@ -48981,11 +49508,89 @@ function RoundStep({ label, symbol, disabled, onClick }) {
48981
49508
  children: symbol
48982
49509
  });
48983
49510
  }
48984
- /** Thin gradient range track with a knob at the setpoint fraction. */
48985
- function RangeTrack({ value, min, max, accent }) {
48986
- 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
+ };
48987
49579
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48988
- 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"),
48989
49594
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48990
49595
  className: "relative h-1.5 w-full rounded-full bg-surface-subtle",
48991
49596
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -48995,7 +49600,7 @@ function RangeTrack({ value, min, max, accent }) {
48995
49600
  background: accent
48996
49601
  }
48997
49602
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48998
- 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",
48999
49604
  style: {
49000
49605
  left: pct,
49001
49606
  background: accent
@@ -49012,7 +49617,7 @@ function RangeTrack({ value, min, max, accent }) {
49012
49617
  * vent; every active mode renders drifting particles that FALL (cool/dry/auto)
49013
49618
  * or RISE (heat), with a horizontal sweep overlaid when a swing axis is active.
49014
49619
  */
49015
- function AirflowViz({ mode, accent, periodSec, swaying }) {
49620
+ function AirflowViz({ mode, accent, periodSec, swaying, forceRising = false }) {
49016
49621
  const columns = [
49017
49622
  0,
49018
49623
  1,
@@ -49021,7 +49626,7 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49021
49626
  4,
49022
49627
  5
49023
49628
  ];
49024
- const rising = mode === "heat";
49629
+ const rising = forceRising || mode === "heat";
49025
49630
  const isDroplet = mode === "dry";
49026
49631
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49027
49632
  "aria-hidden": true,
@@ -49037,6 +49642,7 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49037
49642
  size: 56
49038
49643
  })
49039
49644
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49645
+ "data-testid": forceRising ? "climate-heat-rise" : void 0,
49040
49646
  className: cn("acv-sway-wrap absolute inset-0", swaying && "acv-sway"),
49041
49647
  style: swaying ? { animation: "acv-sway 2.6s ease-in-out infinite" } : void 0,
49042
49648
  children: columns.map((i) => {
@@ -49059,6 +49665,23 @@ function AirflowViz({ mode, accent, periodSec, swaying }) {
49059
49665
  })
49060
49666
  });
49061
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
+ }
49062
49685
  /** Big spinning fan for `fan_only` mode. */
49063
49686
  function FanBlades({ color, periodSec, size }) {
49064
49687
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
@@ -49225,16 +49848,33 @@ var MODE_ARC_COLOR = {
49225
49848
  */
49226
49849
  function ThermostatHeroCard({ trpc, deviceId, features: _features, optimistic }) {
49227
49850
  const t = useUiText();
49228
- const { cap, slice, available } = useDeviceCapability({
49851
+ const climate = useDeviceCapability({
49229
49852
  trpc,
49230
49853
  deviceId,
49231
49854
  cap: (d) => d.climateControl,
49232
49855
  slice: (d) => d.state.climateControl
49233
49856
  });
49234
- const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
49235
- if (cap === void 0) return null;
49236
- if (display === void 0) return null;
49237
- 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;
49238
49878
  const color = MODE_ARC_COLOR[display.mode];
49239
49879
  const isRange = display.mode === "heat_cool" || display.targetLow !== null && display.targetHigh !== null;
49240
49880
  const min = DEFAULT_TEMP_MIN;
@@ -49245,132 +49885,140 @@ function ThermostatHeroCard({ trpc, deviceId, features: _features, optimistic })
49245
49885
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49246
49886
  "data-mode": display.mode,
49247
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]),
49248
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49249
- className: "flex flex-col items-center gap-3",
49250
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49251
- className: "relative",
49252
- style: {
49253
- width: GAUGE_SIZE,
49254
- height: GAUGE_SIZE
49255
- },
49256
- children: [
49257
- isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49258
- mode: "band",
49259
- band: {
49260
- from: fractionLow,
49261
- to: fractionHigh
49262
- },
49263
- color
49264
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeRing, {
49265
- mode: "fill",
49266
- fraction: singleFraction,
49267
- color
49268
- }),
49269
- isRange ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49270
- "data-testid": "arc-knob",
49271
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49272
- fraction: fractionLow,
49273
- color,
49274
- knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49275
- onDragFraction: (f) => {
49276
- if (busy || display.targetHigh === null) return;
49277
- const next = Math.min(fractionToTemp(f, min, max), display.targetHigh);
49278
- applyOptimistic({ targetLow: next });
49279
- cap.setTargetRange({
49280
- targetLow: next,
49281
- targetHigh: display.targetHigh
49282
- });
49283
- }
49284
- })
49285
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49286
- "data-testid": "arc-knob",
49287
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArcKnob, {
49288
- fraction: fractionHigh,
49289
- color,
49290
- knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49291
- onDragFraction: (f) => {
49292
- if (busy || display.targetLow === null) return;
49293
- const next = Math.max(fractionToTemp(f, min, max), display.targetLow);
49294
- applyOptimistic({ targetHigh: next });
49295
- cap.setTargetRange({
49296
- targetLow: display.targetLow,
49297
- targetHigh: next
49298
- });
49299
- }
49300
- })
49301
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49302
- "data-testid": "arc-knob",
49303
- 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",
49304
49907
  fraction: singleFraction,
49305
- color,
49306
- knobTopPx: THERMOSTAT_KNOB_TOP_PX,
49307
- onDragFraction: (f) => {
49308
- if (busy) return;
49309
- const next = fractionToTemp(f, min, max);
49310
- applyOptimistic({ target: next });
49311
- cap.setTarget({ target: next });
49312
- }
49313
- })
49314
- }),
49315
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49316
- className: "absolute inset-0 flex flex-col items-center justify-center gap-0.5 pointer-events-none",
49317
- children: [
49318
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49319
- "data-testid": "current-temp",
49320
- className: "text-4xl font-bold tabular-nums text-foreground leading-none",
49321
- children: [display.currentTemp !== null ? display.currentTemp : "—", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49322
- className: "text-lg text-foreground-subtle font-normal ml-0.5",
49323
- children: "°"
49324
- })]
49325
- }),
49326
- !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49327
- className: "text-xs font-semibold",
49328
- style: { color },
49329
- children: t("uiLibrary.thermostatHeroCard.setTo", "set {{value}}°", { value: display.target })
49330
- }),
49331
- isRange && display.targetLow !== null && display.targetHigh !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49332
- className: "text-xs font-semibold",
49333
- style: { color },
49334
- children: [
49335
- display.targetLow,
49336
- "° – ",
49337
- display.targetHigh,
49338
- "°"
49339
- ]
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
+ }
49340
49925
  })
49341
- ]
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
+ }
49342
49995
  })
49343
- ]
49344
- }), !isRange && display.target !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
49345
- className: "w-[200px]",
49346
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stepper, {
49347
- value: `${display.target}°`,
49348
- onDelta: (d) => {
49349
- if (busy || display.target === null) return;
49350
- const next = display.target + d;
49351
- applyOptimistic({ target: next });
49352
- 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 });
49353
50013
  }
49354
- })
49355
- })]
49356
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
49357
- "aria-label": t("uiLibrary.thermostatHeroCard.mode", "Mode"),
49358
- value: display.mode,
49359
- disabled: busy,
49360
- onChange: (e) => {
49361
- e.stopPropagation();
49362
- const mode = e.target.value;
49363
- if (!busy && mode) {
49364
- applyOptimistic({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
49365
- cap.setMode({ mode: display.availableModes.find((m) => m === mode) ?? display.mode });
49366
- }
49367
- },
49368
- className: cn("w-full rounded border border-border bg-surface px-3 py-1.5 text-sm", busy && "opacity-40 cursor-not-allowed"),
49369
- children: display.availableModes.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
49370
- value: m,
49371
- children: m
49372
- }, m))
49373
- })]
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
+ ]
49374
50022
  });
49375
50023
  }
49376
50024
  //#endregion
@@ -50878,14 +51526,32 @@ function useClimateCapGuard(trpc, deviceId) {
50878
51526
  });
50879
51527
  return cap !== void 0;
50880
51528
  }
50881
- function ThermostatInlineControl({ trpc, deviceId, features, optimistic }) {
50882
- if (!useClimateCapGuard(trpc, deviceId)) return null;
50883
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClimatePanel, {
51529
+ function useHeatingValveCapGuard(trpc, deviceId) {
51530
+ const { cap } = useDeviceCapability({
50884
51531
  trpc,
50885
51532
  deviceId,
50886
- features,
50887
- layout: "compact",
50888
- 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]
50889
51555
  });
50890
51556
  }
50891
51557
  function AcInlineControl({ trpc, deviceId, features, optimistic }) {
@@ -51050,6 +51716,10 @@ var CONTROL_KIND_COMPONENTS = {
51050
51716
  InlineControl: PetFeederInlineControl,
51051
51717
  HeroCard: PetFeederHeroCard
51052
51718
  },
51719
+ dishwasher: {
51720
+ InlineControl: DishwasherInlineControl,
51721
+ HeroCard: DishwasherHeroCard
51722
+ },
51053
51723
  script: {
51054
51724
  InlineControl: ScriptInlineControl,
51055
51725
  HeroCard: ScriptHeroCard
@@ -51126,7 +51796,8 @@ var CONTROL_FILLS = {
51126
51796
  [_camstack_types.DeviceType.Weather]: false,
51127
51797
  [_camstack_types.DeviceType.Container]: false,
51128
51798
  [_camstack_types.DeviceType.Image]: false,
51129
- [_camstack_types.DeviceType.PetFeeder]: false
51799
+ [_camstack_types.DeviceType.PetFeeder]: false,
51800
+ [_camstack_types.DeviceType.Dishwasher]: false
51130
51801
  };
51131
51802
  var DEVICE_TYPE_VALUES = new Set(Object.values(_camstack_types.DeviceType));
51132
51803
  /**
@@ -58683,6 +59354,780 @@ function EntryIcon({ url }) {
58683
59354
  });
58684
59355
  }
58685
59356
  //#endregion
59357
+ //#region src/composites/record-entries/record-entry-secrets.ts
59358
+ /** Whether a record entry field still holds the redaction token from a read. */
59359
+ function isRedactedSecret(value) {
59360
+ return value === _camstack_types.REDACTED_SECRET;
59361
+ }
59362
+ /** Text shown in a password box for a redacted stored secret (empty — placeholder carries the hint). */
59363
+ function secretDisplayValue(value) {
59364
+ if (isRedactedSecret(value)) return "";
59365
+ return typeof value === "string" ? value : "";
59366
+ }
59367
+ /** Merge an edited password box back into the body sent to `putEntry`. */
59368
+ function mergeSecretForSubmit(draft, original) {
59369
+ if (!isRedactedSecret(original)) return draft;
59370
+ if (draft.length === 0) return _camstack_types.REDACTED_SECRET;
59371
+ return draft;
59372
+ }
59373
+ /** Is this JSON-schema leaf (or path) a credential field? */
59374
+ function isSecretField(path, secretPaths, schemaNode) {
59375
+ if (schemaNode["secret"] === true) return true;
59376
+ return secretPaths.includes(path);
59377
+ }
59378
+ //#endregion
59379
+ //#region src/composites/record-entries/entry-json-schema-form.tsx
59380
+ function isRecord$1(value) {
59381
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59382
+ }
59383
+ function labelFor(key, node) {
59384
+ const desc = node["description"];
59385
+ if (typeof desc === "string" && desc.length > 0) return desc;
59386
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase());
59387
+ }
59388
+ function readString(value) {
59389
+ return typeof value === "string" ? value : "";
59390
+ }
59391
+ function readNumber(value) {
59392
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
59393
+ }
59394
+ function readBool(value) {
59395
+ return value === true;
59396
+ }
59397
+ function objectFromJson(text) {
59398
+ try {
59399
+ const parsed = JSON.parse(text);
59400
+ return isRecord$1(parsed) ? parsed : null;
59401
+ } catch {
59402
+ return null;
59403
+ }
59404
+ }
59405
+ function RecordEntrySecretField({ label, required, value, disabled, onPatch }) {
59406
+ const [shown, setShown] = (0, react$1.useState)(false);
59407
+ const stored = isRedactedSecret(value);
59408
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59409
+ label,
59410
+ required,
59411
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PasswordInput, {
59412
+ value: secretDisplayValue(value),
59413
+ onValueChange: (text) => onPatch(mergeSecretForSubmit(text, value)),
59414
+ shown,
59415
+ onToggleShown: () => setShown((s) => !s),
59416
+ disabled,
59417
+ placeholder: stored ? SECRET_MASK_PLACEHOLDER : `Enter ${label.toLowerCase()}`
59418
+ })
59419
+ });
59420
+ }
59421
+ function SchemaField(props) {
59422
+ const { path, name, node, value, secretPaths, required, disabled, onPatch } = props;
59423
+ const label = labelFor(name, node);
59424
+ if (isSecretField(path, secretPaths, node)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntrySecretField, {
59425
+ label,
59426
+ required,
59427
+ value,
59428
+ disabled,
59429
+ onPatch: (next) => onPatch(path, next)
59430
+ });
59431
+ const enumValues = node["enum"];
59432
+ if (Array.isArray(enumValues) && enumValues.every((v) => typeof v === "string")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59433
+ label,
59434
+ required,
59435
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
59436
+ value: readString(value),
59437
+ onChange: (e) => onPatch(path, e.target.value),
59438
+ disabled,
59439
+ options: enumValues.map((opt) => ({
59440
+ value: opt,
59441
+ label: opt
59442
+ }))
59443
+ })
59444
+ });
59445
+ const type = node["type"];
59446
+ if (type === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59447
+ label,
59448
+ required,
59449
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
59450
+ checked: readBool(value),
59451
+ onCheckedChange: (v) => onPatch(path, v),
59452
+ disabled
59453
+ })
59454
+ });
59455
+ if (type === "integer" || type === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59456
+ label,
59457
+ required,
59458
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
59459
+ type: "number",
59460
+ value: readNumber(value) ?? "",
59461
+ onChange: (e) => {
59462
+ const raw = e.target.value;
59463
+ onPatch(path, raw === "" ? void 0 : Number(raw));
59464
+ },
59465
+ disabled
59466
+ })
59467
+ });
59468
+ if (type === "object") {
59469
+ const properties = node["properties"];
59470
+ if (isRecord$1(properties)) {
59471
+ const childValue = isRecord$1(value) ? value : {};
59472
+ const childRequired = Array.isArray(node["required"]) ? node["required"].filter((k) => typeof k === "string") : [];
59473
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
59474
+ className: "space-y-3 rounded-lg border border-border-subtle p-3",
59475
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
59476
+ className: "px-1 text-xs font-medium text-foreground-muted",
59477
+ children: label
59478
+ }), Object.entries(properties).map(([key, childNode]) => isRecord$1(childNode) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SchemaField, {
59479
+ path: `${path}.${key}`,
59480
+ name: key,
59481
+ node: childNode,
59482
+ value: childValue[key],
59483
+ secretPaths,
59484
+ required: childRequired.includes(key),
59485
+ disabled,
59486
+ onPatch
59487
+ }, key) : null)]
59488
+ });
59489
+ }
59490
+ const jsonText = isRecord$1(value) ? JSON.stringify(value, null, 2) : "{}";
59491
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59492
+ label,
59493
+ required,
59494
+ description: "JSON object",
59495
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
59496
+ className: cn("min-h-[8rem] w-full resize-y rounded-md border border-border bg-surface px-2.5 py-2 font-mono text-xs text-foreground", "placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-inset focus:ring-primary", "disabled:cursor-not-allowed disabled:opacity-50"),
59497
+ rows: 6,
59498
+ defaultValue: jsonText,
59499
+ disabled,
59500
+ onBlur: (e) => {
59501
+ const parsed = objectFromJson(e.target.value);
59502
+ if (parsed !== null) onPatch(path, parsed);
59503
+ }
59504
+ })
59505
+ });
59506
+ }
59507
+ if (type === "string" || type === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59508
+ label,
59509
+ required,
59510
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
59511
+ value: readString(value),
59512
+ onChange: (e) => onPatch(path, e.target.value),
59513
+ disabled
59514
+ })
59515
+ });
59516
+ return null;
59517
+ }
59518
+ function setAtPath(root, path, next) {
59519
+ const parts = path.split(".");
59520
+ if (parts.length === 1) return {
59521
+ ...root,
59522
+ [parts[0]]: next
59523
+ };
59524
+ const [head, ...rest] = parts;
59525
+ const key = head;
59526
+ const child = isRecord$1(root[key]) ? root[key] : {};
59527
+ return {
59528
+ ...root,
59529
+ [key]: setAtPath(child, rest.join("."), next)
59530
+ };
59531
+ }
59532
+ /** Renders an entry body from the engine's JSON Schema descriptor. */
59533
+ function EntryJsonSchemaForm({ schema, secretPaths, value, onChange, disabled }) {
59534
+ const properties = schema["properties"];
59535
+ if (!isRecord$1(properties)) return null;
59536
+ const required = Array.isArray(schema["required"]) ? schema["required"].filter((k) => typeof k === "string") : [];
59537
+ const onPatch = (path, next) => {
59538
+ onChange(setAtPath(value, path, next));
59539
+ };
59540
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59541
+ className: "space-y-3",
59542
+ children: Object.entries(properties).map(([key, node]) => isRecord$1(node) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SchemaField, {
59543
+ path: key,
59544
+ name: key,
59545
+ node,
59546
+ value: value[key],
59547
+ secretPaths,
59548
+ required: required.includes(key),
59549
+ disabled,
59550
+ onPatch
59551
+ }, key) : null)
59552
+ });
59553
+ }
59554
+ //#endregion
59555
+ //#region src/composites/record-entries/record-entries-panel.tsx
59556
+ function isRecord(value) {
59557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59558
+ }
59559
+ function defaultBody(schema) {
59560
+ const properties = schema["properties"];
59561
+ if (!isRecord(properties)) return {};
59562
+ const out = {};
59563
+ for (const [key, node] of Object.entries(properties)) {
59564
+ if (!isRecord(node)) continue;
59565
+ if (node["default"] !== void 0) {
59566
+ out[key] = node["default"];
59567
+ continue;
59568
+ }
59569
+ if (node["type"] === "boolean") out[key] = false;
59570
+ else if (node["type"] === "object") out[key] = {};
59571
+ }
59572
+ return out;
59573
+ }
59574
+ function RecordEntriesPanel({ kind }) {
59575
+ const t = useUiText();
59576
+ const queryClient = (0, _tanstack_react_query.useQueryClient)();
59577
+ const confirm = useConfirm();
59578
+ const formUrl = useItemFormUrl({
59579
+ newKey: "entryNew",
59580
+ editKey: "entryEdit"
59581
+ });
59582
+ const listQuery = useRecordEntriesListEntries({
59583
+ owner: kind.owner,
59584
+ kind: kind.kind
59585
+ }, { staleTime: 15e3 });
59586
+ const putMutation = useRecordEntriesPutEntry({ onSuccess: async () => {
59587
+ await queryClient.invalidateQueries({ queryKey: [["recordEntries"]] });
59588
+ formUrl.close();
59589
+ } });
59590
+ const deleteMutation = useRecordEntriesDeleteEntry({ onSuccess: async () => {
59591
+ await queryClient.invalidateQueries({ queryKey: [["recordEntries"]] });
59592
+ } });
59593
+ const entries = listQuery.data?.entries ?? [];
59594
+ const editing = (0, react$1.useMemo)(() => {
59595
+ const st = formUrl.state;
59596
+ if (st.mode !== "edit") return null;
59597
+ return entries.find((e) => e.id === st.id) ?? null;
59598
+ }, [entries, formUrl.state]);
59599
+ const [draft, setDraft] = (0, react$1.useState)({});
59600
+ const [saveError, setSaveError] = (0, react$1.useState)(null);
59601
+ const openNew = (0, react$1.useCallback)(() => {
59602
+ setDraft(defaultBody(kind.fields));
59603
+ setSaveError(null);
59604
+ formUrl.openNew();
59605
+ }, [formUrl, kind.fields]);
59606
+ const openEdit = (0, react$1.useCallback)((entry) => {
59607
+ setDraft({ ...entry.data });
59608
+ setSaveError(null);
59609
+ formUrl.openEdit(entry.id);
59610
+ }, [formUrl]);
59611
+ const onDelete = (0, react$1.useCallback)(async (entry) => {
59612
+ if (!await confirm({
59613
+ title: t("uiLibrary.recordEntries.deleteTitle", "Delete entry"),
59614
+ message: t("uiLibrary.recordEntries.deleteDescription", "Remove \"{{name}}\" permanently?", { name: entry.name }),
59615
+ confirmLabel: t("uiLibrary.recordEntries.deleteConfirm", "Delete"),
59616
+ variant: "danger"
59617
+ })) return;
59618
+ await deleteMutation.mutateAsync({
59619
+ owner: kind.owner,
59620
+ kind: kind.kind,
59621
+ id: entry.id
59622
+ });
59623
+ }, [
59624
+ confirm,
59625
+ deleteMutation,
59626
+ kind.kind,
59627
+ kind.owner,
59628
+ t
59629
+ ]);
59630
+ const columns = (0, react$1.useMemo)(() => {
59631
+ return [
59632
+ {
59633
+ key: "name",
59634
+ header: t("uiLibrary.recordEntries.colName", "Name"),
59635
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59636
+ type: "button",
59637
+ variant: "ghost",
59638
+ size: "sm",
59639
+ className: "h-auto max-w-full justify-start px-0 py-0 font-medium hover:underline",
59640
+ onClick: () => openEdit(row),
59641
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59642
+ className: "truncate",
59643
+ children: row.name
59644
+ })
59645
+ })
59646
+ },
59647
+ {
59648
+ key: "updated",
59649
+ header: t("uiLibrary.recordEntries.colUpdated", "Updated"),
59650
+ nowrap: true,
59651
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59652
+ className: "text-[11px] text-foreground-subtle",
59653
+ children: new Date(row.updatedAt).toLocaleString()
59654
+ })
59655
+ },
59656
+ {
59657
+ key: "actions",
59658
+ header: "",
59659
+ align: "right",
59660
+ nowrap: true,
59661
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
59662
+ className: "flex justify-end gap-1",
59663
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59664
+ size: "sm",
59665
+ variant: "ghost",
59666
+ type: "button",
59667
+ onClick: () => openEdit(row),
59668
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pencil, { className: "h-3.5 w-3.5" })
59669
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59670
+ size: "sm",
59671
+ variant: "ghost",
59672
+ type: "button",
59673
+ onClick: () => void onDelete(row),
59674
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5 text-danger" })
59675
+ })]
59676
+ })
59677
+ }
59678
+ ];
59679
+ }, [
59680
+ onDelete,
59681
+ openEdit,
59682
+ t
59683
+ ]);
59684
+ const dialogOpen = formUrl.state.mode !== "closed";
59685
+ const dialogTitle = formUrl.state.mode === "new" ? t("uiLibrary.recordEntries.addTitle", "Add entry") : t("uiLibrary.recordEntries.editTitle", "Edit entry");
59686
+ const onSave = async () => {
59687
+ setSaveError(null);
59688
+ try {
59689
+ if (formUrl.state.mode === "new") await putMutation.mutateAsync({
59690
+ owner: kind.owner,
59691
+ kind: kind.kind,
59692
+ data: draft
59693
+ });
59694
+ else if (formUrl.state.mode === "edit" && editing !== null) await putMutation.mutateAsync({
59695
+ owner: kind.owner,
59696
+ kind: kind.kind,
59697
+ id: editing.id,
59698
+ data: draft,
59699
+ ifRev: editing.rev
59700
+ });
59701
+ else {
59702
+ setSaveError(t("uiLibrary.recordEntries.saveUnavailable", "Nothing to save"));
59703
+ return;
59704
+ }
59705
+ } catch (err) {
59706
+ setSaveError(err instanceof Error ? err.message : t("uiLibrary.recordEntries.saveFailed", "Save failed"));
59707
+ }
59708
+ };
59709
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59710
+ className: "space-y-3",
59711
+ children: [
59712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59713
+ className: "flex justify-end",
59714
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
59715
+ type: "button",
59716
+ size: "sm",
59717
+ onClick: openNew,
59718
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "mr-1 h-3.5 w-3.5" }), t("uiLibrary.recordEntries.add", "Add")]
59719
+ })
59720
+ }),
59721
+ listQuery.isError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: listQuery.error instanceof Error ? listQuery.error.message : t("uiLibrary.recordEntries.loadFailed", "Could not load entries") }),
59722
+ listQuery.isPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59723
+ className: "flex justify-center py-8 text-foreground-subtle",
59724
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
59725
+ }) : entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
59726
+ title: t("uiLibrary.recordEntries.emptyTitle", "No entries yet"),
59727
+ description: t("uiLibrary.recordEntries.emptyDescription", "Add the first row for this kind.")
59728
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
59729
+ columns,
59730
+ rows: entries,
59731
+ rowKey: (r) => r.id,
59732
+ maxBodyHeight: TABLE_SCROLL_MAX_H,
59733
+ emptyMessage: t("uiLibrary.recordEntries.emptyTitle", "No entries yet")
59734
+ }),
59735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ItemFormDialog, {
59736
+ open: dialogOpen,
59737
+ onClose: formUrl.close,
59738
+ title: dialogTitle,
59739
+ error: saveError,
59740
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59741
+ type: "button",
59742
+ size: "sm",
59743
+ disabled: putMutation.isPending,
59744
+ onClick: () => void onSave(),
59745
+ children: putMutation.isPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }) : t("uiLibrary.recordEntries.save", "Save")
59746
+ }),
59747
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryJsonSchemaForm, {
59748
+ schema: kind.fields,
59749
+ secretPaths: kind.secretPaths,
59750
+ value: draft,
59751
+ onChange: setDraft,
59752
+ disabled: putMutation.isPending
59753
+ })
59754
+ })
59755
+ ]
59756
+ });
59757
+ }
59758
+ //#endregion
59759
+ //#region src/composites/record-entries/record-entries-hub.tsx
59760
+ var RECORD_ENTRY_OWNER_PARAM = "entryOwner";
59761
+ var RECORD_ENTRY_KIND_PARAM = "entryKind";
59762
+ function kindKey$1(k) {
59763
+ return `${k.owner}/${k.kind}`;
59764
+ }
59765
+ function RecordEntriesHub({ filter }) {
59766
+ const t = useUiText();
59767
+ const kindsQuery = useRecordEntriesListEntryKinds({}, { staleTime: 3e4 });
59768
+ const [ownerParam, setOwner] = useUrlQueryParam(RECORD_ENTRY_OWNER_PARAM);
59769
+ const [kindParam, setKind] = useUrlQueryParam(RECORD_ENTRY_KIND_PARAM);
59770
+ const kinds = (0, react$1.useMemo)(() => {
59771
+ return (kindsQuery.data?.kinds ?? []).filter((k) => {
59772
+ if (filter?.owner !== void 0 && k.owner !== filter.owner) return false;
59773
+ if (filter?.kind !== void 0 && k.kind !== filter.kind) return false;
59774
+ return true;
59775
+ });
59776
+ }, [
59777
+ filter?.kind,
59778
+ filter?.owner,
59779
+ kindsQuery.data?.kinds
59780
+ ]);
59781
+ const active = (0, react$1.useMemo)(() => {
59782
+ if (kinds.length === 0) return null;
59783
+ return kinds.find((k) => k.owner === ownerParam && k.kind === kindParam) ?? kinds[0] ?? null;
59784
+ }, [
59785
+ kindParam,
59786
+ kinds,
59787
+ ownerParam
59788
+ ]);
59789
+ const selectKind = (k) => {
59790
+ setOwner(k.owner);
59791
+ setKind(k.kind);
59792
+ };
59793
+ if (kindsQuery.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: kindsQuery.error instanceof Error ? kindsQuery.error.message : t("uiLibrary.recordEntries.kindsFailed", "Could not load entry kinds") });
59794
+ if (kindsQuery.isPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59795
+ className: "flex justify-center py-10 text-foreground-subtle",
59796
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
59797
+ });
59798
+ if (kinds.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
59799
+ title: t("uiLibrary.recordEntries.noKindsTitle", "No entry kinds"),
59800
+ description: t("uiLibrary.recordEntries.noKindsDescription", "No addon has declared an entry record kind yet.")
59801
+ });
59802
+ const activeKey = active === null ? "" : kindKey$1(active);
59803
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59804
+ className: "space-y-4",
59805
+ children: [kinds.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tabs, {
59806
+ appearance: "pills",
59807
+ value: activeKey,
59808
+ onValueChange: (key) => {
59809
+ const found = kinds.find((k) => kindKey$1(k) === key);
59810
+ if (found !== void 0) selectKind(found);
59811
+ },
59812
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabsList, {
59813
+ className: "flex flex-wrap gap-1",
59814
+ children: kinds.map((k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TabsTrigger, {
59815
+ value: kindKey$1(k),
59816
+ children: [k.title, k.rows > 0 ? ` (${k.rows})` : ""]
59817
+ }, kindKey$1(k)))
59818
+ })
59819
+ }), active !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [kinds.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
59820
+ className: "mb-2 text-sm font-semibold text-foreground",
59821
+ children: active.title
59822
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntriesPanel, { kind: active })] })]
59823
+ });
59824
+ }
59825
+ //#endregion
59826
+ //#region src/composites/storage-records/storage-records-panel.tsx
59827
+ function StorageRecordsPanel({ kind }) {
59828
+ const t = useUiText();
59829
+ const [inspect, setInspect] = (0, react$1.useState)(null);
59830
+ const recordsQuery = trpc.storageAdmin.readRecords.useQuery({
59831
+ owner: kind.owner,
59832
+ kind: kind.kind,
59833
+ limit: 100,
59834
+ desc: true
59835
+ }, { staleTime: 15e3 });
59836
+ const records = recordsQuery.data?.records ?? [];
59837
+ const columns = (0, react$1.useMemo)(() => {
59838
+ return [
59839
+ {
59840
+ key: "id",
59841
+ header: t("uiLibrary.storageRecords.colId", "Id"),
59842
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59843
+ type: "button",
59844
+ variant: "ghost",
59845
+ size: "sm",
59846
+ className: "h-auto max-w-full justify-start px-0 py-0 font-mono text-xs hover:underline",
59847
+ onClick: () => setInspect(row),
59848
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59849
+ className: "truncate",
59850
+ children: row.id
59851
+ })
59852
+ })
59853
+ },
59854
+ {
59855
+ key: "deviceId",
59856
+ header: t("uiLibrary.storageRecords.colDevice", "Device"),
59857
+ nowrap: true,
59858
+ render: (row) => row.deviceId === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59859
+ className: "text-foreground-subtle",
59860
+ children: "—"
59861
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59862
+ className: "font-mono text-xs",
59863
+ children: row.deviceId
59864
+ })
59865
+ },
59866
+ {
59867
+ key: "at",
59868
+ header: t("uiLibrary.storageRecords.colAt", "At"),
59869
+ nowrap: true,
59870
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59871
+ className: "text-[11px] text-foreground-subtle",
59872
+ children: row.at === null ? "—" : new Date(row.at).toLocaleString()
59873
+ })
59874
+ },
59875
+ {
59876
+ key: "updatedAt",
59877
+ header: t("uiLibrary.storageRecords.colUpdated", "Updated"),
59878
+ nowrap: true,
59879
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59880
+ className: "text-[11px] text-foreground-subtle",
59881
+ children: new Date(row.updatedAt).toLocaleString()
59882
+ })
59883
+ }
59884
+ ];
59885
+ }, [t]);
59886
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59887
+ className: "space-y-3",
59888
+ children: [
59889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59890
+ className: "flex flex-wrap items-center gap-2",
59891
+ children: [
59892
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
59893
+ className: "text-sm font-semibold text-foreground",
59894
+ children: `${kind.owner}/${kind.kind}`
59895
+ }),
59896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
59897
+ variant: "neutral",
59898
+ style: "outline",
59899
+ children: kind.database
59900
+ }),
59901
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
59902
+ variant: "neutral",
59903
+ style: "outline",
59904
+ children: kind.class
59905
+ }),
59906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
59907
+ variant: kind.state === "declared" ? "default" : "neutral",
59908
+ children: kind.state
59909
+ }),
59910
+ kind.entry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
59911
+ variant: "info",
59912
+ children: t("uiLibrary.storageRecords.entryKind", "Entry")
59913
+ }) : null
59914
+ ]
59915
+ }),
59916
+ recordsQuery.isError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: recordsQuery.error instanceof Error ? recordsQuery.error.message : t("uiLibrary.storageRecords.loadFailed", "Could not load records") }) : recordsQuery.isPending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59917
+ className: "flex justify-center py-8 text-foreground-subtle",
59918
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
59919
+ }) : records.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
59920
+ title: t("uiLibrary.storageRecords.emptyTitle", "No rows"),
59921
+ description: t("uiLibrary.storageRecords.emptyDescription", "This kind has no stored records yet.")
59922
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
59923
+ columns,
59924
+ rows: records,
59925
+ rowKey: (r) => r.id,
59926
+ maxBodyHeight: TABLE_SCROLL_MAX_H,
59927
+ emptyMessage: t("uiLibrary.storageRecords.emptyTitle", "No rows")
59928
+ }),
59929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
59930
+ open: inspect !== null,
59931
+ onOpenChange: (open) => !open && setInspect(null),
59932
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, {
59933
+ width: "lg",
59934
+ placement: "sheet-on-mobile",
59935
+ scroll: "body",
59936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogTitle, { children: t("uiLibrary.storageRecords.inspectTitle", "Record") }), inspect !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, {
59937
+ className: "font-mono",
59938
+ children: inspect.id
59939
+ }) : null] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogBody, { children: inspect !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeBlock, {
59940
+ maxHeight: 480,
59941
+ children: JSON.stringify(inspect, null, 2)
59942
+ }) : null })]
59943
+ })
59944
+ })
59945
+ ]
59946
+ });
59947
+ }
59948
+ //#endregion
59949
+ //#region src/composites/storage-records/storage-records-hub.tsx
59950
+ var STORAGE_RECORDS_OWNER_PARAM = "recordsOwner";
59951
+ var STORAGE_RECORDS_KIND_PARAM = "recordsKind";
59952
+ function kindKey(k) {
59953
+ return `${k.owner}/${k.kind}`;
59954
+ }
59955
+ function formatBytes(n) {
59956
+ if (n === null) return "—";
59957
+ if (n < 1024) return `${n} B`;
59958
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
59959
+ return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
59960
+ }
59961
+ function StorageRecordsHub() {
59962
+ const t = useUiText();
59963
+ const kindsQuery = trpc.storageAdmin.listKinds.useQuery({}, { staleTime: 3e4 });
59964
+ const [ownerParam, setOwner] = useUrlQueryParam(STORAGE_RECORDS_OWNER_PARAM);
59965
+ const [kindParam, setKind] = useUrlQueryParam(STORAGE_RECORDS_KIND_PARAM);
59966
+ const kinds = kindsQuery.data?.kinds ?? [];
59967
+ const filter = useDataTableFilter(kinds, {
59968
+ searchText: (row) => [
59969
+ row.owner,
59970
+ row.kind,
59971
+ row.class,
59972
+ row.database,
59973
+ row.state
59974
+ ],
59975
+ facetGroups: [{
59976
+ id: "owner",
59977
+ value: (row) => row.owner
59978
+ }, {
59979
+ id: "database",
59980
+ value: (row) => row.database
59981
+ }]
59982
+ });
59983
+ const active = (0, react$1.useMemo)(() => {
59984
+ if (kinds.length === 0) return null;
59985
+ return kinds.find((k) => k.owner === ownerParam && k.kind === kindParam) ?? null;
59986
+ }, [
59987
+ kindParam,
59988
+ kinds,
59989
+ ownerParam
59990
+ ]);
59991
+ const selectKind = (0, react$1.useCallback)((k) => {
59992
+ setOwner(k.owner);
59993
+ setKind(k.kind);
59994
+ }, [setKind, setOwner]);
59995
+ const columns = (0, react$1.useMemo)(() => {
59996
+ return [
59997
+ {
59998
+ key: "kind",
59999
+ header: t("uiLibrary.storageRecords.colKind", "Kind"),
60000
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
60001
+ className: "font-mono text-xs",
60002
+ children: [
60003
+ row.owner,
60004
+ "/",
60005
+ row.kind
60006
+ ]
60007
+ }),
60008
+ sortValue: (row) => kindKey(row)
60009
+ },
60010
+ {
60011
+ key: "database",
60012
+ header: t("uiLibrary.storageRecords.colDatabase", "DB"),
60013
+ nowrap: true,
60014
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60015
+ variant: "neutral",
60016
+ style: "outline",
60017
+ children: row.database
60018
+ }),
60019
+ sortValue: (row) => row.database
60020
+ },
60021
+ {
60022
+ key: "class",
60023
+ header: t("uiLibrary.storageRecords.colClass", "Class"),
60024
+ nowrap: true,
60025
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60026
+ className: "text-xs text-foreground-muted",
60027
+ children: row.class
60028
+ }),
60029
+ sortValue: (row) => row.class
60030
+ },
60031
+ {
60032
+ key: "state",
60033
+ header: t("uiLibrary.storageRecords.colState", "State"),
60034
+ nowrap: true,
60035
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60036
+ variant: row.state === "declared" ? "default" : "neutral",
60037
+ children: row.state
60038
+ }),
60039
+ sortValue: (row) => row.state
60040
+ },
60041
+ {
60042
+ key: "rows",
60043
+ header: t("uiLibrary.storageRecords.colRows", "Rows"),
60044
+ align: "right",
60045
+ nowrap: true,
60046
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60047
+ className: "font-mono text-xs tabular-nums",
60048
+ children: row.rows === null ? "—" : row.rows
60049
+ }),
60050
+ sortValue: (row) => row.rows ?? -1
60051
+ },
60052
+ {
60053
+ key: "bytes",
60054
+ header: t("uiLibrary.storageRecords.colSize", "Est. size"),
60055
+ align: "right",
60056
+ nowrap: true,
60057
+ render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60058
+ className: "text-xs text-foreground-subtle",
60059
+ children: formatBytes(row.bytesEstimate)
60060
+ }),
60061
+ sortValue: (row) => row.bytesEstimate ?? -1
60062
+ },
60063
+ {
60064
+ key: "inspect",
60065
+ header: "",
60066
+ align: "right",
60067
+ nowrap: true,
60068
+ render: (row) => {
60069
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
60070
+ type: "button",
60071
+ size: "sm",
60072
+ variant: active !== null && kindKey(active) === kindKey(row) ? "primary" : "outline",
60073
+ onClick: () => selectKind(row),
60074
+ children: t("uiLibrary.storageRecords.viewRows", "View rows")
60075
+ });
60076
+ },
60077
+ notSortableReason: t("uiLibrary.storageRecords.actionColumn", "Actions")
60078
+ }
60079
+ ];
60080
+ }, [
60081
+ active,
60082
+ selectKind,
60083
+ t
60084
+ ]);
60085
+ if (kindsQuery.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: kindsQuery.error instanceof Error ? kindsQuery.error.message : t("uiLibrary.storageRecords.kindsFailed", "Could not load record kinds") });
60086
+ if (kindsQuery.isPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
60087
+ className: "flex justify-center py-10 text-foreground-subtle",
60088
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
60089
+ });
60090
+ if (kinds.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
60091
+ title: t("uiLibrary.storageRecords.noKindsTitle", "No record kinds"),
60092
+ description: t("uiLibrary.storageRecords.noKindsDescription", "No addon has declared a record kind on this hub yet.")
60093
+ });
60094
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
60095
+ className: "space-y-4",
60096
+ children: [
60097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTableFilterBar, {
60098
+ state: filter.state,
60099
+ facets: filter.facets,
60100
+ groups: filter.groups.map((g) => ({
60101
+ ...g,
60102
+ label: g.id === "owner" ? t("uiLibrary.storageRecords.facetOwner", "Owner") : t("uiLibrary.storageRecords.facetDatabase", "Database")
60103
+ })),
60104
+ labels: {
60105
+ searchPlaceholder: t("uiLibrary.storageRecords.search", "Search kinds…"),
60106
+ searchLabel: t("uiLibrary.storageRecords.searchLabel", "Search record kinds"),
60107
+ facetsLabel: t("uiLibrary.storageRecords.facetsLabel", "Filter record kinds"),
60108
+ clearLabel: t("uiLibrary.storageRecords.clearFilters", "Clear filters")
60109
+ },
60110
+ onSearch: filter.setSearch,
60111
+ onToggleFacet: filter.toggleFacet,
60112
+ onToggleGroupFacet: filter.toggleGroupFacet,
60113
+ onClear: filter.clear
60114
+ }),
60115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
60116
+ columns,
60117
+ rows: filter.rows,
60118
+ rowKey: (k) => kindKey(k),
60119
+ maxBodyHeight: TABLE_SCROLL_MAX_H,
60120
+ defaultSort: {
60121
+ columnKey: "kind",
60122
+ dir: "asc"
60123
+ },
60124
+ emptyMessage: t("uiLibrary.storageRecords.noMatch", "No kinds match the filter")
60125
+ }),
60126
+ active !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StorageRecordsPanel, { kind: active }) : null
60127
+ ]
60128
+ });
60129
+ }
60130
+ //#endregion
58686
60131
  //#region src/composites/response-log.tsx
58687
60132
  /**
58688
60133
  * Expandable log box showing the raw JSON response from a pipeline run.
@@ -63152,6 +64597,7 @@ exports.EllipsisVertical = EllipsisVertical;
63152
64597
  exports.EmptyState = EmptyState;
63153
64598
  exports.EndpointResultsCard = EndpointResultsCard;
63154
64599
  exports.EndpointTestCard = EndpointTestCard;
64600
+ exports.EntryJsonSchemaForm = EntryJsonSchemaForm;
63155
64601
  exports.ErrorBox = ErrorBox;
63156
64602
  exports.EventKindGlyph = EventKindGlyph;
63157
64603
  exports.EventStream = EventStream;
@@ -63199,6 +64645,7 @@ exports.Hammer = Hammer;
63199
64645
  exports.Hand = Hand;
63200
64646
  exports.HardDrive = HardDrive;
63201
64647
  exports.Hash = Hash;
64648
+ exports.Heater = Heater;
63202
64649
  exports.HlsVideo = HlsVideo;
63203
64650
  exports.House = House;
63204
64651
  exports.HoverZoomImage = HoverZoomImage;
@@ -63339,6 +64786,8 @@ exports.QrCode = QrCode;
63339
64786
  exports.QrCode$1 = QrCode$1;
63340
64787
  exports.RECONNECT_POLICY = RECONNECT_POLICY;
63341
64788
  exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
64789
+ exports.RECORD_ENTRY_KIND_PARAM = RECORD_ENTRY_KIND_PARAM;
64790
+ exports.RECORD_ENTRY_OWNER_PARAM = RECORD_ENTRY_OWNER_PARAM;
63342
64791
  exports.RIGHT = RIGHT;
63343
64792
  exports.ROI_AREA_WARN_FRACTION = ROI_AREA_WARN_FRACTION;
63344
64793
  exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
@@ -63347,6 +64796,8 @@ exports.Radar = Radar;
63347
64796
  exports.RadialGauge = RadialGauge;
63348
64797
  exports.Radio = Radio;
63349
64798
  exports.RadioTower = RadioTower;
64799
+ exports.RecordEntriesHub = RecordEntriesHub;
64800
+ exports.RecordEntriesPanel = RecordEntriesPanel;
63350
64801
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
63351
64802
  exports.RecordingPanel = RecordingPanel;
63352
64803
  exports.RefreshCw = RefreshCw;
@@ -63379,6 +64830,8 @@ exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
63379
64830
  exports.STACK_GAP = STACK_GAP;
63380
64831
  exports.STATE_COLOR = STATE_COLOR;
63381
64832
  exports.STEP_DOCK_WIDTH_PX = STEP_DOCK_WIDTH_PX;
64833
+ exports.STORAGE_RECORDS_KIND_PARAM = STORAGE_RECORDS_KIND_PARAM;
64834
+ exports.STORAGE_RECORDS_OWNER_PARAM = STORAGE_RECORDS_OWNER_PARAM;
63382
64835
  exports.Sailboat = Sailboat;
63383
64836
  exports.Save = Save;
63384
64837
  exports.Scan = Scan;
@@ -63437,6 +64890,8 @@ exports.StepInspectorPanel = StepInspectorPanel;
63437
64890
  exports.StepModelRow = StepModelRow;
63438
64891
  exports.StepTimings = StepTimings;
63439
64892
  exports.Stepper = Stepper;
64893
+ exports.StorageRecordsHub = StorageRecordsHub;
64894
+ exports.StorageRecordsPanel = StorageRecordsPanel;
63440
64895
  exports.StreamBrokerSelector = StreamBrokerSelector;
63441
64896
  exports.StreamDebugOverlay = StreamDebugOverlay;
63442
64897
  exports.StreamPanel = StreamPanel;
@@ -63466,6 +64921,7 @@ exports.ThemeProvider = require_theme_index.ThemeProvider$1;
63466
64921
  exports.Thermometer = Thermometer;
63467
64922
  exports.ThermostatHeroCard = ThermostatHeroCard;
63468
64923
  exports.ThermostatInlineControl = ThermostatInlineControl;
64924
+ exports.Timer = Timer;
63469
64925
  exports.TimezoneSelector = TimezoneSelector;
63470
64926
  exports.ToggleLeft = ToggleLeft;
63471
64927
  exports.ToggleRight = ToggleRight;
@@ -63629,6 +65085,8 @@ exports.isDataTableFilterActive = isDataTableFilterActive;
63629
65085
  exports.isFieldVisible = isFieldVisible;
63630
65086
  exports.isGroupExpanded = isGroupExpanded;
63631
65087
  exports.isKeychainFieldValue = isKeychainFieldValue;
65088
+ exports.isRedactedSecret = isRedactedSecret;
65089
+ exports.isSecretField = isSecretField;
63632
65090
  exports.isValueField = isValueField;
63633
65091
  exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
63634
65092
  exports.lightColors = require_theme_to_root_css.lightColors;
@@ -63637,6 +65095,7 @@ exports.loadRemoteBundle = loadRemoteBundle;
63637
65095
  exports.lucideExportName = lucideExportName;
63638
65096
  exports.makeScrubBridge = makeScrubBridge;
63639
65097
  exports.mapRootDetectorPlacements = mapRootDetectorPlacements;
65098
+ exports.mergeSecretForSubmit = mergeSecretForSubmit;
63640
65099
  exports.metadataEntries = metadataEntries;
63641
65100
  exports.metadataString = metadataString;
63642
65101
  exports.mirror = mirror;
@@ -63709,6 +65168,7 @@ exports.routerBasename = routerBasename;
63709
65168
  exports.sameLocationSuggestions = sameLocationSuggestions;
63710
65169
  exports.scrubReducer = scrubReducer;
63711
65170
  exports.searchSteps = searchSteps;
65171
+ exports.secretDisplayValue = secretDisplayValue;
63712
65172
  exports.secretSubmitValue = secretSubmitValue;
63713
65173
  exports.selectPickerGroup = selectPickerGroup;
63714
65174
  exports.selectedDeviceOptions = selectedDeviceOptions;
@@ -64142,6 +65602,15 @@ exports.useDeviceStateSlice$1 = useDeviceStateSlice;
64142
65602
  exports.useDeviceStatusGetStatus = useDeviceStatusGetStatus;
64143
65603
  exports.useDeviceWebrtc = useDeviceWebrtc;
64144
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;
64145
65614
  exports.useDisplayIconResolver = useDisplayIconResolver;
64146
65615
  exports.useDoorbellEvents = useDoorbellEvents;
64147
65616
  exports.useDoorbellGetStatus = useDoorbellGetStatus;
@@ -64174,6 +65643,7 @@ exports.useFanControlSetPreset = useFanControlSetPreset;
64174
65643
  exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
64175
65644
  exports.useFloodGetStatus = useFloodGetStatus;
64176
65645
  exports.useGasGetStatus = useGasGetStatus;
65646
+ exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
64177
65647
  exports.useHumidifierGetStatus = useHumidifierGetStatus;
64178
65648
  exports.useHumidifierSetMode = useHumidifierSetMode;
64179
65649
  exports.useHumidifierSetOn = useHumidifierSetOn;