@camstack/ui-library 1.2.375 → 1.2.377

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
@@ -3471,6 +3471,58 @@ var RefreshCw = require_createLucideIcon.createLucideIcon("refresh-cw", [
3471
3471
  * This source code is licensed under the ISC license.
3472
3472
  * See the LICENSE file in the root directory of this source tree.
3473
3473
  */
3474
+ var Repeat1 = require_createLucideIcon.createLucideIcon("repeat-1", [
3475
+ ["path", {
3476
+ d: "m17 2 4 4-4 4",
3477
+ key: "nntrym"
3478
+ }],
3479
+ ["path", {
3480
+ d: "M3 11v-1a4 4 0 0 1 4-4h14",
3481
+ key: "84bu3i"
3482
+ }],
3483
+ ["path", {
3484
+ d: "m7 22-4-4 4-4",
3485
+ key: "1wqhfi"
3486
+ }],
3487
+ ["path", {
3488
+ d: "M21 13v1a4 4 0 0 1-4 4H3",
3489
+ key: "1rx37r"
3490
+ }],
3491
+ ["path", {
3492
+ d: "M11 10h1v4",
3493
+ key: "70cz1p"
3494
+ }]
3495
+ ]);
3496
+ /**
3497
+ * @license lucide-react v0.576.0 - ISC
3498
+ *
3499
+ * This source code is licensed under the ISC license.
3500
+ * See the LICENSE file in the root directory of this source tree.
3501
+ */
3502
+ var Repeat = require_createLucideIcon.createLucideIcon("repeat", [
3503
+ ["path", {
3504
+ d: "m17 2 4 4-4 4",
3505
+ key: "nntrym"
3506
+ }],
3507
+ ["path", {
3508
+ d: "M3 11v-1a4 4 0 0 1 4-4h14",
3509
+ key: "84bu3i"
3510
+ }],
3511
+ ["path", {
3512
+ d: "m7 22-4-4 4-4",
3513
+ key: "1wqhfi"
3514
+ }],
3515
+ ["path", {
3516
+ d: "M21 13v1a4 4 0 0 1-4 4H3",
3517
+ key: "1rx37r"
3518
+ }]
3519
+ ]);
3520
+ /**
3521
+ * @license lucide-react v0.576.0 - ISC
3522
+ *
3523
+ * This source code is licensed under the ISC license.
3524
+ * See the LICENSE file in the root directory of this source tree.
3525
+ */
3474
3526
  var RotateCcw = require_createLucideIcon.createLucideIcon("rotate-ccw", [["path", {
3475
3527
  d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",
3476
3528
  key: "1357e3"
@@ -3846,6 +3898,34 @@ var Ship = require_createLucideIcon.createLucideIcon("ship", [
3846
3898
  * This source code is licensed under the ISC license.
3847
3899
  * See the LICENSE file in the root directory of this source tree.
3848
3900
  */
3901
+ var Shuffle = require_createLucideIcon.createLucideIcon("shuffle", [
3902
+ ["path", {
3903
+ d: "m18 14 4 4-4 4",
3904
+ key: "10pe0f"
3905
+ }],
3906
+ ["path", {
3907
+ d: "m18 2 4 4-4 4",
3908
+ key: "pucp1d"
3909
+ }],
3910
+ ["path", {
3911
+ d: "M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22",
3912
+ key: "1ailkh"
3913
+ }],
3914
+ ["path", {
3915
+ d: "M2 6h1.972a4 4 0 0 1 3.6 2.2",
3916
+ key: "km57vx"
3917
+ }],
3918
+ ["path", {
3919
+ d: "M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45",
3920
+ key: "os18l9"
3921
+ }]
3922
+ ]);
3923
+ /**
3924
+ * @license lucide-react v0.576.0 - ISC
3925
+ *
3926
+ * This source code is licensed under the ISC license.
3927
+ * See the LICENSE file in the root directory of this source tree.
3928
+ */
3849
3929
  var Siren = require_createLucideIcon.createLucideIcon("siren", [
3850
3930
  ["path", {
3851
3931
  d: "M7 18v-6a5 5 0 1 1 10 0v6",
@@ -5994,7 +6074,7 @@ function lucideExportName(key) {
5994
6074
  function loadIconGlyphs() {
5995
6075
  if (fullTable !== void 0) return Promise.resolve();
5996
6076
  if (failure$1 !== void 0) return Promise.reject(failure$1);
5997
- pending ??= Promise.resolve().then(() => require("./icons-DXwPEKdJ.cjs")).then((module) => {
6077
+ pending ??= Promise.resolve().then(() => require("./icons-BWpVdTJu.cjs")).then((module) => {
5998
6078
  fullTable = module;
5999
6079
  publish();
6000
6080
  }, (error) => {
@@ -9739,14 +9819,15 @@ function BottomSheet({ open, onClose, title, children, className }) {
9739
9819
  document.body.style.overflow = "";
9740
9820
  };
9741
9821
  }, [open, onClose]);
9822
+ if (!open) return null;
9742
9823
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9743
- className: cn("fixed inset-0 z-40 bg-black/50 transition-opacity duration-200", open ? "opacity-100" : "pointer-events-none opacity-0"),
9824
+ className: "fixed inset-0 z-40 bg-black/50 animate-in fade-in duration-200",
9744
9825
  onClick: onClose,
9745
9826
  "aria-hidden": "true"
9746
9827
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9747
9828
  role: "dialog",
9748
9829
  "aria-modal": "true",
9749
- className: cn("fixed inset-x-0 bottom-0 z-50 flex flex-col bg-background-elevated border-t border-border rounded-t-xl shadow-2xl transition-transform duration-200 ease-out pb-[env(safe-area-inset-bottom)]", "max-h-[80dvh]", open ? "translate-y-0" : "translate-y-full", className),
9830
+ className: cn("fixed inset-x-0 bottom-0 z-50 flex flex-col bg-background-elevated border-t border-border rounded-t-xl shadow-2xl animate-in slide-in-from-bottom duration-200 ease-out pb-[env(safe-area-inset-bottom)]", "max-h-[80dvh]", className),
9750
9831
  children: [
9751
9832
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9752
9833
  className: "flex justify-center pt-2 pb-1",
@@ -11852,6 +11933,18 @@ var useLockControlUnlock = /*#__PURE__*/ alias(() => trpc.lockControl.unlock.use
11852
11933
  var useLockControlOpen = /*#__PURE__*/ alias(() => trpc.lockControl.open.useMutation);
11853
11934
  /** Generated alias around `trpc.lockControl.getStatus.useQuery`. */
11854
11935
  var useLockControlGetStatus = /*#__PURE__*/ alias(() => trpc.lockControl.getStatus.useQuery);
11936
+ /** Generated alias around `trpc.mediaPlayer.turnOn.useMutation`. */
11937
+ var useMediaPlayerTurnOn = /*#__PURE__*/ alias(() => trpc.mediaPlayer.turnOn.useMutation);
11938
+ /** Generated alias around `trpc.mediaPlayer.turnOff.useMutation`. */
11939
+ var useMediaPlayerTurnOff = /*#__PURE__*/ alias(() => trpc.mediaPlayer.turnOff.useMutation);
11940
+ /** Generated alias around `trpc.mediaPlayer.volumeUp.useMutation`. */
11941
+ var useMediaPlayerVolumeUp = /*#__PURE__*/ alias(() => trpc.mediaPlayer.volumeUp.useMutation);
11942
+ /** Generated alias around `trpc.mediaPlayer.volumeDown.useMutation`. */
11943
+ var useMediaPlayerVolumeDown = /*#__PURE__*/ alias(() => trpc.mediaPlayer.volumeDown.useMutation);
11944
+ /** Generated alias around `trpc.mediaPlayer.channelUp.useMutation`. */
11945
+ var useMediaPlayerChannelUp = /*#__PURE__*/ alias(() => trpc.mediaPlayer.channelUp.useMutation);
11946
+ /** Generated alias around `trpc.mediaPlayer.channelDown.useMutation`. */
11947
+ var useMediaPlayerChannelDown = /*#__PURE__*/ alias(() => trpc.mediaPlayer.channelDown.useMutation);
11855
11948
  /** Generated alias around `trpc.mediaPlayer.play.useMutation`. */
11856
11949
  var useMediaPlayerPlay = /*#__PURE__*/ alias(() => trpc.mediaPlayer.play.useMutation);
11857
11950
  /** Generated alias around `trpc.mediaPlayer.pause.useMutation`. */
@@ -12826,6 +12919,8 @@ var useRemoteSetConfig = /*#__PURE__*/ alias(() => trpc.remote.setConfig.useMuta
12826
12919
  var useRemoteListCommands = /*#__PURE__*/ alias(() => trpc.remote.listCommands.useQuery);
12827
12920
  /** Generated alias around `trpc.remote.sendCommand.useMutation`. */
12828
12921
  var useRemoteSendCommand = /*#__PURE__*/ alias(() => trpc.remote.sendCommand.useMutation);
12922
+ /** Generated alias around `trpc.remote.getAssumedStatus.useQuery`. */
12923
+ var useRemoteGetAssumedStatus = /*#__PURE__*/ alias(() => trpc.remote.getAssumedStatus.useQuery);
12829
12924
  /** Generated alias around `trpc.remote.sendMediaCommand.useMutation`. */
12830
12925
  var useRemoteSendMediaCommand = /*#__PURE__*/ alias(() => trpc.remote.sendMediaCommand.useMutation);
12831
12926
  /** Generated alias around `trpc.remoteLayoutComponents.listComponents.useQuery`. */
@@ -27746,6 +27841,92 @@ function NavigationPanel({ deviceId }) {
27746
27841
  });
27747
27842
  }
27748
27843
  //#endregion
27844
+ //#region src/composites/cap-settings/PowerMeterLine.tsx
27845
+ /**
27846
+ * PowerMeterLine — the ONE place a device's electrical draw is shown (D1004):
27847
+ * a slim line directly under the device page's hero card, for every device
27848
+ * bound to the `power-meter` capability, whatever its type. The cap declares
27849
+ * `ui: { kind:'widget', widgetId:'host/power-meter-line', placement:'hero' }`;
27850
+ * no device-controls hero card draws watts itself
27851
+ * (`scripts/check-no-hero-self-drawn-power.ts`).
27852
+ *
27853
+ * A reading the device never reported is not drawn as a number: no slice, or
27854
+ * a slice with neither power nor energy, renders nothing; a missing field
27855
+ * (power but no energy, or the reverse) is simply absent, never `0` or `NaN`.
27856
+ */
27857
+ /** A finite number, or null: the one gate between a slice field and the screen. */
27858
+ function known(value) {
27859
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
27860
+ }
27861
+ /** Whole watts below a kilowatt, one decimal above. */
27862
+ function formatPowerWatts(watts) {
27863
+ return Math.abs(watts) >= 1e3 ? {
27864
+ value: (watts / 1e3).toFixed(1),
27865
+ unit: "kW"
27866
+ } : {
27867
+ value: String(Math.round(watts)),
27868
+ unit: "W"
27869
+ };
27870
+ }
27871
+ /** Two decimals under 100 kWh, whole kWh above. */
27872
+ function formatEnergyKwh(kwh) {
27873
+ return Math.abs(kwh) >= 100 ? String(Math.round(kwh)) : kwh.toFixed(2);
27874
+ }
27875
+ function PowerMeterLine({ deviceId }) {
27876
+ const t = useUiText();
27877
+ const { slice } = useDeviceCapability({
27878
+ trpc: useSystem().trpcClient,
27879
+ deviceId,
27880
+ cap: (d) => d.powerMeter,
27881
+ slice: (d) => d.state.powerMeter
27882
+ });
27883
+ const power = known(slice?.power);
27884
+ const energy = known(slice?.energy);
27885
+ if (power === null && energy === null) return null;
27886
+ const drawing = power !== null && power > 0;
27887
+ const shown = power === null ? null : formatPowerWatts(power);
27888
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27889
+ "data-testid": "power-meter-line",
27890
+ className: "flex items-center gap-3 rounded-lg border border-border bg-surface px-3 py-2",
27891
+ children: [
27892
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27893
+ className: "relative flex h-5 w-5 shrink-0 items-center justify-center",
27894
+ children: [drawing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27895
+ "aria-hidden": "true",
27896
+ "data-testid": "power-meter-pulse",
27897
+ className: "absolute inset-0 rounded-full bg-primary/20 motion-safe:animate-pulse"
27898
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
27899
+ className: cn("relative h-3.5 w-3.5", drawing ? "text-primary" : "text-foreground-subtle"),
27900
+ "aria-hidden": true
27901
+ })]
27902
+ }),
27903
+ shown !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27904
+ "data-testid": "power-meter-power",
27905
+ className: "flex items-baseline gap-1 tabular-nums",
27906
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27907
+ className: "text-lg font-semibold text-foreground",
27908
+ children: shown.value
27909
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27910
+ className: "text-xs text-foreground-muted",
27911
+ children: shown.unit
27912
+ })]
27913
+ }),
27914
+ energy !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27915
+ "data-testid": "power-meter-energy",
27916
+ className: "ml-auto flex items-baseline gap-1 tabular-nums",
27917
+ title: t("uiLibrary.powerMeterLine.energy", "Energy"),
27918
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27919
+ className: "text-sm text-foreground-muted",
27920
+ children: formatEnergyKwh(energy)
27921
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27922
+ className: "text-xs text-foreground-subtle",
27923
+ children: "kWh"
27924
+ })]
27925
+ })
27926
+ ]
27927
+ });
27928
+ }
27929
+ //#endregion
27749
27930
  //#region src/lib/cap-error.ts
27750
27931
  /**
27751
27932
  * Shared capability-error utilities for the admin UI.
@@ -30724,6 +30905,7 @@ var HOST_WIDGETS = {
30724
30905
  "host/ptz-panel": PtzPanel,
30725
30906
  "host/navigation-panel": NavigationPanel,
30726
30907
  "host/consumables-panel": ConsumablesPanel,
30908
+ "host/power-meter-line": PowerMeterLine,
30727
30909
  "host/ptz-autotrack": AutotrackSection,
30728
30910
  "host/recording-panel": RecordingPanel,
30729
30911
  "host/camera-grid-layout": CameraGridLayoutEditor,
@@ -36444,7 +36626,8 @@ function MobileDrawer({ open, onClose, children, className, width = "w-64" }) {
36444
36626
  document.body.style.overflow = "";
36445
36627
  };
36446
36628
  }, [open, onClose]);
36447
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36629
+ if (!open) return null;
36630
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36448
36631
  className: "fixed inset-0 z-40 bg-black/50 animate-in fade-in duration-200",
36449
36632
  onClick: onClose,
36450
36633
  "aria-hidden": "true"
@@ -36452,7 +36635,7 @@ function MobileDrawer({ open, onClose, children, className, width = "w-64" }) {
36452
36635
  ref: drawerRef,
36453
36636
  role: "dialog",
36454
36637
  "aria-modal": "true",
36455
- className: cn("fixed inset-y-0 left-0 z-50 flex flex-col bg-surface border-r border-border shadow-2xl transition-transform duration-200 ease-out pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", width, open ? "translate-x-0" : "-translate-x-full shadow-none", className),
36638
+ className: cn("fixed inset-y-0 left-0 z-50 flex flex-col bg-surface border-r border-border shadow-2xl animate-in slide-in-from-left duration-200 ease-out pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", width, className),
36456
36639
  children
36457
36640
  })] });
36458
36641
  }
@@ -36537,7 +36720,7 @@ function AppShell({ sidebar, header, mobileLogo, mobileActions, children, classN
36537
36720
  className: "flex flex-1 flex-col min-w-0",
36538
36721
  children: [
36539
36722
  isMobile && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
36540
- className: "flex items-center h-12 border-b border-border px-3 shrink-0 bg-surface/80 backdrop-blur-sm",
36723
+ className: "flex items-center h-12 border-b border-border px-3 shrink-0 bg-surface",
36541
36724
  children: [
36542
36725
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36543
36726
  onClick: () => setDrawerOpen(true),
@@ -37059,8 +37242,8 @@ function pickWifiIcon(signalPercent) {
37059
37242
  }
37060
37243
  function signalColor(signalPercent) {
37061
37244
  if (signalPercent === null) return "text-foreground-subtle";
37062
- if (signalPercent <= 20) return "text-rose-400";
37063
- if (signalPercent <= 50) return "text-amber-400";
37245
+ if (signalPercent <= 20) return "text-danger";
37246
+ if (signalPercent <= 50) return "text-warning";
37064
37247
  return "text-foreground-subtle";
37065
37248
  }
37066
37249
  function NetworkLinkBadge({ status, className }) {
@@ -37071,15 +37254,16 @@ function NetworkLinkBadge({ status, className }) {
37071
37254
  title: t("uiLibrary.networkLinkBadge.ethernet", "Network: ethernet"),
37072
37255
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cable, { className: "h-3.5 w-3.5" })
37073
37256
  });
37074
- const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signal);
37075
- const signal = status.signal === null ? null : Math.round(status.signal);
37257
+ const reading = typeof status.signal === "number" && Number.isFinite(status.signal) ? status.signal : null;
37258
+ const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(reading);
37259
+ const signal = reading === null ? null : Math.round(reading);
37076
37260
  const networkText = status.ssid ? t("uiLibrary.networkLinkBadge.typeSsid", "Network: {{type}} ({{ssid}})", {
37077
37261
  type: status.type,
37078
37262
  ssid: status.ssid
37079
37263
  }) : t("uiLibrary.networkLinkBadge.type", "Network: {{type}}", { type: status.type });
37080
37264
  const title = signal === null ? networkText : `${networkText} · ${t("uiLibrary.networkLinkBadge.signal", "signal {{percent}}%", { percent: signal })}`;
37081
37265
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
37082
- className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(status.signal), className),
37266
+ className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(reading), className),
37083
37267
  title,
37084
37268
  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, "%"] })]
37085
37269
  });
@@ -48766,29 +48950,179 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
48766
48950
  });
48767
48951
  }
48768
48952
  //#endregion
48953
+ //#region src/composites/device-controls/media-player-view.ts
48954
+ /**
48955
+ * The state → view mapping of the media-player hero card, kept pure so it can
48956
+ * be specified without rendering.
48957
+ *
48958
+ * Two questions are answered separately. POWER is whether the device is on
48959
+ * (`off` / `standby` / on); RUNNING is what a powered device is doing
48960
+ * (`playing` / `paused` / `buffering` / `idle`). A status that never arrived
48961
+ * is `unknown` for both, and an unknown state is never drawn as off: it is
48962
+ * its own look. A value the driver only guesses (infrared has no feedback)
48963
+ * carries an `assumed` flag so the card can draw it as a guess.
48964
+ *
48965
+ * The controls are derived from what the device exposes — the status fields
48966
+ * it fills and the `DeviceFeature` flags it declares — never from a vendor.
48967
+ */
48968
+ var NO_TRANSPORT = {
48969
+ playPause: false,
48970
+ previous: false,
48971
+ next: false,
48972
+ stop: false
48973
+ };
48974
+ /** Whether the status carries any activity the device actually reported. */
48975
+ function runningOf(state) {
48976
+ switch (state) {
48977
+ case "playing":
48978
+ case "paused":
48979
+ case "buffering":
48980
+ case "idle": return state;
48981
+ case void 0: return "unknown";
48982
+ default: return "none";
48983
+ }
48984
+ }
48985
+ function artworkOf(status) {
48986
+ const url = status.currentMedia?.imageUrl?.trim() ?? "";
48987
+ return url === "" ? null : url;
48988
+ }
48989
+ function powerOf(state) {
48990
+ if (state === void 0) return "unknown";
48991
+ if (state === "off") return "off";
48992
+ if (state === "standby") return "standby";
48993
+ return "on";
48994
+ }
48995
+ /**
48996
+ * A slice from before `assumedFields` existed (stateSource assumed, no list)
48997
+ * means the whole state is a guess; with a list, only the named fields are.
48998
+ */
48999
+ function isAssumed(status, field) {
49000
+ if (status.stateSource !== "assumed") return false;
49001
+ const listed = status.assumedFields ?? [];
49002
+ return listed.length === 0 || listed.includes(field);
49003
+ }
49004
+ var UNKNOWN_VIEW = {
49005
+ power: "unknown",
49006
+ runningReading: "unknown",
49007
+ running: "unknown",
49008
+ stateAssumed: false,
49009
+ canSwitchPower: false,
49010
+ transport: NO_TRANSPORT,
49011
+ seek: false,
49012
+ volume: "none",
49013
+ volumeAssumed: false,
49014
+ volumeKnown: false,
49015
+ muteKnown: false,
49016
+ artworkUrl: null,
49017
+ mute: false,
49018
+ muteAssumed: false,
49019
+ source: null,
49020
+ sourceAssumed: false,
49021
+ shuffle: false,
49022
+ repeat: false
49023
+ };
49024
+ function readingOf(running) {
49025
+ if (running === true) return "running";
49026
+ if (running === false) return "idle";
49027
+ return "unknown";
49028
+ }
49029
+ function deriveMediaPlayerView(status, features, extras = {}) {
49030
+ const runningReading = readingOf(extras.running);
49031
+ const view = deriveFromStatus(status, features);
49032
+ const guessed = status === void 0 || status.state === void 0 || view.stateAssumed;
49033
+ if (runningReading === "unknown" || !guessed) return {
49034
+ ...view,
49035
+ runningReading
49036
+ };
49037
+ if (runningReading === "idle") return {
49038
+ ...UNKNOWN_VIEW,
49039
+ power: "off",
49040
+ running: "none",
49041
+ canSwitchPower: view.canSwitchPower,
49042
+ runningReading
49043
+ };
49044
+ return {
49045
+ ...status === void 0 ? view : deriveFromStatus(status.state === void 0 || status.state === "off" || status.state === "standby" ? {
49046
+ ...status,
49047
+ state: "on"
49048
+ } : status, features),
49049
+ power: "on",
49050
+ stateAssumed: false,
49051
+ runningReading
49052
+ };
49053
+ }
49054
+ function deriveFromStatus(status, features) {
49055
+ const has = (f) => features?.includes(f) ?? false;
49056
+ const canSwitchPower = has(_camstack_types.DeviceFeature.MediaPlayerPower);
49057
+ if (status === void 0 || status.state === void 0) return {
49058
+ ...UNKNOWN_VIEW,
49059
+ canSwitchPower
49060
+ };
49061
+ const power = powerOf(status.state);
49062
+ const running = runningOf(status.state);
49063
+ const base = {
49064
+ ...UNKNOWN_VIEW,
49065
+ power,
49066
+ running,
49067
+ stateAssumed: isAssumed(status, "state"),
49068
+ canSwitchPower
49069
+ };
49070
+ if (power !== "on") return base;
49071
+ const hasTransportFeature = has(_camstack_types.DeviceFeature.MediaPlayerNext) || has(_camstack_types.DeviceFeature.MediaPlayerPrevious) || has(_camstack_types.DeviceFeature.MediaPlayerStop) || has(_camstack_types.DeviceFeature.MediaPlayerSeek);
49072
+ const volume = has(_camstack_types.DeviceFeature.MediaPlayerVolume) ? "slider" : has(_camstack_types.DeviceFeature.MediaPlayerVolumeStep) ? "steps" : "none";
49073
+ const sourceName = typeof status.source === "string" ? status.source.trim() : "";
49074
+ const options = status.availableSources ?? [];
49075
+ const hasSource = sourceName !== "" || options.length > 0;
49076
+ return {
49077
+ ...base,
49078
+ transport: {
49079
+ playPause: running !== "none" || hasTransportFeature,
49080
+ previous: has(_camstack_types.DeviceFeature.MediaPlayerPrevious),
49081
+ next: has(_camstack_types.DeviceFeature.MediaPlayerNext),
49082
+ stop: has(_camstack_types.DeviceFeature.MediaPlayerStop)
49083
+ },
49084
+ seek: has(_camstack_types.DeviceFeature.MediaPlayerSeek) && status.duration !== null && status.duration > 0,
49085
+ volume,
49086
+ volumeAssumed: isAssumed(status, "volumeLevel"),
49087
+ mute: has(_camstack_types.DeviceFeature.MediaPlayerMute) || volume === "slider" && status.isMuted !== null,
49088
+ volumeKnown: status.volumeLevel !== null,
49089
+ muteKnown: status.isMuted !== null,
49090
+ artworkUrl: artworkOf(status),
49091
+ muteAssumed: isAssumed(status, "isMuted"),
49092
+ source: hasSource ? {
49093
+ current: sourceName,
49094
+ options,
49095
+ selectable: has(_camstack_types.DeviceFeature.MediaPlayerSelectSource) && options.length > 1
49096
+ } : null,
49097
+ sourceAssumed: isAssumed(status, "source"),
49098
+ shuffle: has(_camstack_types.DeviceFeature.MediaPlayerShuffle) && status.shuffle !== null,
49099
+ repeat: has(_camstack_types.DeviceFeature.MediaPlayerRepeat) && status.repeat !== null
49100
+ };
49101
+ }
49102
+ //#endregion
48769
49103
  //#region src/composites/device-controls/media-player-panel.tsx
48770
49104
  /**
48771
49105
  * Exhaustive colour map — one entry per MediaPlayerState.
48772
49106
  * Build fails if a state is added and not reflected here.
48773
49107
  */
48774
49108
  var STATE_COLOR = {
48775
- playing: "border-green-400 bg-green-50 dark:bg-green-950",
48776
- paused: "border-amber-400 bg-amber-50 dark:bg-amber-950",
48777
- buffering: "border-blue-400 bg-blue-50 dark:bg-blue-950",
48778
- on: "border-slate-400 bg-slate-50 dark:bg-slate-950",
48779
- idle: "border-slate-400 bg-slate-50 dark:bg-slate-950",
48780
- off: "border-zinc-300 bg-zinc-50 dark:bg-zinc-900",
48781
- standby: "border-zinc-300 bg-zinc-50 dark:bg-zinc-900"
49109
+ playing: "border-success/50 bg-success/10",
49110
+ paused: "border-warning/50 bg-warning/10",
49111
+ buffering: "border-info/50 bg-info/10",
49112
+ on: "border-foreground-subtle/40 bg-surface-hover",
49113
+ idle: "border-foreground-subtle/40 bg-surface-hover",
49114
+ off: "border-border bg-surface",
49115
+ standby: "border-border bg-surface"
48782
49116
  };
48783
49117
  /** Dot indicator colour (small circle) per state. */
48784
49118
  var STATE_DOT_COLOR = {
48785
- playing: "bg-green-500",
48786
- paused: "bg-amber-400",
48787
- buffering: "bg-blue-400",
48788
- on: "bg-slate-400",
48789
- idle: "bg-slate-400",
48790
- off: "bg-zinc-300",
48791
- standby: "bg-zinc-300"
49119
+ playing: "bg-success",
49120
+ paused: "bg-warning",
49121
+ buffering: "bg-info",
49122
+ on: "bg-foreground-subtle",
49123
+ idle: "bg-foreground-subtle",
49124
+ off: "bg-border",
49125
+ standby: "bg-border"
48792
49126
  };
48793
49127
  /** Exhaustive tuple of valid repeat values — single source of truth. */
48794
49128
  var REPEAT_VALUES = [
@@ -48812,7 +49146,7 @@ function toggleAction$1(state) {
48812
49146
  if (state === "playing" || state === "buffering") return "pause";
48813
49147
  return "play";
48814
49148
  }
48815
- function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
49149
+ function MediaPlayerPanel({ trpc, deviceId, features, layout, optimistic }) {
48816
49150
  const t = useUiText();
48817
49151
  const { cap, slice, available } = useDeviceCapability({
48818
49152
  trpc,
@@ -48822,6 +49156,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48822
49156
  });
48823
49157
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48824
49158
  if (display === void 0) return null;
49159
+ const declared = deriveMediaPlayerView(display, features, {});
48825
49160
  const busy = !available;
48826
49161
  const action = toggleAction$1(display.state);
48827
49162
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -48876,7 +49211,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48876
49211
  children: "⏭"
48877
49212
  })]
48878
49213
  }),
48879
- display.volumeLevel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
49214
+ (declared.volume === "slider" || display.volumeLevel !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48880
49215
  className: "flex items-center gap-2 text-xs text-foreground-subtle",
48881
49216
  children: [
48882
49217
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("uiLibrary.mediaPlayerPanel.vol", "Vol") }),
@@ -48886,7 +49221,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48886
49221
  min: 0,
48887
49222
  max: 100,
48888
49223
  step: 1,
48889
- value: display.volumeLevel,
49224
+ value: display.volumeLevel ?? 0,
48890
49225
  disabled: busy,
48891
49226
  onChange: (e) => {
48892
49227
  const volumeLevel = Number(e.target.value);
@@ -48894,25 +49229,25 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48894
49229
  if (!busy) cap?.setVolume({ volumeLevel });
48895
49230
  }
48896
49231
  }),
48897
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48898
49233
  className: "tabular-nums w-8 text-right",
48899
- children: [display.volumeLevel, "%"]
49234
+ children: display.volumeLevel === null ? "—" : `${display.volumeLevel}%`
48900
49235
  })
48901
49236
  ]
48902
49237
  }),
48903
- display.isMuted !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
49238
+ (declared.mute || display.isMuted !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
48904
49239
  type: "button",
48905
49240
  "aria-label": display.isMuted ? t("uiLibrary.mediaPlayerPanel.unmute", "Unmute") : t("uiLibrary.mediaPlayerPanel.mute", "Mute"),
48906
49241
  disabled: busy,
48907
49242
  onClick: (e) => {
48908
49243
  e.stopPropagation();
48909
- if (!busy && display.isMuted !== null) {
48910
- const muted = !display.isMuted;
49244
+ if (!busy) {
49245
+ const muted = display.isMuted === null ? true : !display.isMuted;
48911
49246
  applyOptimistic({ isMuted: muted });
48912
49247
  cap?.setMute({ muted });
48913
49248
  }
48914
49249
  },
48915
- className: cn("inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors", display.isMuted ? "bg-amber-400 text-amber-950" : "text-foreground-subtle hover:text-foreground hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
49250
+ className: cn("inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors", display.isMuted ? "bg-warning-fill text-warning-on" : "text-foreground-subtle hover:text-foreground hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
48916
49251
  children: display.isMuted ? "🔇" : "🔊"
48917
49252
  }),
48918
49253
  display.availableSources.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -48951,7 +49286,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48951
49286
  cap?.setShuffle({ shuffle });
48952
49287
  }
48953
49288
  },
48954
- className: cn("inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors", display.shuffle ? "bg-green-400 text-green-950" : "text-foreground-subtle hover:text-foreground hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
49289
+ className: cn("inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors", display.shuffle ? "bg-success-fill text-success-on" : "text-foreground-subtle hover:text-foreground hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
48955
49290
  children: ["⇌ ", t("uiLibrary.mediaPlayerPanel.shuffle", "Shuffle")]
48956
49291
  }),
48957
49292
  display.repeat !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -55595,7 +55930,7 @@ function SwingToggle({ axis, on, busy, onToggle }) {
55595
55930
  * "Run N min".
55596
55931
  * - HeaterHeroCard (device-detail hero): the glowing heat backdrop (the same
55597
55932
  * rising streaks a heating air conditioner draws) while it is on, the
55598
- * on/off switch, the power draw when the plug measures one, and the timed
55933
+ * on/off switch and the timed
55599
55934
  * run (number, unit, Run, countdown, Cancel).
55600
55935
  *
55601
55936
  * Every reading may be unknown (`null`) and is drawn as a dash, never as zero.
@@ -55617,12 +55952,6 @@ function useHeater({ trpc, deviceId, optimistic }) {
55617
55952
  cap: (d) => d.runningState,
55618
55953
  slice: (d) => d.state.runningState
55619
55954
  });
55620
- const power = useDeviceCapability({
55621
- trpc,
55622
- deviceId,
55623
- cap: (d) => d.powerMeter,
55624
- slice: (d) => d.state.powerMeter
55625
- });
55626
55955
  const timer = useDeviceCapability({
55627
55956
  trpc,
55628
55957
  deviceId,
@@ -55636,7 +55965,6 @@ function useHeater({ trpc, deviceId, optimistic }) {
55636
55965
  available,
55637
55966
  applyOptimistic,
55638
55967
  running: running.slice,
55639
- power: power.slice,
55640
55968
  timer
55641
55969
  };
55642
55970
  }
@@ -55679,11 +56007,10 @@ function HeaterInlineControl(props) {
55679
56007
  }
55680
56008
  function HeaterHeroCard(props) {
55681
56009
  const t = useUiText();
55682
- const { cap, display, available, applyOptimistic, running, power, timer } = useHeater(props);
56010
+ const { cap, display, available, applyOptimistic, running, timer } = useHeater(props);
55683
56011
  if (cap === void 0 || display === void 0) return null;
55684
56012
  const on = display.on === true;
55685
56013
  const heating = running?.running ?? on;
55686
- const watts = power?.power ?? null;
55687
56014
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55688
56015
  className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
55689
56016
  children: [
@@ -55701,21 +56028,13 @@ function HeaterHeroCard(props) {
55701
56028
  })
55702
56029
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55703
56030
  className: "relative z-10 flex flex-col items-center gap-1",
55704
- children: [
55705
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Flame, {
55706
- className: cn("h-8 w-8", heating ? "text-warning" : "text-foreground-subtle"),
55707
- "aria-hidden": true
55708
- }),
55709
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55710
- className: "text-sm font-medium text-foreground",
55711
- children: heating ? t("uiLibrary.heater.heating", "Heating") : t("uiLibrary.heater.off", "Off")
55712
- }),
55713
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55714
- className: "text-xs tabular-nums text-foreground-muted",
55715
- "data-testid": "heater-watts",
55716
- children: watts === null ? "—" : `${Math.round(watts)} W`
55717
- })
55718
- ]
56031
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Flame, {
56032
+ className: cn("h-8 w-8", heating ? "text-warning" : "text-foreground-subtle"),
56033
+ "aria-hidden": true
56034
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56035
+ className: "text-sm font-medium text-foreground",
56036
+ children: heating ? t("uiLibrary.heater.heating", "Heating") : t("uiLibrary.heater.off", "Off")
56037
+ })]
55719
56038
  })]
55720
56039
  }),
55721
56040
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -56160,17 +56479,16 @@ function DishwasherHeroCard(props) {
56160
56479
  * Appliance card for a `DeviceType.Washer` / `DeviceType.Dryer` device.
56161
56480
  *
56162
56481
  * These appliances have no program surface: what is worth showing is whether
56163
- * the cycle is running (the `running-state` cap), for how long, and what the
56164
- * appliance draws right now (the `power-meter` cap). Both slices are read
56165
- * straight from the device; neither is required.
56482
+ * the cycle is running (the `running-state` cap) and for how long. The draw
56483
+ * is NOT drawn here: the `power-meter` cap's hero widget shows it under every
56484
+ * hero card (D1004).
56166
56485
  * - ApplianceInlineControl (device-list row): the running badge round the
56167
- * drum icon, plus the live watts when there are any.
56486
+ * drum icon.
56168
56487
  * - ApplianceHeroCard (device-detail hero): the drum (turning only while a
56169
- * cycle runs), the state, the phase, the dwell and the power.
56488
+ * cycle runs), the state, the phase and the dwell.
56170
56489
  *
56171
56490
  * UNKNOWN IS NOT OFF (D393). `running: null` is drawn as "Unknown" and the
56172
- * drum stays still and unbadged; a missing watt reading is a dash, never
56173
- * "0 W". A slice that has not landed hides its row instead of faking a value.
56491
+ * drum stays still and unbadged.
56174
56492
  */
56175
56493
  var KEYFRAMES = `
56176
56494
  @keyframes apl-tumble { to { transform: rotate(360deg) } }
@@ -56179,10 +56497,6 @@ var KEYFRAMES = `
56179
56497
  .apl-drum-run { animation: none !important; }
56180
56498
  }
56181
56499
  `;
56182
- /** Whole watts, or a dash when the appliance has not reported a reading. */
56183
- function formatWatts(power) {
56184
- return power === void 0 || !Number.isFinite(power) ? "—" : `${Math.round(power)} W`;
56185
- }
56186
56500
  /** The state to a person: running / idle, or why there is none (unknown). */
56187
56501
  function applianceStateLabel(running, t) {
56188
56502
  if (running === true) return {
@@ -56205,10 +56519,7 @@ function dwellText(running, since, now, t) {
56205
56519
  return "minutes" in duration ? t("uiLibrary.appliance.forMinutes", "for {{count}} min", { count: duration.minutes }) : t("uiLibrary.appliance.forHours", "for {{count}} h", { count: duration.hours });
56206
56520
  }
56207
56521
  function useApplianceState(props) {
56208
- return {
56209
- running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState),
56210
- power: useDeviceState(props.trpc, props.deviceId, (s) => s.powerMeter)
56211
- };
56522
+ return { running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState) };
56212
56523
  }
56213
56524
  /** The drum: a ring with a few items in it, turning only while a cycle runs. */
56214
56525
  function Drum({ running }) {
@@ -56230,7 +56541,7 @@ function Drum({ running }) {
56230
56541
  }
56231
56542
  function ApplianceHeroCard(props) {
56232
56543
  const t = useUiText();
56233
- const { running, power } = useApplianceState(props);
56544
+ const { running } = useApplianceState(props);
56234
56545
  const state = running?.running ?? null;
56235
56546
  const meta = applianceStateLabel(state, t);
56236
56547
  const phase = running?.phase?.trim() ?? "";
@@ -56242,50 +56553,35 @@ function ApplianceHeroCard(props) {
56242
56553
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Drum, { running: state }),
56243
56554
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56244
56555
  className: "flex min-w-0 flex-1 flex-col gap-2",
56245
- children: [
56246
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56247
- className: "flex flex-wrap items-baseline gap-x-2",
56248
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56249
- "data-testid": "appliance-state",
56250
- className: cn("text-3xl font-semibold", meta.tone),
56251
- children: meta.label
56252
- }), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56253
- className: "text-sm tabular-nums text-foreground-subtle",
56254
- children: dwell
56255
- })]
56256
- }),
56257
- phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56258
- className: "text-sm text-foreground-subtle",
56259
- children: phase
56260
- }),
56261
- power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
56262
- "data-testid": "appliance-power",
56263
- className: "inline-flex items-center gap-1 text-sm tabular-nums text-foreground-muted",
56264
- title: power.power === void 0 ? t("uiLibrary.appliance.powerUnknown", "Power unknown") : void 0,
56265
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
56266
- className: "h-3.5 w-3.5",
56267
- "aria-hidden": true
56268
- }), formatWatts(power.power)]
56269
- })
56270
- ]
56556
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56557
+ className: "flex flex-wrap items-baseline gap-x-2",
56558
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56559
+ "data-testid": "appliance-state",
56560
+ className: cn("text-3xl font-semibold", meta.tone),
56561
+ children: meta.label
56562
+ }), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56563
+ className: "text-sm tabular-nums text-foreground-subtle",
56564
+ children: dwell
56565
+ })]
56566
+ }), phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56567
+ className: "text-sm text-foreground-subtle",
56568
+ children: phase
56569
+ })]
56271
56570
  })
56272
56571
  ]
56273
56572
  });
56274
56573
  }
56275
56574
  function ApplianceInlineControl(props) {
56276
- const { running, power } = useApplianceState(props);
56277
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56575
+ const { running } = useApplianceState(props);
56576
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
56278
56577
  className: "inline-flex items-center gap-1.5 max-w-full",
56279
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56578
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56280
56579
  running: running?.running ?? null,
56281
56580
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WashingMachine, {
56282
56581
  className: "h-4 w-4 text-foreground-muted",
56283
56582
  "aria-hidden": true
56284
56583
  })
56285
- }), power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56286
- className: "text-xs tabular-nums text-foreground-subtle",
56287
- children: formatWatts(power.power)
56288
- })]
56584
+ })
56289
56585
  });
56290
56586
  }
56291
56587
  //#endregion
@@ -57250,6 +57546,106 @@ function SwitchHeroCard({ trpc, deviceId, optimistic }) {
57250
57546
  });
57251
57547
  }
57252
57548
  //#endregion
57549
+ //#region src/composites/device-controls/media-player-hero-parts.tsx
57550
+ /**
57551
+ * The animated parts of the media-player hero: the power orb and the running
57552
+ * indicator. Motion is transform / opacity only, plays only while the state
57553
+ * says so, and stops under `prefers-reduced-motion` (the state is still told
57554
+ * by colour, icon and text, never by motion alone).
57555
+ */
57556
+ var MEDIA_HERO_KEYFRAMES = `
57557
+ @keyframes mph-eq { 0%, 100% { transform: scaleY(.3) } 50% { transform: scaleY(1) } }
57558
+ @keyframes mph-halo { 0% { transform: scale(.92); opacity: .55 } 100% { transform: scale(1.4); opacity: 0 } }
57559
+ .mph-eq { transform-origin: bottom; animation: mph-eq 1s ease-in-out infinite; }
57560
+ .mph-eq:nth-child(2) { animation-duration: .8s; animation-delay: -.3s; }
57561
+ .mph-eq:nth-child(3) { animation-duration: 1.2s; animation-delay: -.6s; }
57562
+ .mph-eq:nth-child(4) { animation-duration: .9s; animation-delay: -.1s; }
57563
+ .mph-halo { animation: mph-halo 2.8s ease-out infinite; }
57564
+ @keyframes mph-in { from { opacity: 0 } to { opacity: 1 } }
57565
+ @keyframes mph-out { from { opacity: 1 } to { opacity: 0 } }
57566
+ .mph-fade-in { animation: mph-in .6s ease-out both; }
57567
+ .mph-fade-out { animation: mph-out .6s ease-out both; }
57568
+ @media (prefers-reduced-motion: reduce) {
57569
+ .mph-eq, .mph-halo, .mph-fade-in, .mph-fade-out { animation: none !important; }
57570
+ .mph-fade-out { opacity: 0; }
57571
+ .mph-eq:nth-child(odd) { transform: scaleY(.6); }
57572
+ .mph-halo { opacity: 0; }
57573
+ }
57574
+ `;
57575
+ var ORB_TONE = {
57576
+ on: "border-success bg-success/15 text-success shadow-lg shadow-success/30",
57577
+ standby: "border-warning bg-warning/10 text-warning",
57578
+ off: "border-border bg-surface-hover text-foreground-subtle",
57579
+ unknown: "border-dashed border-border bg-transparent text-foreground-subtle"
57580
+ };
57581
+ /** The device's power: a glowing ring when on, amber in standby, flat when off, dashed when unknown. */
57582
+ function PowerOrb({ power, onPress, label, busy }) {
57583
+ const body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: power === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CircleQuestionMark, {
57584
+ className: "h-7 w-7",
57585
+ "aria-hidden": "true"
57586
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, {
57587
+ className: "h-7 w-7",
57588
+ "aria-hidden": "true"
57589
+ }) });
57590
+ const classes = cn("relative z-10 inline-flex h-16 w-16 items-center justify-center rounded-full border-2", "transition-all duration-500 motion-reduce:transition-none", ORB_TONE[power]);
57591
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57592
+ className: "relative flex h-20 w-20 items-center justify-center",
57593
+ "data-power": power,
57594
+ children: [power === "on" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57595
+ "aria-hidden": "true",
57596
+ "data-testid": "media-power-halo",
57597
+ className: "mph-halo absolute h-16 w-16 rounded-full bg-success/25"
57598
+ }), onPress === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57599
+ className: classes,
57600
+ "data-testid": "media-power",
57601
+ role: "img",
57602
+ "aria-label": label,
57603
+ children: body
57604
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57605
+ type: "button",
57606
+ "aria-label": label,
57607
+ title: label,
57608
+ disabled: busy,
57609
+ "data-testid": "media-power",
57610
+ onClick: (e) => {
57611
+ e.stopPropagation();
57612
+ if (!busy) onPress();
57613
+ },
57614
+ className: cn(classes, "hover:brightness-110 active:scale-95", busy && "cursor-not-allowed opacity-40"),
57615
+ children: body
57616
+ })]
57617
+ });
57618
+ }
57619
+ var EQ_BARS = [
57620
+ 0,
57621
+ 1,
57622
+ 2,
57623
+ 3
57624
+ ];
57625
+ /** Equalizer while playing, a spinner while buffering, a pause glyph when paused; nothing otherwise. */
57626
+ function RunningIndicator({ running }) {
57627
+ if (running === "playing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57628
+ className: "inline-flex h-4 items-end gap-0.5 text-primary",
57629
+ "data-testid": "media-running",
57630
+ "data-running": "playing",
57631
+ "aria-hidden": "true",
57632
+ children: EQ_BARS.map((bar) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "mph-eq h-4 w-1 rounded-full bg-primary" }, bar))
57633
+ });
57634
+ if (running === "buffering") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, {
57635
+ className: "h-4 w-4 animate-spin text-foreground-muted motion-reduce:animate-none",
57636
+ "data-testid": "media-running",
57637
+ "data-running": "buffering",
57638
+ "aria-hidden": "true"
57639
+ });
57640
+ if (running === "paused") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, {
57641
+ className: "h-4 w-4 text-foreground-muted",
57642
+ "data-testid": "media-running",
57643
+ "data-running": "paused",
57644
+ "aria-hidden": "true"
57645
+ });
57646
+ return null;
57647
+ }
57648
+ //#endregion
57253
57649
  //#region src/composites/device-controls/media-player-hero-card.tsx
57254
57650
  /** Which states map to play() vs pause() on the central transport button. */
57255
57651
  function toggleAction(state) {
@@ -57266,12 +57662,50 @@ function formatClock(ms) {
57266
57662
  const total = Math.floor(ms / 1e3);
57267
57663
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
57268
57664
  }
57269
- function NowPlaying({ title, subtitle }) {
57665
+ var NEXT_REPEAT = {
57666
+ off: "all",
57667
+ all: "one",
57668
+ one: "off"
57669
+ };
57670
+ /** The artwork of what is playing; a change cross-fades from the previous cover. */
57671
+ function Artwork({ url }) {
57672
+ const [shown, setShown] = (0, react$1.useState)(url);
57673
+ const [previous, setPrevious] = (0, react$1.useState)(null);
57674
+ const [failed, setFailed] = (0, react$1.useState)(null);
57675
+ const last = (0, react$1.useRef)(url);
57676
+ (0, react$1.useEffect)(() => {
57677
+ if (last.current === url) return;
57678
+ setPrevious(last.current);
57679
+ setShown(url);
57680
+ last.current = url;
57681
+ const timer = setTimeout(() => setPrevious(null), 700);
57682
+ return () => clearTimeout(timer);
57683
+ }, [url]);
57684
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57685
+ "data-testid": "media-artwork",
57686
+ className: "relative h-40 w-40 overflow-hidden rounded-lg border border-border bg-surface-hover",
57687
+ children: [previous !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
57688
+ src: previous,
57689
+ alt: "",
57690
+ className: "mph-fade-out absolute inset-0 h-full w-full object-cover"
57691
+ }), shown !== null && failed !== shown ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
57692
+ src: shown,
57693
+ alt: "",
57694
+ "data-testid": "media-artwork-img",
57695
+ className: "mph-fade-in absolute inset-0 h-full w-full object-cover",
57696
+ onError: () => setFailed(shown)
57697
+ }, shown) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Music, {
57698
+ className: "absolute inset-0 m-auto h-8 w-8 text-foreground-subtle",
57699
+ "aria-hidden": "true"
57700
+ })]
57701
+ });
57702
+ }
57703
+ function NowPlaying({ title, subtitle, artworkUrl }) {
57270
57704
  const t = useUiText();
57271
57705
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57272
- className: "flex flex-col items-center gap-1 text-center",
57706
+ className: "flex flex-col items-center gap-2 text-center",
57273
57707
  children: [
57274
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Music, {
57708
+ artworkUrl !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Artwork, { url: artworkUrl }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Music, {
57275
57709
  className: "h-6 w-6 text-foreground-subtle",
57276
57710
  "aria-hidden": "true"
57277
57711
  }),
@@ -57286,33 +57720,77 @@ function NowPlaying({ title, subtitle }) {
57286
57720
  ]
57287
57721
  });
57288
57722
  }
57289
- function TransportButton({ label, busy, onPress, children, primary }) {
57723
+ var STATE_LABEL$1 = {
57724
+ off: "Off",
57725
+ on: "On",
57726
+ idle: "Idle",
57727
+ playing: "Playing",
57728
+ paused: "Paused",
57729
+ buffering: "Buffering",
57730
+ standby: "Standby"
57731
+ };
57732
+ function StateLine({ state, assumed, source, running }) {
57733
+ const t = useUiText();
57734
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57735
+ className: "flex flex-wrap items-center justify-center gap-2 text-xs text-foreground-muted",
57736
+ "data-testid": "media-state",
57737
+ children: [
57738
+ running,
57739
+ state === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57740
+ className: "font-medium text-foreground-subtle",
57741
+ "data-testid": "media-state-unknown",
57742
+ children: t("uiLibrary.mediaPlayerHero.state.unknown", "State unknown")
57743
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57744
+ className: "font-medium text-foreground",
57745
+ children: t(`uiLibrary.mediaPlayerHero.state.${state}`, STATE_LABEL$1[state])
57746
+ }),
57747
+ assumed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57748
+ className: "rounded border border-dashed border-border px-1.5 py-0.5 text-[11px] text-foreground-subtle",
57749
+ title: t("uiLibrary.mediaPlayerHero.assumedHint", "No feedback from the device: this is what was last sent."),
57750
+ "data-testid": "media-assumed",
57751
+ children: t("uiLibrary.mediaPlayerHero.assumed", "assumed")
57752
+ }),
57753
+ source !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57754
+ "data-testid": "media-source",
57755
+ children: source
57756
+ })
57757
+ ]
57758
+ });
57759
+ }
57760
+ function TransportButton({ label, busy, onPress, children, primary, active, testId }) {
57290
57761
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57291
57762
  type: "button",
57292
57763
  "aria-label": label,
57764
+ "aria-pressed": active,
57293
57765
  title: label,
57294
57766
  disabled: busy,
57767
+ "data-testid": testId,
57295
57768
  onClick: (e) => {
57296
57769
  e.stopPropagation();
57297
57770
  if (!busy) onPress();
57298
57771
  },
57299
- className: cn("inline-flex items-center justify-center rounded-full transition-colors", primary ? "h-12 w-12 bg-primary text-primary-foreground hover:bg-primary/90 shadow-md" : "h-10 w-10 text-foreground-muted hover:bg-surface-hover", busy && "opacity-40 cursor-not-allowed"),
57772
+ className: cn("inline-flex items-center justify-center rounded-full transition-colors", primary ? "h-12 w-12 bg-primary text-primary-foreground hover:bg-primary/90 shadow-md" : "h-10 w-10 text-foreground-muted hover:bg-surface-hover", active === true && "bg-primary/15 text-primary", busy && "opacity-40 cursor-not-allowed"),
57300
57773
  children
57301
57774
  });
57302
57775
  }
57303
57776
  /**
57304
- * MediaPlayerHeroCard — now-playing hero for MediaPlayer devices.
57777
+ * MediaPlayerHeroCard — the state of a MediaPlayer device and every control it
57778
+ * exposes, drawn from `deriveMediaPlayerView` (the state → view mapping and
57779
+ * its spec live in `media-player-view.ts`).
57305
57780
  *
57306
- * Renders (Spec §3):
57307
- * - Now-playing block: cover glyph + title + artist/album.
57308
- * - Seek GripTrack with a native range overlay (gated on DeviceFeature
57309
- * MediaPlayerSeek + a known duration).
57310
- * - Transport row: SkipBack / Play-Pause / SkipForward (lucide icons). Skip
57311
- * buttons are gated on MediaPlayerPrevious / MediaPlayerNext.
57312
- * - Volume GripTrack with a native range overlay + mute toggle (gated on a
57313
- * non-null volume surface + DeviceFeature.MediaPlayerVolume).
57781
+ * - Power orb: glowing when on, amber in standby, flat when off, dashed with a
57782
+ * question mark while the state is unknown (never drawn as off). It is the
57783
+ * power switch when the device declares `MediaPlayerPower`.
57784
+ * - Running line: equalizer while playing, spinner while buffering, pause
57785
+ * glyph when paused; a value the driver only guesses carries an "assumed" mark.
57786
+ * - Only while the device is on: now-playing, seek, transport (previous /
57787
+ * play-pause / stop / next), volume (slider with mute, or steps when the
57788
+ * device has no level reading), the source (a picker when it can switch),
57789
+ * shuffle and repeat. Each appears only when the status fills it or a
57790
+ * `DeviceFeature` declares it — no per-vendor list.
57314
57791
  *
57315
57792
  * Cap-gated on `media-player` — returns null when the device lacks the cap.
57793
+ * Motion respects `prefers-reduced-motion`.
57316
57794
  */
57317
57795
  function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57318
57796
  const t = useUiText();
@@ -57323,14 +57801,24 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57323
57801
  slice: (d) => d.state.mediaPlayer
57324
57802
  });
57325
57803
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
57804
+ const runningSlice = useDeviceState(trpc, deviceId, (st) => st.runningState);
57326
57805
  if (cap === void 0) return null;
57327
- if (display === void 0) return null;
57806
+ const view = deriveMediaPlayerView(display, features, { running: runningSlice?.running });
57328
57807
  const busy = !available;
57329
- const action = toggleAction(display.state);
57330
- const supportsSeek = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerSeek) ?? false) && display.duration !== null && display.duration > 0;
57331
- const supportsNext = features?.includes(_camstack_types.DeviceFeature.MediaPlayerNext) ?? false;
57332
- const supportsPrevious = features?.includes(_camstack_types.DeviceFeature.MediaPlayerPrevious) ?? false;
57333
- const supportsVolume = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerVolume) ?? false) && display.volumeLevel !== null;
57808
+ const status = display;
57809
+ const powered = status !== void 0 && view.power === "on";
57810
+ const action = toggleAction(status?.state ?? "off");
57811
+ const switchPower = () => {
57812
+ if (busy) return;
57813
+ if (view.power === "on") {
57814
+ applyOptimistic({ state: "off" });
57815
+ cap.turnOff({});
57816
+ } else {
57817
+ applyOptimistic({ state: "on" });
57818
+ cap.turnOn({});
57819
+ }
57820
+ };
57821
+ const powerLabel = powerLabelOf(view.power, t);
57334
57822
  const handleToggle = () => {
57335
57823
  if (busy) return;
57336
57824
  if (action === "play") {
@@ -57341,17 +57829,60 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57341
57829
  cap.pause({});
57342
57830
  }
57343
57831
  };
57344
- const subtitle = display.currentMedia?.artist ?? display.currentMedia?.album ?? null;
57345
- const seekFraction = progressFraction(display.position, display.duration);
57346
- const volumeLevel = display.volumeLevel ?? 0;
57832
+ const title = status?.currentMedia?.title ?? null;
57833
+ const subtitle = status?.currentMedia?.artist ?? status?.currentMedia?.album ?? null;
57834
+ const hasNowPlaying = powered && (title !== null || subtitle !== null || view.artworkUrl !== null);
57835
+ const assumedAny = view.stateAssumed || view.volumeAssumed || view.muteAssumed || view.sourceAssumed;
57836
+ const sourceText = powered && view.source !== null && !view.source.selectable ? view.source.current : "";
57837
+ const volumeLevel = status?.volumeLevel ?? null;
57347
57838
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57348
- className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-6",
57839
+ className: cn("flex flex-col items-stretch gap-4 rounded-lg border bg-surface p-6", "transition-colors duration-500 motion-reduce:transition-none", view.power === "on" ? "border-success/40" : "border-border"),
57840
+ "data-testid": "media-hero",
57841
+ "data-power": view.power,
57349
57842
  children: [
57350
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NowPlaying, {
57351
- title: display.currentMedia?.title ?? null,
57352
- subtitle
57843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: MEDIA_HERO_KEYFRAMES }),
57844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57845
+ className: "flex flex-col items-center gap-2",
57846
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PowerOrb, {
57847
+ power: view.power,
57848
+ label: powerLabel,
57849
+ busy,
57850
+ onPress: view.canSwitchPower ? switchPower : void 0
57851
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateLine, {
57852
+ state: status?.state,
57853
+ assumed: assumedAny,
57854
+ source: sourceText,
57855
+ running: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningIndicator, { running: view.running })
57856
+ })]
57857
+ }),
57858
+ hasNowPlaying && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NowPlaying, {
57859
+ title,
57860
+ subtitle,
57861
+ artworkUrl: view.artworkUrl
57862
+ }),
57863
+ powered && view.source !== null && view.source.selectable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57864
+ "data-testid": "media-source-select",
57865
+ className: "flex items-center gap-2",
57866
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57867
+ className: "text-xs text-foreground-subtle",
57868
+ children: t("uiLibrary.mediaPlayerHero.source", "Source")
57869
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
57870
+ "aria-label": t("uiLibrary.mediaPlayerHero.source", "Source"),
57871
+ value: view.source.current,
57872
+ disabled: busy,
57873
+ wrapperClassName: "relative min-w-0 flex-1",
57874
+ options: view.source.options.map((o) => ({
57875
+ value: o,
57876
+ label: o
57877
+ })),
57878
+ onChange: (e) => {
57879
+ if (busy) return;
57880
+ applyOptimistic({ source: e.target.value });
57881
+ cap.selectSource({ source: e.target.value });
57882
+ }
57883
+ })]
57353
57884
  }),
57354
- supportsSeek && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57885
+ powered && view.seek && status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57355
57886
  "data-testid": "media-seek",
57356
57887
  className: "flex flex-col gap-1",
57357
57888
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -57365,10 +57896,10 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57365
57896
  "aria-label": t("uiLibrary.mediaPlayerHero.seek", "Seek"),
57366
57897
  type: "range",
57367
57898
  min: 0,
57368
- max: display.duration ?? 0,
57899
+ max: status.duration ?? 0,
57369
57900
  step: 1e3,
57370
57901
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
57371
- value: display.position ?? 0,
57902
+ value: status.position ?? 0,
57372
57903
  disabled: busy,
57373
57904
  onChange: (e) => {
57374
57905
  if (busy) return;
@@ -57379,18 +57910,19 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57379
57910
  }),
57380
57911
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57381
57912
  className: "pointer-events-none",
57382
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: seekFraction })
57913
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: progressFraction(status.position, status.duration) })
57383
57914
  })
57384
57915
  ]
57385
57916
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57386
57917
  className: "flex justify-between text-[11px] tabular-nums text-foreground-subtle",
57387
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(display.position) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(display.duration) })]
57918
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(status.position) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(status.duration) })]
57388
57919
  })]
57389
57920
  }),
57390
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57921
+ powered && view.transport.playPause && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57391
57922
  className: "flex items-center justify-center gap-4",
57923
+ "data-testid": "media-transport",
57392
57924
  children: [
57393
- supportsPrevious && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57925
+ view.transport.previous && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57394
57926
  label: t("uiLibrary.mediaPlayerHero.previous", "Previous"),
57395
57927
  busy,
57396
57928
  onPress: () => void cap.previous({}),
@@ -57412,7 +57944,16 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57412
57944
  "aria-hidden": "true"
57413
57945
  })
57414
57946
  }),
57415
- supportsNext && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57947
+ view.transport.stop && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57948
+ label: t("uiLibrary.mediaPlayerHero.stop", "Stop"),
57949
+ busy,
57950
+ onPress: () => void cap.stop({}),
57951
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, {
57952
+ className: "h-4 w-4",
57953
+ "aria-hidden": "true"
57954
+ })
57955
+ }),
57956
+ view.transport.next && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57416
57957
  label: t("uiLibrary.mediaPlayerHero.next", "Next"),
57417
57958
  busy,
57418
57959
  onPress: () => void cap.next({}),
@@ -57423,24 +57964,24 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57423
57964
  })
57424
57965
  ]
57425
57966
  }),
57426
- supportsVolume && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57967
+ powered && view.volume === "slider" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57427
57968
  "data-testid": "media-volume",
57428
57969
  className: "flex items-center gap-2",
57429
57970
  children: [
57430
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57971
+ view.mute && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57431
57972
  type: "button",
57432
- "aria-label": display.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57433
- title: display.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57973
+ "aria-label": status?.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57974
+ title: status?.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57434
57975
  disabled: busy,
57435
57976
  onClick: (e) => {
57436
57977
  e.stopPropagation();
57437
- if (busy || display.isMuted === null) return;
57438
- const muted = !display.isMuted;
57978
+ if (busy) return;
57979
+ const muted = status?.isMuted === null || status === void 0 ? true : !status.isMuted;
57439
57980
  applyOptimistic({ isMuted: muted });
57440
57981
  cap.setMute({ muted });
57441
57982
  },
57442
57983
  className: cn("inline-flex h-7 w-7 items-center justify-center rounded text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors", busy && "opacity-40 cursor-not-allowed"),
57443
- children: display.isMuted === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VolumeX, {
57984
+ children: status?.isMuted === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VolumeX, {
57444
57985
  className: "h-4 w-4",
57445
57986
  "aria-hidden": "true"
57446
57987
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Volume2, {
@@ -57462,7 +58003,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57462
58003
  max: 100,
57463
58004
  step: 1,
57464
58005
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
57465
- value: volumeLevel,
58006
+ value: volumeLevel ?? 0,
57466
58007
  disabled: busy,
57467
58008
  onChange: (e) => {
57468
58009
  if (busy) return;
@@ -57473,19 +58014,92 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57473
58014
  }),
57474
58015
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57475
58016
  className: "pointer-events-none",
57476
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: volumeLevel / 100 })
58017
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58018
+ className: cn(volumeLevel === null && "opacity-40"),
58019
+ "data-testid": "media-volume-track",
58020
+ "data-known": volumeLevel !== null,
58021
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: status?.isMuted === true || volumeLevel === null ? 0 : volumeLevel / 100 })
58022
+ })
57477
58023
  })
57478
58024
  ]
57479
58025
  }),
57480
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
58026
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57481
58027
  className: "w-9 text-right text-[11px] tabular-nums text-foreground-subtle",
57482
- children: [volumeLevel, "%"]
58028
+ children: volumeLevel === null ? "—" : `${volumeLevel}%`
57483
58029
  })
57484
58030
  ]
58031
+ }),
58032
+ powered && view.volume === "steps" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58033
+ "data-testid": "media-volume-steps",
58034
+ className: "flex items-center justify-center gap-3 text-xs text-foreground-subtle",
58035
+ children: [
58036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58037
+ label: t("uiLibrary.mediaPlayerHero.volumeDown", "Volume down"),
58038
+ busy,
58039
+ onPress: () => void cap.volumeDown({ steps: 1 }),
58040
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minus, {
58041
+ className: "h-4 w-4",
58042
+ "aria-hidden": "true"
58043
+ })
58044
+ }),
58045
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Volume2, {
58046
+ className: "h-4 w-4",
58047
+ "aria-hidden": "true"
58048
+ }),
58049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58050
+ label: t("uiLibrary.mediaPlayerHero.volumeUp", "Volume up"),
58051
+ busy,
58052
+ onPress: () => void cap.volumeUp({ steps: 1 }),
58053
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, {
58054
+ className: "h-4 w-4",
58055
+ "aria-hidden": "true"
58056
+ })
58057
+ })
58058
+ ]
58059
+ }),
58060
+ powered && status !== void 0 && (view.shuffle || view.repeat) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58061
+ className: "flex items-center justify-center gap-2",
58062
+ "data-testid": "media-modes",
58063
+ children: [view.shuffle && status.shuffle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58064
+ label: t("uiLibrary.mediaPlayerHero.shuffle", "Shuffle"),
58065
+ busy,
58066
+ active: status.shuffle,
58067
+ onPress: () => {
58068
+ const shuffle = !status.shuffle;
58069
+ applyOptimistic({ shuffle });
58070
+ cap.setShuffle({ shuffle });
58071
+ },
58072
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Shuffle, {
58073
+ className: "h-4 w-4",
58074
+ "aria-hidden": "true"
58075
+ })
58076
+ }), view.repeat && status.repeat !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58077
+ label: t("uiLibrary.mediaPlayerHero.repeat", "Repeat"),
58078
+ busy,
58079
+ active: status.repeat !== "off",
58080
+ onPress: () => {
58081
+ if (status.repeat === null) return;
58082
+ const repeat = NEXT_REPEAT[status.repeat];
58083
+ applyOptimistic({ repeat });
58084
+ cap.setRepeat({ repeat });
58085
+ },
58086
+ children: status.repeat === "one" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Repeat1, {
58087
+ className: "h-4 w-4",
58088
+ "aria-hidden": "true"
58089
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Repeat, {
58090
+ className: "h-4 w-4",
58091
+ "aria-hidden": "true"
58092
+ })
58093
+ })]
57485
58094
  })
57486
58095
  ]
57487
58096
  });
57488
58097
  }
58098
+ function powerLabelOf(power, t) {
58099
+ if (power === "on") return t("uiLibrary.mediaPlayerHero.turnOff", "Turn off");
58100
+ if (power === "unknown") return t("uiLibrary.mediaPlayerHero.powerUnknown", "Power state unknown");
58101
+ return t("uiLibrary.mediaPlayerHero.turnOn", "Turn on");
58102
+ }
57489
58103
  //#endregion
57490
58104
  //#region src/composites/device-controls/alarm-hero-card.tsx
57491
58105
  var ALARM_STATE_CONFIG = {
@@ -73059,7 +73673,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
73059
73673
  }
73060
73674
  //#endregion
73061
73675
  //#region src/ui-library-version.ts
73062
- var uiLibraryVersion = "1.2.375";
73676
+ var uiLibraryVersion = "1.2.377";
73063
73677
  //#endregion
73064
73678
  //#region src/hooks/use-account-unit-preferences.ts
73065
73679
  /**
@@ -74424,6 +75038,7 @@ exports.PopoverContent = PopoverContent;
74424
75038
  exports.PopoverRowAction = PopoverRowAction;
74425
75039
  exports.PopoverTrigger = PopoverTrigger;
74426
75040
  exports.Power = Power;
75041
+ exports.PowerMeterLine = PowerMeterLine;
74427
75042
  exports.PowerOff = PowerOff;
74428
75043
  exports.PrimaryChildPicker = PrimaryChildPicker;
74429
75044
  exports.PrivacyMaskSettings = PrivacyMaskSettings;
@@ -74450,6 +75065,8 @@ exports.RecordEntriesPanel = RecordEntriesPanel;
74450
75065
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
74451
75066
  exports.RecordingPanel = RecordingPanel;
74452
75067
  exports.RefreshCw = RefreshCw;
75068
+ exports.Repeat = Repeat;
75069
+ exports.Repeat1 = Repeat1;
74453
75070
  exports.ResponseLog = ResponseLog;
74454
75071
  exports.RetrainReviewBadge = RetrainReviewBadge;
74455
75072
  exports.RoleDisplayDefaultsFromServer = RoleDisplayDefaultsFromServer;
@@ -74533,6 +75150,7 @@ exports.ShieldAlert = ShieldAlert;
74533
75150
  exports.ShieldCheck = ShieldCheck;
74534
75151
  exports.ShieldOff = ShieldOff;
74535
75152
  exports.Ship = Ship;
75153
+ exports.Shuffle = Shuffle;
74536
75154
  exports.Sidebar = Sidebar;
74537
75155
  exports.SidebarItem = SidebarItem;
74538
75156
  exports.Siren = Siren;
@@ -75627,6 +76245,8 @@ exports.useLockControlLock = useLockControlLock;
75627
76245
  exports.useLockControlOpen = useLockControlOpen;
75628
76246
  exports.useLockControlUnlock = useLockControlUnlock;
75629
76247
  exports.useLockDoorSensor = useLockDoorSensor;
76248
+ exports.useMediaPlayerChannelDown = useMediaPlayerChannelDown;
76249
+ exports.useMediaPlayerChannelUp = useMediaPlayerChannelUp;
75630
76250
  exports.useMediaPlayerGetStatus = useMediaPlayerGetStatus;
75631
76251
  exports.useMediaPlayerNext = useMediaPlayerNext;
75632
76252
  exports.useMediaPlayerPause = useMediaPlayerPause;
@@ -75640,6 +76260,10 @@ exports.useMediaPlayerSetRepeat = useMediaPlayerSetRepeat;
75640
76260
  exports.useMediaPlayerSetShuffle = useMediaPlayerSetShuffle;
75641
76261
  exports.useMediaPlayerSetVolume = useMediaPlayerSetVolume;
75642
76262
  exports.useMediaPlayerStop = useMediaPlayerStop;
76263
+ exports.useMediaPlayerTurnOff = useMediaPlayerTurnOff;
76264
+ exports.useMediaPlayerTurnOn = useMediaPlayerTurnOn;
76265
+ exports.useMediaPlayerVolumeDown = useMediaPlayerVolumeDown;
76266
+ exports.useMediaPlayerVolumeUp = useMediaPlayerVolumeUp;
75643
76267
  exports.useMeshNetworkGetStatus = useMeshNetworkGetStatus;
75644
76268
  exports.useMeshNetworkJoin = useMeshNetworkJoin;
75645
76269
  exports.useMeshNetworkLeave = useMeshNetworkLeave;
@@ -76126,6 +76750,7 @@ exports.useRecordingOnboardSetSettings = useRecordingOnboardSetSettings;
76126
76750
  exports.useRecordingSignalGetStatus = useRecordingSignalGetStatus;
76127
76751
  exports.useRegisteredHotkeys = useRegisteredHotkeys;
76128
76752
  exports.useRemoteComponent = useRemoteComponent;
76753
+ exports.useRemoteGetAssumedStatus = useRemoteGetAssumedStatus;
76129
76754
  exports.useRemoteGetConfig = useRemoteGetConfig;
76130
76755
  exports.useRemoteLayoutComponentsListComponents = useRemoteLayoutComponentsListComponents;
76131
76756
  exports.useRemoteListCommands = useRemoteListCommands;