@camstack/ui-library 1.2.324 → 1.2.326

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20973,21 +20973,16 @@ Skeleton.displayName = "Skeleton";
20973
20973
  /** `value` null = unknown (D393) — renders an em-dash, never a fake `0`. */
20974
20974
  function formatRemaining(t, r) {
20975
20975
  if (r.value === null) return "—";
20976
- const count = r.value;
20977
- switch (r.unit) {
20978
- case "days": return t("uiLibrary.consumablesPanel.days", {
20979
- one: "{{count}} day",
20980
- other: "{{count}} days"
20981
- }, { count });
20982
- case "hours": return t("uiLibrary.consumablesPanel.hours", {
20983
- one: "{{count}} hour",
20984
- other: "{{count}} hours"
20985
- }, { count });
20986
- case "cycles": return t("uiLibrary.consumablesPanel.cycles", {
20987
- one: "{{count}} cycle",
20988
- other: "{{count}} cycles"
20989
- }, { count });
20990
- }
20976
+ if (r.kind === "count") return t("uiLibrary.consumablesPanel.cycles", {
20977
+ one: "{{count}} cycle",
20978
+ other: "{{count}} cycles"
20979
+ }, { count: r.value });
20980
+ const rendered = (0, _camstack_types.formatReading)({
20981
+ value: r.value,
20982
+ quantity: "duration",
20983
+ precision: 0
20984
+ });
20985
+ return rendered.unit.length === 0 ? rendered.text : `${rendered.text} ${rendered.unit}`;
20991
20986
  }
20992
20987
  /** Format a ms-epoch as a compact local date for the "replaced" hint. */
20993
20988
  function formatResetAt(ms) {
@@ -33995,10 +33990,19 @@ function AudioWaveform({ samples, sampleRate = 16e3, height = 96, className = ""
33995
33990
  });
33996
33991
  }
33997
33992
  //#endregion
33993
+ //#region src/composites/binary-battery.ts
33994
+ /**
33995
+ * Binary battery copy lives with the badge. A binary source has no real
33996
+ * percentage: normal is the full stand-in, low is anything under the threshold.
33997
+ */
33998
+ var BINARY_BATTERY_LOW_BELOW = 50;
33999
+ var BATTERY_BADGE_LOW_KEY = "uiLibrary.batteryBadge.low";
34000
+ var BATTERY_BADGE_NORMAL_KEY = "uiLibrary.batteryBadge.normal";
34001
+ function binaryBatteryIsLow(percentage) {
34002
+ return percentage < 50;
34003
+ }
34004
+ //#endregion
33998
34005
  //#region src/composites/battery-badge.tsx
33999
- /** A binary battery is "low" below this stand-in percentage (normal maps to
34000
- * 100, low to a sub-threshold value). Keep in sync with the HA container. */
34001
- var BINARY_LOW_THRESHOLD = 50;
34002
34006
  function pickIcon(percentage) {
34003
34007
  if (percentage <= 10) return BatteryWarning;
34004
34008
  if (percentage <= 30) return BatteryLow;
@@ -34025,12 +34029,12 @@ function BatteryBadge({ status, variant = "compact", className }) {
34025
34029
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Battery, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("uiLibrary.batteryBadge.unknown", "Unknown") })]
34026
34030
  });
34027
34031
  if (binary === true) {
34028
- const low = percentage < BINARY_LOW_THRESHOLD;
34032
+ const low = binaryBatteryIsLow(percentage);
34029
34033
  const BinIcon = low ? BatteryWarning : BatteryFull;
34030
34034
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
34031
34035
  className: cn("inline-flex items-center gap-1 text-[10px] font-medium", low ? "text-rose-400" : "text-foreground-subtle", className),
34032
34036
  title: low ? t("uiLibrary.batteryBadge.titleLow", "Battery: low") : t("uiLibrary.batteryBadge.titleNormal", "Battery: normal"),
34033
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BinIcon, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: low ? t("uiLibrary.batteryBadge.low", "Low") : t("uiLibrary.batteryBadge.normal", "Normal") })]
34037
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BinIcon, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: low ? t(BATTERY_BADGE_LOW_KEY, "Low") : t(BATTERY_BADGE_NORMAL_KEY, "Normal") })]
34034
34038
  });
34035
34039
  }
34036
34040
  const Icon = pickIcon(percentage);
@@ -46074,6 +46078,66 @@ function formatNumeric(value, precision) {
46074
46078
  return Number.isInteger(value) ? String(value) : value.toFixed(1);
46075
46079
  }
46076
46080
  //#endregion
46081
+ //#region src/lib/resolve-sensor-display.ts
46082
+ function canonUnit(raw) {
46083
+ if (raw === void 0) return void 0;
46084
+ return (0, _camstack_types.normalizeUnit)(raw) ?? raw;
46085
+ }
46086
+ function concreteQuantity(input) {
46087
+ if (input.quantity === void 0) return void 0;
46088
+ if (input.quantity === "device") {
46089
+ const chosen = input.deviceQuantity;
46090
+ if (chosen === void 0 || chosen === "device") return void 0;
46091
+ return chosen;
46092
+ }
46093
+ return input.quantity;
46094
+ }
46095
+ function resolveSensorDisplay(input) {
46096
+ const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
46097
+ const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.declarationPrecision;
46098
+ const quantity = concreteQuantity(input);
46099
+ const preference = input.preference ?? _camstack_types.DEFAULT_DISPLAY_PREFERENCE;
46100
+ const base = quantity === void 0 ? void 0 : (0, _camstack_types.baseUnitOf)(quantity);
46101
+ const preferred = quantity === void 0 ? void 0 : (0, _camstack_types.displayUnitFor)(quantity, preference);
46102
+ const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
46103
+ const overrideOk = overrideUnit !== void 0 && base !== void 0 && overrideUnit !== base && (0, _camstack_types.canConvertUnit)(base, overrideUnit);
46104
+ const unit = overrideOk ? overrideUnit : preferred ?? "";
46105
+ const scales = quantity === "duration" || quantity === "information" || quantity === "duration-ms";
46106
+ const toDisplayValue = (raw) => {
46107
+ if (scales || base === void 0 || unit.length === 0 || unit === base) return raw;
46108
+ return (0, _camstack_types.tryConvertUnit)(raw, base, unit) ?? raw;
46109
+ };
46110
+ const fromDisplayValue = (shown) => {
46111
+ if (scales || base === void 0 || unit.length === 0 || unit === base) return shown;
46112
+ return (0, _camstack_types.tryConvertUnit)(shown, unit, base) ?? shown;
46113
+ };
46114
+ const format = (raw) => {
46115
+ if (quantity !== void 0 && !overrideOk) {
46116
+ const rendered = (0, _camstack_types.formatReading)({
46117
+ value: raw,
46118
+ quantity,
46119
+ preference,
46120
+ precision
46121
+ });
46122
+ return {
46123
+ text: rendered.text,
46124
+ unit: rendered.unit
46125
+ };
46126
+ }
46127
+ return {
46128
+ text: formatNumeric(toDisplayValue(raw), precision),
46129
+ unit
46130
+ };
46131
+ };
46132
+ return {
46133
+ unit,
46134
+ precision,
46135
+ toDisplayValue,
46136
+ fromDisplayValue,
46137
+ format
46138
+ };
46139
+ }
46140
+ //#endregion
46077
46141
  //#region src/lib/format-control-datetime.ts
46078
46142
  var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
46079
46143
  var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
@@ -46139,8 +46203,20 @@ function toDateInput(value) {
46139
46203
  function toTimeInput(value) {
46140
46204
  return value.slice(0, 5);
46141
46205
  }
46142
- /** Trim an HA datetime value to the `<input type=datetime-local>` shape
46143
- * ('YYYY-MM-DDTHH:MM'). Accepts both 'T'- and space-separated forms. */
46206
+ function datetimeEditor(format, rawValue) {
46207
+ if (format === "date") return {
46208
+ type: "date",
46209
+ value: toDateInput(rawValue)
46210
+ };
46211
+ if (format === "time") return {
46212
+ type: "time",
46213
+ value: toTimeInput(rawValue)
46214
+ };
46215
+ return {
46216
+ type: "datetime-local",
46217
+ value: toDateTimeInput(rawValue)
46218
+ };
46219
+ }
46144
46220
  function toDateTimeInput(value) {
46145
46221
  return (value.includes("T") ? value : value.replace(" ", "T")).slice(0, 16);
46146
46222
  }
@@ -46233,24 +46309,35 @@ function TextControl({ value, busy, onCommit }) {
46233
46309
  className: cn("rounded border border-border bg-surface px-2 py-0.5 text-xs", busy && "opacity-40 cursor-not-allowed")
46234
46310
  });
46235
46311
  }
46236
- function ControlWidget({ slice, busy, setValueFn, layout }) {
46312
+ function ControlWidget({ slice, busy, setValueFn, layout, quantity, display, dateTimeShape }) {
46237
46313
  const t = useUiText();
46238
46314
  switch (slice.kind) {
46239
- case "numeric": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumericControl, {
46240
- value: typeof slice.value === "number" ? slice.value : Number(slice.value),
46241
- busy,
46242
- onCommit: (v) => {
46243
- setValueFn({ control: {
46244
- kind: "numeric",
46245
- value: v
46246
- } });
46247
- },
46248
- unit: slice.unit,
46249
- min: slice.min,
46250
- max: slice.max,
46251
- step: slice.step,
46252
- precision: slice.precision
46253
- });
46315
+ case "numeric": {
46316
+ const numericValue = typeof slice.value === "number" ? slice.value : Number(slice.value);
46317
+ const resolved = quantity === void 0 ? void 0 : resolveSensorDisplay({
46318
+ quantity,
46319
+ display,
46320
+ capName: "control"
46321
+ });
46322
+ const toShown = resolved?.toDisplayValue ?? ((raw) => raw);
46323
+ const toBase = resolved?.fromDisplayValue ?? ((shown) => shown);
46324
+ const unit = resolved !== void 0 && resolved.unit.length > 0 ? resolved.unit : void 0;
46325
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumericControl, {
46326
+ value: toShown(numericValue),
46327
+ busy,
46328
+ onCommit: (v) => {
46329
+ setValueFn({ control: {
46330
+ kind: "numeric",
46331
+ value: toBase(v)
46332
+ } });
46333
+ },
46334
+ unit,
46335
+ min: slice.min === void 0 ? void 0 : toShown(slice.min),
46336
+ max: slice.max === void 0 ? void 0 : toShown(slice.max),
46337
+ step: slice.step === void 0 ? void 0 : toShown(slice.step),
46338
+ precision: resolved?.precision
46339
+ });
46340
+ }
46254
46341
  case "select": {
46255
46342
  const selectValue = String(slice.value);
46256
46343
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
@@ -46283,7 +46370,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
46283
46370
  });
46284
46371
  case "datetime": {
46285
46372
  const rawValue = String(slice.value);
46286
- const format = slice.format ?? "datetime";
46373
+ const format = dateTimeShape ?? "datetime";
46287
46374
  if (layout === "compact") {
46288
46375
  if (rawValue === "") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
46289
46376
  "aria-label": t("uiLibrary.controlPanel.value", "Control value"),
@@ -46296,8 +46383,9 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
46296
46383
  children: formatControlDateTime(rawValue, format)
46297
46384
  });
46298
46385
  }
46299
- const inputType = format === "date" ? "date" : format === "time" ? "time" : "datetime-local";
46300
- const inputValue = format === "date" ? toDateInput(rawValue) : format === "time" ? toTimeInput(rawValue) : toDateTimeInput(rawValue);
46386
+ const editor = datetimeEditor(format, rawValue);
46387
+ const inputType = editor.type;
46388
+ const inputValue = editor.value;
46301
46389
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
46302
46390
  type: inputType,
46303
46391
  "aria-label": t("uiLibrary.controlPanel.value", "Control value"),
@@ -46317,6 +46405,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
46317
46405
  }
46318
46406
  }
46319
46407
  function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
46408
+ const info = useOptionalSystem()?.getDeviceInfo(deviceId) ?? null;
46320
46409
  const { cap, slice, available } = useDeviceCapability({
46321
46410
  trpc,
46322
46411
  deviceId,
@@ -46326,6 +46415,8 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
46326
46415
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
46327
46416
  if (display === void 0) return null;
46328
46417
  const busy = !available;
46418
+ const chosen = info?.sourceInfo?.quantity;
46419
+ const quantity = (0, _camstack_types.isReadingQuantity)(chosen) && chosen !== "device" && chosen !== "none" ? chosen : void 0;
46329
46420
  const setValueFn = (input) => {
46330
46421
  if (!busy && cap) {
46331
46422
  applyOptimistic({ value: input.control.value });
@@ -46339,7 +46430,10 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
46339
46430
  slice: display,
46340
46431
  busy,
46341
46432
  setValueFn,
46342
- layout
46433
+ layout,
46434
+ quantity,
46435
+ display: info?.display,
46436
+ dateTimeShape: info?.sourceInfo?.dateTimeShape
46343
46437
  })
46344
46438
  });
46345
46439
  }
@@ -46443,30 +46537,6 @@ function PopoverRowAction({ icon: Icon, title, children }) {
46443
46537
  }), document.body)] });
46444
46538
  }
46445
46539
  //#endregion
46446
- //#region src/lib/resolve-sensor-display.ts
46447
- /** Table keys are canonical spellings; providers often send `C` / `F` / `celsius`. */
46448
- function canonUnit(raw) {
46449
- if (raw === void 0) return void 0;
46450
- return (0, _camstack_types.normalizeUnit)(raw) ?? raw;
46451
- }
46452
- var identity = (raw) => raw;
46453
- function resolveSensorDisplay(input) {
46454
- const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
46455
- const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.slicePrecision;
46456
- const sourceUnit = canonUnit(input.sliceUnit ?? input.canonicalUnit);
46457
- const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
46458
- if (overrideUnit !== void 0 && sourceUnit !== void 0 && overrideUnit !== sourceUnit && (0, _camstack_types.canConvertUnit)(sourceUnit, overrideUnit)) return {
46459
- unit: overrideUnit,
46460
- precision,
46461
- toDisplayValue: (raw) => (0, _camstack_types.tryConvertUnit)(raw, sourceUnit, overrideUnit) ?? raw
46462
- };
46463
- return {
46464
- unit: sourceUnit ?? "",
46465
- precision,
46466
- toDisplayValue: identity
46467
- };
46468
- }
46469
- //#endregion
46470
46540
  //#region src/composites/device-controls/sensor-value-atom.tsx
46471
46541
  /**
46472
46542
  * Binary fallback for the `battery-sensor` role.
@@ -46507,102 +46577,105 @@ function hasBatteryNumericValue(slice, field) {
46507
46577
  * avoids dynamic string indexing — each one is a concrete property
46508
46578
  * access on the typed `DeviceProxy['state']` shape.
46509
46579
  *
46510
- * Canonical unit rules:
46511
- * - Typed roles (temperature/humidity/ambient-light/pressure/power/energy)
46512
- * carry a `canonicalUnit` constant. The slice is normalised to one unit
46513
- * so no per-device lookup is needed.
46514
- * - Generic `numeric-sensor` has no `canonicalUnit`. The unit is read live
46515
- * from the slice (`slice.unit`) as populated by the provider from the
46516
- * upstream source on every state push.
46580
+ * The unit comes from the descriptor's quantity through `resolveSensorDisplay`.
46581
+ * A generic numeric sensor uses quantity `device` and the device's
46582
+ * `sourceInfo.quantity`.
46517
46583
  */
46518
46584
  var ROLE_DESCRIPTOR = {
46519
46585
  "temperature-sensor": {
46520
46586
  kind: "numeric",
46521
46587
  field: "celsius",
46522
- canonicalUnit: "°C",
46588
+ quantity: "temperature",
46523
46589
  getSliceHandle: (s) => s.temperatureSensor
46524
46590
  },
46525
46591
  "humidity-sensor": {
46526
46592
  kind: "numeric",
46527
46593
  field: "percent",
46528
- canonicalUnit: "%",
46594
+ quantity: "humidity",
46529
46595
  getSliceHandle: (s) => s.humiditySensor
46530
46596
  },
46531
46597
  "ambient-light-sensor": {
46532
46598
  kind: "numeric",
46533
46599
  field: "lux",
46534
- canonicalUnit: "lx",
46600
+ quantity: "illuminance",
46535
46601
  getSliceHandle: (s) => s.ambientLightSensor
46536
46602
  },
46537
46603
  "pressure-sensor": {
46538
46604
  kind: "numeric",
46539
46605
  field: "hpa",
46540
- canonicalUnit: "hPa",
46606
+ quantity: "pressure",
46541
46607
  getSliceHandle: (s) => s.pressureSensor
46542
46608
  },
46543
46609
  "numeric-sensor": {
46544
46610
  kind: "numeric",
46545
46611
  field: "value",
46612
+ quantity: "device",
46546
46613
  getSliceHandle: (s) => s.numericSensor
46547
46614
  },
46548
46615
  "wind-sensor": {
46549
46616
  kind: "numeric",
46550
46617
  field: "value",
46618
+ quantity: "device",
46551
46619
  getSliceHandle: (s) => s.numericSensor
46552
46620
  },
46553
46621
  "rain-sensor": {
46554
46622
  kind: "numeric",
46555
46623
  field: "value",
46624
+ quantity: "device",
46556
46625
  getSliceHandle: (s) => s.numericSensor
46557
46626
  },
46558
46627
  "uv-sensor": {
46559
46628
  kind: "numeric",
46560
46629
  field: "value",
46630
+ quantity: "device",
46561
46631
  getSliceHandle: (s) => s.numericSensor
46562
46632
  },
46563
46633
  "solar-radiation-sensor": {
46564
46634
  kind: "numeric",
46565
46635
  field: "value",
46636
+ quantity: "device",
46566
46637
  getSliceHandle: (s) => s.numericSensor
46567
46638
  },
46568
46639
  "soil-moisture-sensor": {
46569
46640
  kind: "numeric",
46570
46641
  field: "value",
46642
+ quantity: "device",
46571
46643
  getSliceHandle: (s) => s.numericSensor
46572
46644
  },
46573
46645
  "power-sensor": {
46574
46646
  kind: "numeric",
46575
46647
  field: "watts",
46576
- canonicalUnit: "W",
46648
+ quantity: "power",
46577
46649
  getSliceHandle: (s) => s.powerMeter
46578
46650
  },
46579
46651
  "energy-sensor": {
46580
46652
  kind: "numeric",
46581
46653
  field: "kwhTotal",
46582
- canonicalUnit: "kWh",
46654
+ quantity: "energy",
46583
46655
  getSliceHandle: (s) => s.powerMeter
46584
46656
  },
46585
46657
  "voltage-sensor": {
46586
46658
  kind: "numeric",
46587
46659
  field: "volts",
46588
- canonicalUnit: "V",
46660
+ quantity: "voltage",
46589
46661
  getSliceHandle: (s) => s.powerMeter
46590
46662
  },
46591
46663
  "current-sensor": {
46592
46664
  kind: "numeric",
46593
46665
  field: "amps",
46594
- canonicalUnit: "A",
46666
+ quantity: "current",
46595
46667
  getSliceHandle: (s) => s.powerMeter
46596
46668
  },
46597
46669
  "battery-sensor": {
46598
46670
  kind: "numeric",
46599
46671
  field: "value",
46600
- canonicalUnit: "%",
46672
+ quantity: "percentage",
46601
46673
  getSliceHandle: (s) => s.numericSensor
46602
46674
  },
46603
46675
  "generic-sensor": {
46604
46676
  kind: "numeric",
46605
46677
  field: "value",
46678
+ quantity: "device",
46606
46679
  getSliceHandle: (s) => s.numericSensor
46607
46680
  },
46608
46681
  "motion-sensor": {
@@ -46720,19 +46793,16 @@ function SensorValueAtom({ trpc, device }) {
46720
46793
  children: enumSensorSlice.value
46721
46794
  });
46722
46795
  if (numericIsColStart) return null;
46723
- const sliceRecord = slice;
46724
- const liveUnit = typeof sliceRecord["unit"] === "string" ? sliceRecord["unit"] : void 0;
46725
- const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
46726
- const resolved = resolveSensorDisplay({
46796
+ const deviceQuantity = device.sourceInfo?.quantity;
46797
+ const rendered = resolveSensorDisplay({
46727
46798
  display: device.display,
46728
46799
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
46729
46800
  roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
46730
- sliceUnit: liveUnit,
46731
- slicePrecision,
46732
- canonicalUnit: descriptor.canonicalUnit
46733
- });
46734
- const unit = resolved.unit;
46735
- const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
46801
+ quantity: descriptor.quantity ?? "device",
46802
+ deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
46803
+ }).format(rawValue);
46804
+ const unit = rendered.unit;
46805
+ const formatted = rendered.text;
46736
46806
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
46737
46807
  className: cn("inline-flex items-baseline gap-0.5 tabular-nums text-xs text-foreground"),
46738
46808
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatted }), unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -46762,7 +46832,7 @@ function SensorValueAtom({ trpc, device }) {
46762
46832
  if (descriptor.kind === "datetime") {
46763
46833
  if (!isStringSliceValue(slice)) return null;
46764
46834
  if (slice.value === "") return null;
46765
- const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
46835
+ const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
46766
46836
  const formatted = formatControlDateTime(slice.value, format);
46767
46837
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
46768
46838
  className: cn("inline-flex items-baseline text-xs text-foreground"),
@@ -47317,17 +47387,14 @@ function SensorInlineControl({ trpc, deviceId }) {
47317
47387
  *
47318
47388
  * Renders:
47319
47389
  * - Typed numeric sensors: value in `text-3xl font-semibold tabular-nums` +
47320
- * canonical unit from ROLE_DESCRIPTOR (e.g. temperature-sensor → '°C').
47321
- * - Generic numeric-sensor: value + unit read live from the slice
47322
- * (`slice.unit`, populated by the HA provider from state attributes).
47390
+ * the unit `resolveSensorDisplay` derives from the quantity.
47391
+ * - Generic numeric-sensor: the same formatter, with the device's quantity.
47323
47392
  * - Binary sensors: coloured state badge ("Detected"/"Clear" etc.) — same
47324
47393
  * labels and colours as SensorValueAtom's binary branch.
47325
47394
  * - Presence: Home/Away/zone-name badge — same labels as SensorValueAtom.
47326
47395
  * - All: sensor name label below the value.
47327
47396
  *
47328
- * Unit resolution uses the same logic as SensorValueAtom (live slice.unit
47329
- * wins, ROLE_DESCRIPTOR canonicalUnit as fallback) — the two surfaces stay
47330
- * in sync.
47397
+ * Unit resolution uses the same formatter as SensorValueAtom.
47331
47398
  */
47332
47399
  function isNumericSliceValue(v) {
47333
47400
  return typeof v === "object" && v !== null;
@@ -47394,19 +47461,16 @@ function SensorHeroInner({ trpc, device }) {
47394
47461
  if (!isNumericSliceValue(slice)) return null;
47395
47462
  const rawValue = slice[descriptor.field];
47396
47463
  if (typeof rawValue !== "number") return null;
47397
- const sliceRecord = slice;
47398
- const liveUnit = typeof sliceRecord["unit"] === "string" ? sliceRecord["unit"] : void 0;
47399
- const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
47400
- const resolved = resolveSensorDisplay({
47464
+ const deviceQuantity = device.sourceInfo?.quantity;
47465
+ const rendered = resolveSensorDisplay({
47401
47466
  display: device.display,
47402
47467
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
47403
47468
  roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
47404
- sliceUnit: liveUnit,
47405
- slicePrecision,
47406
- canonicalUnit: descriptor.canonicalUnit
47407
- });
47408
- const unit = resolved.unit;
47409
- const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
47469
+ quantity: descriptor.quantity ?? "device",
47470
+ deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
47471
+ }).format(rawValue);
47472
+ const unit = rendered.unit;
47473
+ const formatted = rendered.text;
47410
47474
  const occupancyOn = occupancySlice !== void 0 && isBinarySliceValue(occupancySlice) && typeof occupancySlice["on"] === "boolean" ? Boolean(occupancySlice["on"]) : null;
47411
47475
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47412
47476
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
@@ -47462,7 +47526,7 @@ function SensorHeroInner({ trpc, device }) {
47462
47526
  children: t("uiLibrary.sensorHeroCard.awaiting", "Awaiting state…")
47463
47527
  })]
47464
47528
  });
47465
- const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
47529
+ const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
47466
47530
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47467
47531
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
47468
47532
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -49918,10 +49982,15 @@ function useWeather(trpc, deviceId) {
49918
49982
  slice: (d) => d.state.weather
49919
49983
  });
49920
49984
  }
49921
- /** Format a temperature + unit readout (e.g. `21°C`). */
49922
- function formatTemp(temp, unit) {
49923
- if (temp === null) return "—";
49924
- return `${Math.round(temp)}${unit ?? "°"}`;
49985
+ /** Format a base-unit weather reading (metric). */
49986
+ function formatWeather(value, quantity) {
49987
+ if (value === null) return "—";
49988
+ const rendered = (0, _camstack_types.formatReading)({
49989
+ value,
49990
+ quantity,
49991
+ precision: 0
49992
+ });
49993
+ return rendered.unit.length === 0 ? rendered.text : `${rendered.text}${rendered.unit}`;
49925
49994
  }
49926
49995
  /**
49927
49996
  * WeatherInlineControl — compact condition icon + temperature readout
@@ -49941,7 +50010,7 @@ function WeatherInlineControl({ trpc, deviceId }) {
49941
50010
  children: inlineGlyph(meta.glyph)
49942
50011
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49943
50012
  className: "tabular-nums",
49944
- children: formatTemp(slice.temperature, slice.temperatureUnit)
50013
+ children: formatWeather(slice.temperature, "temperature")
49945
50014
  })]
49946
50015
  });
49947
50016
  }
@@ -50251,7 +50320,11 @@ function WeatherHeroCard({ trpc, deviceId }) {
50251
50320
  if (slice === void 0) return null;
50252
50321
  const meta = weatherConditionMeta(slice.condition, t);
50253
50322
  const tint = weatherTint(slice.condition);
50254
- const unit = slice.temperatureUnit ?? "°";
50323
+ const temperature = slice.temperature === null ? void 0 : (0, _camstack_types.formatReading)({
50324
+ value: slice.temperature,
50325
+ quantity: "temperature",
50326
+ precision: 0
50327
+ });
50255
50328
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50256
50329
  className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
50257
50330
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -50266,11 +50339,14 @@ function WeatherHeroCard({ trpc, deviceId }) {
50266
50339
  children: [
50267
50340
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50268
50341
  className: "text-3xl font-semibold tabular-nums text-foreground",
50269
- children: slice.temperature !== null ? Math.round(slice.temperature) : "—"
50342
+ children: temperature !== void 0 ? temperature.text : "—"
50270
50343
  }),
50271
50344
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50272
50345
  className: "text-base text-foreground-subtle",
50273
- children: unit
50346
+ children: temperature !== void 0 ? temperature.unit : (0, _camstack_types.formatReading)({
50347
+ value: 0,
50348
+ quantity: "temperature"
50349
+ }).unit
50274
50350
  }),
50275
50351
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50276
50352
  className: "text-xs text-foreground-subtle ml-2 capitalize",
@@ -50291,12 +50367,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
50291
50367
  style: { transform: `rotate(${slice.windBearing}deg)` }
50292
50368
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
50293
50369
  label: t("uiLibrary.weatherControl.wind", "Wind"),
50294
- value: `${Math.round(slice.windSpeed)} ${slice.windSpeedUnit ?? ""}`.trim()
50370
+ value: formatWeather(slice.windSpeed, "speed")
50295
50371
  }),
50296
50372
  slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
50297
50373
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5" }),
50298
50374
  label: t("uiLibrary.weatherControl.pressure", "Pressure"),
50299
- value: `${Math.round(slice.pressure)} ${slice.pressureUnit ?? ""}`.trim()
50375
+ value: formatWeather(slice.pressure, "pressure")
50300
50376
  })
50301
50377
  ]
50302
50378
  })]
@@ -50535,6 +50611,16 @@ var VOLUME_MAX = 9;
50535
50611
  /** Amber kibble accent for the bowl fill; destructive red when low. */
50536
50612
  var KIBBLE = "#f59e0b";
50537
50613
  var KIBBLE_LOW = "#ef4444";
50614
+ function desiccantLabel(t, seconds) {
50615
+ const rendered = (0, _camstack_types.formatReading)({
50616
+ value: seconds,
50617
+ quantity: "duration"
50618
+ });
50619
+ return t("uiLibrary.petFeeder.desiccant", "{{text}}{{unit}} desiccant", {
50620
+ text: rendered.text,
50621
+ unit: rendered.unit
50622
+ });
50623
+ }
50538
50624
  function clampPortion(grams) {
50539
50625
  return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
50540
50626
  }
@@ -50657,7 +50743,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
50657
50743
  }),
50658
50744
  display.desiccantLeftDays !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
50659
50745
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
50660
- label: t("uiLibrary.petFeeder.desiccant", "{{days}}d desiccant", { days: display.desiccantLeftDays })
50746
+ label: desiccantLabel(t, display.desiccantLeftDays)
50661
50747
  }),
50662
50748
  onBatteries && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50663
50749
  className: "inline-flex items-center gap-1 font-medium text-amber-500",
@@ -68652,6 +68738,9 @@ exports.AudioWaveform = AudioWaveform;
68652
68738
  exports.AutomationHeroCard = AutomationHeroCard;
68653
68739
  exports.AutomationInlineControl = AutomationInlineControl;
68654
68740
  exports.AutotrackSection = AutotrackSection;
68741
+ exports.BATTERY_BADGE_LOW_KEY = BATTERY_BADGE_LOW_KEY;
68742
+ exports.BATTERY_BADGE_NORMAL_KEY = BATTERY_BADGE_NORMAL_KEY;
68743
+ exports.BINARY_BATTERY_LOW_BELOW = BINARY_BATTERY_LOW_BELOW;
68655
68744
  exports.BOOLEAN_STATES = BOOLEAN_STATES;
68656
68745
  exports.BTN_COMPACT = BTN_COMPACT;
68657
68746
  exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
@@ -69259,6 +69348,7 @@ exports.assignNodeColorId = _camstack_types.assignNodeColorId;
69259
69348
  exports.axisTicks = axisTicks;
69260
69349
  exports.badgeFor = badgeFor;
69261
69350
  exports.badgeFor$1 = badgeFor;
69351
+ exports.binaryBatteryIsLow = binaryBatteryIsLow;
69262
69352
  exports.bindsToModel = bindsToModel;
69263
69353
  exports.booleanLabels = booleanLabels;
69264
69354
  exports.bucketTotals = bucketTotals;