@camstack/ui-library 1.2.341 → 1.2.343

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.343] - 2026-10-28
11
+
12
+ ### Changed
13
+
14
+ - Device value fields are named after their quantity instead of their unit; run the quantity-field migration after updating
15
+
16
+ ## [1.2.342] - 2026-10-28
17
+
18
+ ### Changed
19
+
20
+ - Option lists can group options under headings, and a dropdown can be opened and closed from outside
21
+
10
22
  ## [1.2.341] - 2026-10-28
11
23
 
12
24
  ### Changed
@@ -15,7 +15,7 @@ import type { JSX } from "react";
15
15
  export interface BatteryStatusLike {
16
16
  /** `null` means no reading has landed yet (cold seed) — NOT a flat 0 %
17
17
  * (D315). Render "Unknown", never a red critical badge. */
18
- readonly percentage: number | null;
18
+ readonly level: number | null;
19
19
  readonly charging: "dc" | "solar" | "none" | "unknown";
20
20
  readonly sleeping: boolean;
21
21
  /** True for a binary LOW_BAT source with no real % — render "Normal"/"Low"
@@ -1,7 +1,7 @@
1
1
  import { type JSX } from "react";
2
2
  export interface NetworkLinkStatusLike {
3
3
  readonly type: "wifi" | "ethernet" | "cellular" | "unknown";
4
- readonly signalPercent: number | null;
4
+ readonly signal: number | null;
5
5
  readonly ssid?: string;
6
6
  }
7
7
  export interface NetworkLinkBadgeProps {
@@ -13,6 +13,12 @@ export type OptionSelectOption = {
13
13
  iconId?: string;
14
14
  disabled?: boolean;
15
15
  badge?: string;
16
+ /**
17
+ * Heading this option is listed under. Options of one group are expected
18
+ * together (the list keeps its order); a heading is drawn where the group
19
+ * changes.
20
+ */
21
+ group?: string;
16
22
  };
17
23
  /** Number of options at/above which search is enabled by default (unless creatable). */
18
24
  export declare const OPTION_SELECT_SEARCH_MIN_OPTIONS = 8;
@@ -10,8 +10,8 @@ import { type UseDeviceProxyTrpc } from "./use-device-proxy";
10
10
  export interface NetworkLinkStatus {
11
11
  readonly type: "wifi" | "ethernet" | "cellular" | "unknown";
12
12
  /** `null` = not known or not applicable (a wired link has no signal). */
13
- readonly signalPercent: number | null;
14
- readonly rssiDbm?: number;
13
+ readonly signal: number | null;
14
+ readonly rssi?: number;
15
15
  readonly ssid?: string;
16
16
  readonly lastUpdated: number;
17
17
  }
package/dist/index.cjs CHANGED
@@ -12818,8 +12818,14 @@ function useDropdownContext() {
12818
12818
  if (!ctx) throw new Error("Dropdown compound components must be used within <Dropdown>");
12819
12819
  return ctx;
12820
12820
  }
12821
- function Dropdown({ children }) {
12822
- const [open, setOpen] = (0, react$1.useState)(false);
12821
+ function Dropdown({ children, open: controlledOpen, onOpenChange }) {
12822
+ const [innerOpen, setInnerOpen] = (0, react$1.useState)(false);
12823
+ const controlled = controlledOpen !== void 0 && onOpenChange !== void 0;
12824
+ const open = controlled ? controlledOpen : innerOpen;
12825
+ const setOpen = (0, react$1.useCallback)((next) => {
12826
+ if (controlled) onOpenChange(next);
12827
+ else setInnerOpen(next);
12828
+ }, [controlled, onOpenChange]);
12823
12829
  const triggerId = (0, react$1.useId)();
12824
12830
  const contentId = (0, react$1.useId)();
12825
12831
  const triggerRef = (0, react$1.useRef)(null);
@@ -14010,7 +14016,13 @@ function OptionSelect(props) {
14010
14016
  const checked = mode === "single" ? props.value === opt.value : props.value.includes(opt.value);
14011
14017
  const blockedByMax = mode === "multi" && !checked && atMax === true;
14012
14018
  const optionDisabled = opt.disabled === true || blockedByMax;
14013
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14019
+ const previous = dropdownRows[i - 1];
14020
+ const previousGroup = previous !== void 0 && previous.kind === "option" ? previous.opt.group : void 0;
14021
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react$1.Fragment, { children: [opt.group !== void 0 && opt.group !== previousGroup ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14022
+ role: "presentation",
14023
+ className: "px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wide text-foreground-subtle",
14024
+ children: opt.group
14025
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14014
14026
  type: "button",
14015
14027
  role: "option",
14016
14028
  "aria-selected": checked,
@@ -14038,7 +14050,7 @@ function OptionSelect(props) {
14038
14050
  children: opt.badge
14039
14051
  }) : null
14040
14052
  ]
14041
- }, opt.value);
14053
+ })] }, opt.value);
14042
14054
  }) });
14043
14055
  const footerAddNewIndex = dropdownRows.length;
14044
14056
  const fieldShowsChoice = mode === "single" && props.singleDisplay === "field" && selectedOptions.length > 0;
@@ -35994,7 +36006,7 @@ function levelColor(percentage, charging) {
35994
36006
  function BatteryBadge({ status, variant = "compact", className }) {
35995
36007
  const t = useUiText();
35996
36008
  if (!status) return null;
35997
- const { percentage, charging, sleeping, binary } = status;
36009
+ const { level: percentage, charging, sleeping, binary } = status;
35998
36010
  if (percentage === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35999
36011
  className: cn("inline-flex items-center gap-1 text-[10px] font-medium text-foreground-subtle", className),
36000
36012
  title: t("uiLibrary.batteryBadge.titleUnknown", "Battery: unknown"),
@@ -36157,15 +36169,15 @@ function NetworkLinkBadge({ status, className }) {
36157
36169
  title: t("uiLibrary.networkLinkBadge.ethernet", "Network: ethernet"),
36158
36170
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cable, { className: "h-3.5 w-3.5" })
36159
36171
  });
36160
- const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signalPercent);
36161
- const signal = status.signalPercent === null ? null : Math.round(status.signalPercent);
36172
+ const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signal);
36173
+ const signal = status.signal === null ? null : Math.round(status.signal);
36162
36174
  const networkText = status.ssid ? t("uiLibrary.networkLinkBadge.typeSsid", "Network: {{type}} ({{ssid}})", {
36163
36175
  type: status.type,
36164
36176
  ssid: status.ssid
36165
36177
  }) : t("uiLibrary.networkLinkBadge.type", "Network: {{type}}", { type: status.type });
36166
36178
  const title = signal === null ? networkText : `${networkText} · ${t("uiLibrary.networkLinkBadge.signal", "signal {{percent}}%", { percent: signal })}`;
36167
36179
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
36168
- className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(status.signalPercent), className),
36180
+ className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(status.signal), className),
36169
36181
  title,
36170
36182
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), signal !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [signal, "%"] })]
36171
36183
  });
@@ -47415,16 +47427,16 @@ function BrightnessPanel({ trpc, deviceId, layout, optimistic }) {
47415
47427
  min: 0,
47416
47428
  max: 100,
47417
47429
  step: 1,
47418
- value: display.percentage,
47430
+ value: display.level,
47419
47431
  disabled: busy,
47420
47432
  onChange: (e) => {
47421
47433
  const pct = Number(e.target.value);
47422
- applyOptimistic({ percentage: pct });
47423
- if (!busy) cap?.setBrightness({ percentage: pct });
47434
+ applyOptimistic({ level: pct });
47435
+ if (!busy) cap?.setBrightness({ level: pct });
47424
47436
  }
47425
47437
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47426
47438
  className: "tabular-nums w-8 text-right",
47427
- children: [display.percentage, "%"]
47439
+ children: [display.level, "%"]
47428
47440
  })]
47429
47441
  });
47430
47442
  }
@@ -47455,17 +47467,17 @@ function FanPanel({ trpc, deviceId, features, layout, optimistic }) {
47455
47467
  min: 0,
47456
47468
  max: 100,
47457
47469
  step,
47458
- value: display.percentage ?? 0,
47470
+ value: display.speed ?? 0,
47459
47471
  disabled: busy,
47460
47472
  onChange: (e) => {
47461
47473
  const raw = Number(e.target.value);
47462
47474
  const pct = Math.min(100, Math.max(0, Math.round(raw / step) * step));
47463
- applyOptimistic({ percentage: pct });
47464
- if (!busy) cap?.setPercentage({ percentage: pct });
47475
+ applyOptimistic({ speed: pct });
47476
+ if (!busy) cap?.setPercentage({ speed: pct });
47465
47477
  }
47466
47478
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
47467
47479
  className: "tabular-nums w-8 text-right",
47468
- children: [display.percentage ?? "—", "%"]
47480
+ children: [display.speed ?? "—", "%"]
47469
47481
  })]
47470
47482
  }), showPreset && display.availablePresets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47471
47483
  className: "flex items-center gap-2 text-xs text-foreground-subtle",
@@ -48553,25 +48565,25 @@ function hasBatteryNumericValue(slice, field) {
48553
48565
  var ROLE_DESCRIPTOR = {
48554
48566
  "temperature-sensor": {
48555
48567
  kind: "numeric",
48556
- field: "celsius",
48568
+ field: "temperature",
48557
48569
  quantity: "temperature",
48558
48570
  getSliceHandle: (s) => s.temperatureSensor
48559
48571
  },
48560
48572
  "humidity-sensor": {
48561
48573
  kind: "numeric",
48562
- field: "percent",
48574
+ field: "humidity",
48563
48575
  quantity: "humidity",
48564
48576
  getSliceHandle: (s) => s.humiditySensor
48565
48577
  },
48566
48578
  "ambient-light-sensor": {
48567
48579
  kind: "numeric",
48568
- field: "lux",
48580
+ field: "illuminance",
48569
48581
  quantity: "illuminance",
48570
48582
  getSliceHandle: (s) => s.ambientLightSensor
48571
48583
  },
48572
48584
  "pressure-sensor": {
48573
48585
  kind: "numeric",
48574
- field: "hpa",
48586
+ field: "pressure",
48575
48587
  quantity: "pressure",
48576
48588
  getSliceHandle: (s) => s.pressureSensor
48577
48589
  },
@@ -48613,25 +48625,25 @@ var ROLE_DESCRIPTOR = {
48613
48625
  },
48614
48626
  "power-sensor": {
48615
48627
  kind: "numeric",
48616
- field: "watts",
48628
+ field: "power",
48617
48629
  quantity: "power",
48618
48630
  getSliceHandle: (s) => s.powerMeter
48619
48631
  },
48620
48632
  "energy-sensor": {
48621
48633
  kind: "numeric",
48622
- field: "kwhTotal",
48634
+ field: "energy",
48623
48635
  quantity: "energy",
48624
48636
  getSliceHandle: (s) => s.powerMeter
48625
48637
  },
48626
48638
  "voltage-sensor": {
48627
48639
  kind: "numeric",
48628
- field: "volts",
48640
+ field: "voltage",
48629
48641
  quantity: "voltage",
48630
48642
  getSliceHandle: (s) => s.powerMeter
48631
48643
  },
48632
48644
  "current-sensor": {
48633
48645
  kind: "numeric",
48634
- field: "amps",
48646
+ field: "current",
48635
48647
  quantity: "current",
48636
48648
  getSliceHandle: (s) => s.powerMeter
48637
48649
  },
@@ -49403,16 +49415,16 @@ function capSlice(caps, capName) {
49403
49415
  return caps[capName];
49404
49416
  }
49405
49417
  /**
49406
- * A battery group writes `battery.percentage`. The role's usual cap is
49418
+ * A battery group writes `battery.level`. The role's usual cap is
49407
49419
  * `numeric-sensor.value`. Hand the renderer a slice whose `value` is that
49408
49420
  * percentage so one numeric path draws both.
49409
49421
  */
49410
49422
  function batteryPercentageSlice(caps) {
49411
49423
  const battery = capSlice(caps, "battery");
49412
- if (!isRecord$2(battery) || typeof battery["percentage"] !== "number") return void 0;
49424
+ if (!isRecord$2(battery) || typeof battery["level"] !== "number") return void 0;
49413
49425
  return {
49414
49426
  ...battery,
49415
- value: battery["percentage"]
49427
+ value: battery["level"]
49416
49428
  };
49417
49429
  }
49418
49430
  function snapshotSlice(input) {
@@ -50172,17 +50184,17 @@ function VacuumHeroCard({ trpc, deviceId }) {
50172
50184
  children: meta.label
50173
50185
  }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$2, { level: slice.batteryLevel })]
50174
50186
  }),
50175
- slice?.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50187
+ slice?.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50176
50188
  className: "flex items-center gap-2",
50177
50189
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50178
50190
  className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
50179
50191
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50180
50192
  className: "h-full bg-primary transition-[width] duration-500",
50181
- style: { width: `${Math.max(0, Math.min(100, slice.progressPercent))}%` }
50193
+ style: { width: `${Math.max(0, Math.min(100, slice.progress))}%` }
50182
50194
  })
50183
50195
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50184
50196
  className: "tabular-nums text-xs text-foreground-subtle",
50185
- children: [Math.round(slice.progressPercent), "%"]
50197
+ children: [Math.round(slice.progress), "%"]
50186
50198
  })]
50187
50199
  }),
50188
50200
  slice?.errorCode != null && slice.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -50691,7 +50703,7 @@ function LawnMowerHeroCard({ trpc, deviceId }) {
50691
50703
  children: meta.label
50692
50704
  }), slice?.batteryLevel != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BatteryBadge$1, { level: slice.batteryLevel })]
50693
50705
  }),
50694
- slice?.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, { percent: slice.progressPercent }),
50706
+ slice?.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressBar, { percent: slice.progress }),
50695
50707
  slice?.currentCode != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeLine, {
50696
50708
  code: slice.currentCode,
50697
50709
  label: slice.currentCodeLabel,
@@ -53342,9 +53354,9 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
53342
53354
  title: display.program,
53343
53355
  children: featureLabel(display.program)
53344
53356
  }),
53345
- display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53357
+ display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53346
53358
  className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
53347
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remainingSeconds, t)]
53359
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remaining, t)]
53348
53360
  }),
53349
53361
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
53350
53362
  label: t("uiLibrary.dishwasher.start", "Start"),
@@ -53370,7 +53382,7 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
53370
53382
  className: "flex items-start gap-4",
53371
53383
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
53372
53384
  running,
53373
- progressPercent: display.progressPercent
53385
+ progressPercent: display.progress
53374
53386
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53375
53387
  className: "flex min-w-0 flex-1 flex-col gap-2",
53376
53388
  children: [
@@ -53389,25 +53401,25 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
53389
53401
  title: display.program,
53390
53402
  children: featureLabel(display.program)
53391
53403
  }),
53392
- display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53404
+ display.remaining != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53393
53405
  className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
53394
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remainingSeconds, t) })]
53406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remaining, t) })]
53395
53407
  }),
53396
- display.delayedStartSeconds != null && display.delayedStartSeconds > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53408
+ display.delayedStart != null && display.delayedStart > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53397
53409
  className: "text-xs text-foreground-subtle tabular-nums",
53398
- children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStartSeconds, t) })
53410
+ children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStart, t) })
53399
53411
  }),
53400
- display.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53412
+ display.progress != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53401
53413
  className: "flex items-center gap-2",
53402
53414
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
53403
53415
  className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
53404
53416
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
53405
53417
  className: "h-full bg-primary transition-[width] duration-500",
53406
- style: { width: `${Math.max(0, Math.min(100, display.progressPercent))}%` }
53418
+ style: { width: `${Math.max(0, Math.min(100, display.progress))}%` }
53407
53419
  })
53408
53420
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53409
53421
  className: "tabular-nums text-xs text-foreground-subtle",
53410
- children: [Math.round(display.progressPercent), "%"]
53422
+ children: [Math.round(display.progress), "%"]
53411
53423
  })]
53412
53424
  }),
53413
53425
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -54802,12 +54814,12 @@ function LightHeroCard({ trpc, deviceId, features, optimistic }) {
54802
54814
  if (swOpt.display === void 0) return null;
54803
54815
  const on = swOpt.display.on === true;
54804
54816
  const swBusy = !sw.available;
54805
- const percentage = brightOpt.display?.percentage ?? 0;
54817
+ const percentage = brightOpt.display?.level ?? 0;
54806
54818
  const brightBusy = !bright.available;
54807
54819
  const fraction = on ? percentage / 100 : 0;
54808
54820
  const arcColor = on ? ARC_COLOR_ON$1 : ARC_COLOR_OFF$1;
54809
54821
  const supportsCt = (features?.includes(_camstack_types.DeviceFeature.LightColorMired) ?? false) && color.cap !== void 0;
54810
- const ctMireds = colorOpt.display?.mireds;
54822
+ const ctMireds = colorOpt.display?.colorTemperature;
54811
54823
  const ctBusy = !color.available;
54812
54824
  const handleToggle = () => {
54813
54825
  if (swBusy) return;
@@ -54852,8 +54864,8 @@ function LightHeroCard({ trpc, deviceId, features, optimistic }) {
54852
54864
  disabled: brightBusy || !on,
54853
54865
  onChange: (e) => {
54854
54866
  const pct = Number(e.target.value);
54855
- brightOpt.applyOptimistic({ percentage: pct });
54856
- if (!brightBusy) bright.cap?.setBrightness({ percentage: pct });
54867
+ brightOpt.applyOptimistic({ level: pct });
54868
+ if (!brightBusy) bright.cap?.setBrightness({ level: pct });
54857
54869
  }
54858
54870
  }),
54859
54871
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -54900,11 +54912,11 @@ function LightHeroCard({ trpc, deviceId, features, optimistic }) {
54900
54912
  const mireds = Number(e.target.value);
54901
54913
  colorOpt.applyOptimistic({
54902
54914
  mode: "mired",
54903
- mireds
54915
+ colorTemperature: mireds
54904
54916
  });
54905
54917
  if (!ctBusy) color.cap?.setColor({ color: {
54906
54918
  mode: "mired",
54907
- mireds
54919
+ colorTemperature: mireds
54908
54920
  } });
54909
54921
  }
54910
54922
  }),
@@ -54964,7 +54976,7 @@ function FanHeroCard({ trpc, deviceId, features, optimistic }) {
54964
54976
  if (swOpt.display === void 0) return null;
54965
54977
  const on = swOpt.display.on === true;
54966
54978
  const swBusy = !sw.available;
54967
- const percentage = fanOpt.display?.percentage ?? 0;
54979
+ const percentage = fanOpt.display?.speed ?? 0;
54968
54980
  const fanBusy = !fan.available;
54969
54981
  const fraction = on ? percentage / 100 : 0;
54970
54982
  const arcColor = on ? ARC_COLOR_ON : ARC_COLOR_OFF;
@@ -54993,8 +55005,8 @@ function FanHeroCard({ trpc, deviceId, features, optimistic }) {
54993
55005
  if (slice === void 0) return;
54994
55006
  const step = slice.percentageStep !== void 0 && slice.percentageStep > 0 ? slice.percentageStep : 1;
54995
55007
  const pct = Math.min(100, Math.max(0, Math.round(f * 100 / step) * step));
54996
- fanOpt.applyOptimistic({ percentage: pct });
54997
- fan.cap?.setPercentage({ percentage: pct });
55008
+ fanOpt.applyOptimistic({ speed: pct });
55009
+ fan.cap?.setPercentage({ speed: pct });
54998
55010
  };
54999
55011
  const handlePreset = (preset) => {
55000
55012
  if (fanBusy || preset === "") return;
@@ -55216,7 +55228,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55216
55228
  if (display === void 0) return null;
55217
55229
  const busy = !available;
55218
55230
  const action = toggleAction(display.state);
55219
- const supportsSeek = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerSeek) ?? false) && display.durationMs !== null && display.durationMs > 0;
55231
+ const supportsSeek = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerSeek) ?? false) && display.duration !== null && display.duration > 0;
55220
55232
  const supportsNext = features?.includes(_camstack_types.DeviceFeature.MediaPlayerNext) ?? false;
55221
55233
  const supportsPrevious = features?.includes(_camstack_types.DeviceFeature.MediaPlayerPrevious) ?? false;
55222
55234
  const supportsVolume = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerVolume) ?? false) && display.volumeLevel !== null;
@@ -55231,7 +55243,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55231
55243
  }
55232
55244
  };
55233
55245
  const subtitle = display.currentMedia?.artist ?? display.currentMedia?.album ?? null;
55234
- const seekFraction = progressFraction(display.positionMs, display.durationMs);
55246
+ const seekFraction = progressFraction(display.position, display.duration);
55235
55247
  const volumeLevel = display.volumeLevel ?? 0;
55236
55248
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55237
55249
  className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-6",
@@ -55254,16 +55266,16 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55254
55266
  "aria-label": t("uiLibrary.mediaPlayerHero.seek", "Seek"),
55255
55267
  type: "range",
55256
55268
  min: 0,
55257
- max: display.durationMs ?? 0,
55269
+ max: display.duration ?? 0,
55258
55270
  step: 1e3,
55259
55271
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
55260
- value: display.positionMs ?? 0,
55272
+ value: display.position ?? 0,
55261
55273
  disabled: busy,
55262
55274
  onChange: (e) => {
55263
55275
  if (busy) return;
55264
55276
  const positionMs = Number(e.target.value);
55265
- applyOptimistic({ positionMs });
55266
- cap.seek({ positionMs });
55277
+ applyOptimistic({ position: positionMs });
55278
+ cap.seek({ position: positionMs });
55267
55279
  }
55268
55280
  }),
55269
55281
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -55273,7 +55285,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55273
55285
  ]
55274
55286
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55275
55287
  className: "flex justify-between text-[11px] tabular-nums text-foreground-subtle",
55276
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(display.positionMs) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(display.durationMs) })]
55288
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(display.position) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(display.duration) })]
55277
55289
  })]
55278
55290
  }),
55279
55291
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
package/dist/index.js CHANGED
@@ -12794,8 +12794,14 @@ function useDropdownContext() {
12794
12794
  if (!ctx) throw new Error("Dropdown compound components must be used within <Dropdown>");
12795
12795
  return ctx;
12796
12796
  }
12797
- function Dropdown({ children }) {
12798
- const [open, setOpen] = useState(false);
12797
+ function Dropdown({ children, open: controlledOpen, onOpenChange }) {
12798
+ const [innerOpen, setInnerOpen] = useState(false);
12799
+ const controlled = controlledOpen !== void 0 && onOpenChange !== void 0;
12800
+ const open = controlled ? controlledOpen : innerOpen;
12801
+ const setOpen = useCallback((next) => {
12802
+ if (controlled) onOpenChange(next);
12803
+ else setInnerOpen(next);
12804
+ }, [controlled, onOpenChange]);
12799
12805
  const triggerId = useId();
12800
12806
  const contentId = useId();
12801
12807
  const triggerRef = useRef(null);
@@ -13986,7 +13992,13 @@ function OptionSelect(props) {
13986
13992
  const checked = mode === "single" ? props.value === opt.value : props.value.includes(opt.value);
13987
13993
  const blockedByMax = mode === "multi" && !checked && atMax === true;
13988
13994
  const optionDisabled = opt.disabled === true || blockedByMax;
13989
- return /* @__PURE__ */ jsxs("button", {
13995
+ const previous = dropdownRows[i - 1];
13996
+ const previousGroup = previous !== void 0 && previous.kind === "option" ? previous.opt.group : void 0;
13997
+ return /* @__PURE__ */ jsxs(Fragment, { children: [opt.group !== void 0 && opt.group !== previousGroup ? /* @__PURE__ */ jsx("div", {
13998
+ role: "presentation",
13999
+ className: "px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wide text-foreground-subtle",
14000
+ children: opt.group
14001
+ }) : null, /* @__PURE__ */ jsxs("button", {
13990
14002
  type: "button",
13991
14003
  role: "option",
13992
14004
  "aria-selected": checked,
@@ -14014,7 +14026,7 @@ function OptionSelect(props) {
14014
14026
  children: opt.badge
14015
14027
  }) : null
14016
14028
  ]
14017
- }, opt.value);
14029
+ })] }, opt.value);
14018
14030
  }) });
14019
14031
  const footerAddNewIndex = dropdownRows.length;
14020
14032
  const fieldShowsChoice = mode === "single" && props.singleDisplay === "field" && selectedOptions.length > 0;
@@ -35970,7 +35982,7 @@ function levelColor(percentage, charging) {
35970
35982
  function BatteryBadge({ status, variant = "compact", className }) {
35971
35983
  const t = useUiText();
35972
35984
  if (!status) return null;
35973
- const { percentage, charging, sleeping, binary } = status;
35985
+ const { level: percentage, charging, sleeping, binary } = status;
35974
35986
  if (percentage === null) return /* @__PURE__ */ jsxs("span", {
35975
35987
  className: cn("inline-flex items-center gap-1 text-[10px] font-medium text-foreground-subtle", className),
35976
35988
  title: t("uiLibrary.batteryBadge.titleUnknown", "Battery: unknown"),
@@ -36133,15 +36145,15 @@ function NetworkLinkBadge({ status, className }) {
36133
36145
  title: t("uiLibrary.networkLinkBadge.ethernet", "Network: ethernet"),
36134
36146
  children: /* @__PURE__ */ jsx(Cable, { className: "h-3.5 w-3.5" })
36135
36147
  });
36136
- const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signalPercent);
36137
- const signal = status.signalPercent === null ? null : Math.round(status.signalPercent);
36148
+ const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signal);
36149
+ const signal = status.signal === null ? null : Math.round(status.signal);
36138
36150
  const networkText = status.ssid ? t("uiLibrary.networkLinkBadge.typeSsid", "Network: {{type}} ({{ssid}})", {
36139
36151
  type: status.type,
36140
36152
  ssid: status.ssid
36141
36153
  }) : t("uiLibrary.networkLinkBadge.type", "Network: {{type}}", { type: status.type });
36142
36154
  const title = signal === null ? networkText : `${networkText} · ${t("uiLibrary.networkLinkBadge.signal", "signal {{percent}}%", { percent: signal })}`;
36143
36155
  return /* @__PURE__ */ jsxs("span", {
36144
- className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(status.signalPercent), className),
36156
+ className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(status.signal), className),
36145
36157
  title,
36146
36158
  children: [/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), signal !== null && /* @__PURE__ */ jsxs("span", { children: [signal, "%"] })]
36147
36159
  });
@@ -47391,16 +47403,16 @@ function BrightnessPanel({ trpc, deviceId, layout, optimistic }) {
47391
47403
  min: 0,
47392
47404
  max: 100,
47393
47405
  step: 1,
47394
- value: display.percentage,
47406
+ value: display.level,
47395
47407
  disabled: busy,
47396
47408
  onChange: (e) => {
47397
47409
  const pct = Number(e.target.value);
47398
- applyOptimistic({ percentage: pct });
47399
- if (!busy) cap?.setBrightness({ percentage: pct });
47410
+ applyOptimistic({ level: pct });
47411
+ if (!busy) cap?.setBrightness({ level: pct });
47400
47412
  }
47401
47413
  }), /* @__PURE__ */ jsxs("span", {
47402
47414
  className: "tabular-nums w-8 text-right",
47403
- children: [display.percentage, "%"]
47415
+ children: [display.level, "%"]
47404
47416
  })]
47405
47417
  });
47406
47418
  }
@@ -47431,17 +47443,17 @@ function FanPanel({ trpc, deviceId, features, layout, optimistic }) {
47431
47443
  min: 0,
47432
47444
  max: 100,
47433
47445
  step,
47434
- value: display.percentage ?? 0,
47446
+ value: display.speed ?? 0,
47435
47447
  disabled: busy,
47436
47448
  onChange: (e) => {
47437
47449
  const raw = Number(e.target.value);
47438
47450
  const pct = Math.min(100, Math.max(0, Math.round(raw / step) * step));
47439
- applyOptimistic({ percentage: pct });
47440
- if (!busy) cap?.setPercentage({ percentage: pct });
47451
+ applyOptimistic({ speed: pct });
47452
+ if (!busy) cap?.setPercentage({ speed: pct });
47441
47453
  }
47442
47454
  }), /* @__PURE__ */ jsxs("span", {
47443
47455
  className: "tabular-nums w-8 text-right",
47444
- children: [display.percentage ?? "—", "%"]
47456
+ children: [display.speed ?? "—", "%"]
47445
47457
  })]
47446
47458
  }), showPreset && display.availablePresets.length > 0 && /* @__PURE__ */ jsx("div", {
47447
47459
  className: "flex items-center gap-2 text-xs text-foreground-subtle",
@@ -48529,25 +48541,25 @@ function hasBatteryNumericValue(slice, field) {
48529
48541
  var ROLE_DESCRIPTOR = {
48530
48542
  "temperature-sensor": {
48531
48543
  kind: "numeric",
48532
- field: "celsius",
48544
+ field: "temperature",
48533
48545
  quantity: "temperature",
48534
48546
  getSliceHandle: (s) => s.temperatureSensor
48535
48547
  },
48536
48548
  "humidity-sensor": {
48537
48549
  kind: "numeric",
48538
- field: "percent",
48550
+ field: "humidity",
48539
48551
  quantity: "humidity",
48540
48552
  getSliceHandle: (s) => s.humiditySensor
48541
48553
  },
48542
48554
  "ambient-light-sensor": {
48543
48555
  kind: "numeric",
48544
- field: "lux",
48556
+ field: "illuminance",
48545
48557
  quantity: "illuminance",
48546
48558
  getSliceHandle: (s) => s.ambientLightSensor
48547
48559
  },
48548
48560
  "pressure-sensor": {
48549
48561
  kind: "numeric",
48550
- field: "hpa",
48562
+ field: "pressure",
48551
48563
  quantity: "pressure",
48552
48564
  getSliceHandle: (s) => s.pressureSensor
48553
48565
  },
@@ -48589,25 +48601,25 @@ var ROLE_DESCRIPTOR = {
48589
48601
  },
48590
48602
  "power-sensor": {
48591
48603
  kind: "numeric",
48592
- field: "watts",
48604
+ field: "power",
48593
48605
  quantity: "power",
48594
48606
  getSliceHandle: (s) => s.powerMeter
48595
48607
  },
48596
48608
  "energy-sensor": {
48597
48609
  kind: "numeric",
48598
- field: "kwhTotal",
48610
+ field: "energy",
48599
48611
  quantity: "energy",
48600
48612
  getSliceHandle: (s) => s.powerMeter
48601
48613
  },
48602
48614
  "voltage-sensor": {
48603
48615
  kind: "numeric",
48604
- field: "volts",
48616
+ field: "voltage",
48605
48617
  quantity: "voltage",
48606
48618
  getSliceHandle: (s) => s.powerMeter
48607
48619
  },
48608
48620
  "current-sensor": {
48609
48621
  kind: "numeric",
48610
- field: "amps",
48622
+ field: "current",
48611
48623
  quantity: "current",
48612
48624
  getSliceHandle: (s) => s.powerMeter
48613
48625
  },
@@ -49379,16 +49391,16 @@ function capSlice(caps, capName) {
49379
49391
  return caps[capName];
49380
49392
  }
49381
49393
  /**
49382
- * A battery group writes `battery.percentage`. The role's usual cap is
49394
+ * A battery group writes `battery.level`. The role's usual cap is
49383
49395
  * `numeric-sensor.value`. Hand the renderer a slice whose `value` is that
49384
49396
  * percentage so one numeric path draws both.
49385
49397
  */
49386
49398
  function batteryPercentageSlice(caps) {
49387
49399
  const battery = capSlice(caps, "battery");
49388
- if (!isRecord$2(battery) || typeof battery["percentage"] !== "number") return void 0;
49400
+ if (!isRecord$2(battery) || typeof battery["level"] !== "number") return void 0;
49389
49401
  return {
49390
49402
  ...battery,
49391
- value: battery["percentage"]
49403
+ value: battery["level"]
49392
49404
  };
49393
49405
  }
49394
49406
  function snapshotSlice(input) {
@@ -50148,17 +50160,17 @@ function VacuumHeroCard({ trpc, deviceId }) {
50148
50160
  children: meta.label
50149
50161
  }), slice?.batteryLevel != null && /* @__PURE__ */ jsx(BatteryBadge$2, { level: slice.batteryLevel })]
50150
50162
  }),
50151
- slice?.progressPercent != null && /* @__PURE__ */ jsxs("div", {
50163
+ slice?.progress != null && /* @__PURE__ */ jsxs("div", {
50152
50164
  className: "flex items-center gap-2",
50153
50165
  children: [/* @__PURE__ */ jsx("div", {
50154
50166
  className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
50155
50167
  children: /* @__PURE__ */ jsx("div", {
50156
50168
  className: "h-full bg-primary transition-[width] duration-500",
50157
- style: { width: `${Math.max(0, Math.min(100, slice.progressPercent))}%` }
50169
+ style: { width: `${Math.max(0, Math.min(100, slice.progress))}%` }
50158
50170
  })
50159
50171
  }), /* @__PURE__ */ jsxs("span", {
50160
50172
  className: "tabular-nums text-xs text-foreground-subtle",
50161
- children: [Math.round(slice.progressPercent), "%"]
50173
+ children: [Math.round(slice.progress), "%"]
50162
50174
  })]
50163
50175
  }),
50164
50176
  slice?.errorCode != null && slice.errorCode !== 0 && /* @__PURE__ */ jsxs("div", {
@@ -50667,7 +50679,7 @@ function LawnMowerHeroCard({ trpc, deviceId }) {
50667
50679
  children: meta.label
50668
50680
  }), slice?.batteryLevel != null && /* @__PURE__ */ jsx(BatteryBadge$1, { level: slice.batteryLevel })]
50669
50681
  }),
50670
- slice?.progressPercent != null && /* @__PURE__ */ jsx(ProgressBar, { percent: slice.progressPercent }),
50682
+ slice?.progress != null && /* @__PURE__ */ jsx(ProgressBar, { percent: slice.progress }),
50671
50683
  slice?.currentCode != null && /* @__PURE__ */ jsx(CodeLine, {
50672
50684
  code: slice.currentCode,
50673
50685
  label: slice.currentCodeLabel,
@@ -53318,9 +53330,9 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
53318
53330
  title: display.program,
53319
53331
  children: featureLabel(display.program)
53320
53332
  }),
53321
- display.remainingSeconds != null && /* @__PURE__ */ jsxs("span", {
53333
+ display.remaining != null && /* @__PURE__ */ jsxs("span", {
53322
53334
  className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
53323
- children: [/* @__PURE__ */ jsx(Timer, { className: "h-3 w-3" }), formatDuration(display.remainingSeconds, t)]
53335
+ children: [/* @__PURE__ */ jsx(Timer, { className: "h-3 w-3" }), formatDuration(display.remaining, t)]
53324
53336
  }),
53325
53337
  /* @__PURE__ */ jsx(DishwasherIconButton, {
53326
53338
  label: t("uiLibrary.dishwasher.start", "Start"),
@@ -53346,7 +53358,7 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
53346
53358
  className: "flex items-start gap-4",
53347
53359
  children: [/* @__PURE__ */ jsx(DishwasherGraphic, {
53348
53360
  running,
53349
- progressPercent: display.progressPercent
53361
+ progressPercent: display.progress
53350
53362
  }), /* @__PURE__ */ jsxs("div", {
53351
53363
  className: "flex min-w-0 flex-1 flex-col gap-2",
53352
53364
  children: [
@@ -53365,25 +53377,25 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
53365
53377
  title: display.program,
53366
53378
  children: featureLabel(display.program)
53367
53379
  }),
53368
- display.remainingSeconds != null && /* @__PURE__ */ jsxs("span", {
53380
+ display.remaining != null && /* @__PURE__ */ jsxs("span", {
53369
53381
  className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
53370
- children: [/* @__PURE__ */ jsx(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remainingSeconds, t) })]
53382
+ children: [/* @__PURE__ */ jsx(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remaining, t) })]
53371
53383
  }),
53372
- display.delayedStartSeconds != null && display.delayedStartSeconds > 0 && /* @__PURE__ */ jsx("span", {
53384
+ display.delayedStart != null && display.delayedStart > 0 && /* @__PURE__ */ jsx("span", {
53373
53385
  className: "text-xs text-foreground-subtle tabular-nums",
53374
- children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStartSeconds, t) })
53386
+ children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStart, t) })
53375
53387
  }),
53376
- display.progressPercent != null && /* @__PURE__ */ jsxs("div", {
53388
+ display.progress != null && /* @__PURE__ */ jsxs("div", {
53377
53389
  className: "flex items-center gap-2",
53378
53390
  children: [/* @__PURE__ */ jsx("div", {
53379
53391
  className: "h-1.5 flex-1 rounded bg-surface-hover overflow-hidden",
53380
53392
  children: /* @__PURE__ */ jsx("div", {
53381
53393
  className: "h-full bg-primary transition-[width] duration-500",
53382
- style: { width: `${Math.max(0, Math.min(100, display.progressPercent))}%` }
53394
+ style: { width: `${Math.max(0, Math.min(100, display.progress))}%` }
53383
53395
  })
53384
53396
  }), /* @__PURE__ */ jsxs("span", {
53385
53397
  className: "tabular-nums text-xs text-foreground-subtle",
53386
- children: [Math.round(display.progressPercent), "%"]
53398
+ children: [Math.round(display.progress), "%"]
53387
53399
  })]
53388
53400
  }),
53389
53401
  /* @__PURE__ */ jsxs("div", {
@@ -54778,12 +54790,12 @@ function LightHeroCard({ trpc, deviceId, features, optimistic }) {
54778
54790
  if (swOpt.display === void 0) return null;
54779
54791
  const on = swOpt.display.on === true;
54780
54792
  const swBusy = !sw.available;
54781
- const percentage = brightOpt.display?.percentage ?? 0;
54793
+ const percentage = brightOpt.display?.level ?? 0;
54782
54794
  const brightBusy = !bright.available;
54783
54795
  const fraction = on ? percentage / 100 : 0;
54784
54796
  const arcColor = on ? ARC_COLOR_ON$1 : ARC_COLOR_OFF$1;
54785
54797
  const supportsCt = (features?.includes(DeviceFeature.LightColorMired) ?? false) && color.cap !== void 0;
54786
- const ctMireds = colorOpt.display?.mireds;
54798
+ const ctMireds = colorOpt.display?.colorTemperature;
54787
54799
  const ctBusy = !color.available;
54788
54800
  const handleToggle = () => {
54789
54801
  if (swBusy) return;
@@ -54828,8 +54840,8 @@ function LightHeroCard({ trpc, deviceId, features, optimistic }) {
54828
54840
  disabled: brightBusy || !on,
54829
54841
  onChange: (e) => {
54830
54842
  const pct = Number(e.target.value);
54831
- brightOpt.applyOptimistic({ percentage: pct });
54832
- if (!brightBusy) bright.cap?.setBrightness({ percentage: pct });
54843
+ brightOpt.applyOptimistic({ level: pct });
54844
+ if (!brightBusy) bright.cap?.setBrightness({ level: pct });
54833
54845
  }
54834
54846
  }),
54835
54847
  /* @__PURE__ */ jsxs("span", {
@@ -54876,11 +54888,11 @@ function LightHeroCard({ trpc, deviceId, features, optimistic }) {
54876
54888
  const mireds = Number(e.target.value);
54877
54889
  colorOpt.applyOptimistic({
54878
54890
  mode: "mired",
54879
- mireds
54891
+ colorTemperature: mireds
54880
54892
  });
54881
54893
  if (!ctBusy) color.cap?.setColor({ color: {
54882
54894
  mode: "mired",
54883
- mireds
54895
+ colorTemperature: mireds
54884
54896
  } });
54885
54897
  }
54886
54898
  }),
@@ -54940,7 +54952,7 @@ function FanHeroCard({ trpc, deviceId, features, optimistic }) {
54940
54952
  if (swOpt.display === void 0) return null;
54941
54953
  const on = swOpt.display.on === true;
54942
54954
  const swBusy = !sw.available;
54943
- const percentage = fanOpt.display?.percentage ?? 0;
54955
+ const percentage = fanOpt.display?.speed ?? 0;
54944
54956
  const fanBusy = !fan.available;
54945
54957
  const fraction = on ? percentage / 100 : 0;
54946
54958
  const arcColor = on ? ARC_COLOR_ON : ARC_COLOR_OFF;
@@ -54969,8 +54981,8 @@ function FanHeroCard({ trpc, deviceId, features, optimistic }) {
54969
54981
  if (slice === void 0) return;
54970
54982
  const step = slice.percentageStep !== void 0 && slice.percentageStep > 0 ? slice.percentageStep : 1;
54971
54983
  const pct = Math.min(100, Math.max(0, Math.round(f * 100 / step) * step));
54972
- fanOpt.applyOptimistic({ percentage: pct });
54973
- fan.cap?.setPercentage({ percentage: pct });
54984
+ fanOpt.applyOptimistic({ speed: pct });
54985
+ fan.cap?.setPercentage({ speed: pct });
54974
54986
  };
54975
54987
  const handlePreset = (preset) => {
54976
54988
  if (fanBusy || preset === "") return;
@@ -55192,7 +55204,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55192
55204
  if (display === void 0) return null;
55193
55205
  const busy = !available;
55194
55206
  const action = toggleAction(display.state);
55195
- const supportsSeek = (features?.includes(DeviceFeature.MediaPlayerSeek) ?? false) && display.durationMs !== null && display.durationMs > 0;
55207
+ const supportsSeek = (features?.includes(DeviceFeature.MediaPlayerSeek) ?? false) && display.duration !== null && display.duration > 0;
55196
55208
  const supportsNext = features?.includes(DeviceFeature.MediaPlayerNext) ?? false;
55197
55209
  const supportsPrevious = features?.includes(DeviceFeature.MediaPlayerPrevious) ?? false;
55198
55210
  const supportsVolume = (features?.includes(DeviceFeature.MediaPlayerVolume) ?? false) && display.volumeLevel !== null;
@@ -55207,7 +55219,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55207
55219
  }
55208
55220
  };
55209
55221
  const subtitle = display.currentMedia?.artist ?? display.currentMedia?.album ?? null;
55210
- const seekFraction = progressFraction(display.positionMs, display.durationMs);
55222
+ const seekFraction = progressFraction(display.position, display.duration);
55211
55223
  const volumeLevel = display.volumeLevel ?? 0;
55212
55224
  return /* @__PURE__ */ jsxs("div", {
55213
55225
  className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-6",
@@ -55230,16 +55242,16 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55230
55242
  "aria-label": t("uiLibrary.mediaPlayerHero.seek", "Seek"),
55231
55243
  type: "range",
55232
55244
  min: 0,
55233
- max: display.durationMs ?? 0,
55245
+ max: display.duration ?? 0,
55234
55246
  step: 1e3,
55235
55247
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
55236
- value: display.positionMs ?? 0,
55248
+ value: display.position ?? 0,
55237
55249
  disabled: busy,
55238
55250
  onChange: (e) => {
55239
55251
  if (busy) return;
55240
55252
  const positionMs = Number(e.target.value);
55241
- applyOptimistic({ positionMs });
55242
- cap.seek({ positionMs });
55253
+ applyOptimistic({ position: positionMs });
55254
+ cap.seek({ position: positionMs });
55243
55255
  }
55244
55256
  }),
55245
55257
  /* @__PURE__ */ jsx("div", {
@@ -55249,7 +55261,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
55249
55261
  ]
55250
55262
  }), /* @__PURE__ */ jsxs("div", {
55251
55263
  className: "flex justify-between text-[11px] tabular-nums text-foreground-subtle",
55252
- children: [/* @__PURE__ */ jsx("span", { children: formatClock(display.positionMs) }), /* @__PURE__ */ jsx("span", { children: formatClock(display.durationMs) })]
55264
+ children: [/* @__PURE__ */ jsx("span", { children: formatClock(display.position) }), /* @__PURE__ */ jsx("span", { children: formatClock(display.duration) })]
55253
55265
  })]
55254
55266
  }),
55255
55267
  /* @__PURE__ */ jsxs("div", {
@@ -1,8 +1,16 @@
1
1
  import { type JSX, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
2
2
  import { type LucideIcon } from "lucide-react";
3
- export declare function Dropdown({ children }: {
4
- children: ReactNode;
5
- }): JSX.Element | null;
3
+ export interface DropdownProps {
4
+ readonly children: ReactNode;
5
+ /**
6
+ * Controlled open state. Give both `open` and `onOpenChange` to drive the menu
7
+ * from outside (a "{" typed in a field opens the variable list); give neither
8
+ * and the dropdown keeps its own state.
9
+ */
10
+ readonly open?: boolean;
11
+ readonly onOpenChange?: (open: boolean) => void;
12
+ }
13
+ export declare function Dropdown({ children, open: controlledOpen, onOpenChange }: DropdownProps): JSX.Element | null;
6
14
  export declare function DropdownTrigger({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>): JSX.Element | null;
7
15
  interface DropdownContentProps extends HTMLAttributes<HTMLDivElement> {
8
16
  /** Horizontal alignment relative to the trigger. Default: 'start' (left-aligned). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.341",
3
+ "version": "1.2.343",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",