@camstack/ui-library 1.2.376 → 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) => {
@@ -27761,6 +27841,92 @@ function NavigationPanel({ deviceId }) {
27761
27841
  });
27762
27842
  }
27763
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
27764
27930
  //#region src/lib/cap-error.ts
27765
27931
  /**
27766
27932
  * Shared capability-error utilities for the admin UI.
@@ -30739,6 +30905,7 @@ var HOST_WIDGETS = {
30739
30905
  "host/ptz-panel": PtzPanel,
30740
30906
  "host/navigation-panel": NavigationPanel,
30741
30907
  "host/consumables-panel": ConsumablesPanel,
30908
+ "host/power-meter-line": PowerMeterLine,
30742
30909
  "host/ptz-autotrack": AutotrackSection,
30743
30910
  "host/recording-panel": RecordingPanel,
30744
30911
  "host/camera-grid-layout": CameraGridLayoutEditor,
@@ -37075,8 +37242,8 @@ function pickWifiIcon(signalPercent) {
37075
37242
  }
37076
37243
  function signalColor(signalPercent) {
37077
37244
  if (signalPercent === null) return "text-foreground-subtle";
37078
- if (signalPercent <= 20) return "text-rose-400";
37079
- if (signalPercent <= 50) return "text-amber-400";
37245
+ if (signalPercent <= 20) return "text-danger";
37246
+ if (signalPercent <= 50) return "text-warning";
37080
37247
  return "text-foreground-subtle";
37081
37248
  }
37082
37249
  function NetworkLinkBadge({ status, className }) {
@@ -37087,15 +37254,16 @@ function NetworkLinkBadge({ status, className }) {
37087
37254
  title: t("uiLibrary.networkLinkBadge.ethernet", "Network: ethernet"),
37088
37255
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cable, { className: "h-3.5 w-3.5" })
37089
37256
  });
37090
- const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signal);
37091
- 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);
37092
37260
  const networkText = status.ssid ? t("uiLibrary.networkLinkBadge.typeSsid", "Network: {{type}} ({{ssid}})", {
37093
37261
  type: status.type,
37094
37262
  ssid: status.ssid
37095
37263
  }) : t("uiLibrary.networkLinkBadge.type", "Network: {{type}}", { type: status.type });
37096
37264
  const title = signal === null ? networkText : `${networkText} · ${t("uiLibrary.networkLinkBadge.signal", "signal {{percent}}%", { percent: signal })}`;
37097
37265
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
37098
- 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),
37099
37267
  title,
37100
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, "%"] })]
37101
37269
  });
@@ -48782,29 +48950,179 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
48782
48950
  });
48783
48951
  }
48784
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
48785
49103
  //#region src/composites/device-controls/media-player-panel.tsx
48786
49104
  /**
48787
49105
  * Exhaustive colour map — one entry per MediaPlayerState.
48788
49106
  * Build fails if a state is added and not reflected here.
48789
49107
  */
48790
49108
  var STATE_COLOR = {
48791
- playing: "border-green-400 bg-green-50 dark:bg-green-950",
48792
- paused: "border-amber-400 bg-amber-50 dark:bg-amber-950",
48793
- buffering: "border-blue-400 bg-blue-50 dark:bg-blue-950",
48794
- on: "border-slate-400 bg-slate-50 dark:bg-slate-950",
48795
- idle: "border-slate-400 bg-slate-50 dark:bg-slate-950",
48796
- off: "border-zinc-300 bg-zinc-50 dark:bg-zinc-900",
48797
- 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"
48798
49116
  };
48799
49117
  /** Dot indicator colour (small circle) per state. */
48800
49118
  var STATE_DOT_COLOR = {
48801
- playing: "bg-green-500",
48802
- paused: "bg-amber-400",
48803
- buffering: "bg-blue-400",
48804
- on: "bg-slate-400",
48805
- idle: "bg-slate-400",
48806
- off: "bg-zinc-300",
48807
- 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"
48808
49126
  };
48809
49127
  /** Exhaustive tuple of valid repeat values — single source of truth. */
48810
49128
  var REPEAT_VALUES = [
@@ -48828,7 +49146,7 @@ function toggleAction$1(state) {
48828
49146
  if (state === "playing" || state === "buffering") return "pause";
48829
49147
  return "play";
48830
49148
  }
48831
- function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
49149
+ function MediaPlayerPanel({ trpc, deviceId, features, layout, optimistic }) {
48832
49150
  const t = useUiText();
48833
49151
  const { cap, slice, available } = useDeviceCapability({
48834
49152
  trpc,
@@ -48838,6 +49156,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48838
49156
  });
48839
49157
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48840
49158
  if (display === void 0) return null;
49159
+ const declared = deriveMediaPlayerView(display, features, {});
48841
49160
  const busy = !available;
48842
49161
  const action = toggleAction$1(display.state);
48843
49162
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -48892,7 +49211,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48892
49211
  children: "⏭"
48893
49212
  })]
48894
49213
  }),
48895
- display.volumeLevel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
49214
+ (declared.volume === "slider" || display.volumeLevel !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48896
49215
  className: "flex items-center gap-2 text-xs text-foreground-subtle",
48897
49216
  children: [
48898
49217
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("uiLibrary.mediaPlayerPanel.vol", "Vol") }),
@@ -48902,7 +49221,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48902
49221
  min: 0,
48903
49222
  max: 100,
48904
49223
  step: 1,
48905
- value: display.volumeLevel,
49224
+ value: display.volumeLevel ?? 0,
48906
49225
  disabled: busy,
48907
49226
  onChange: (e) => {
48908
49227
  const volumeLevel = Number(e.target.value);
@@ -48910,25 +49229,25 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48910
49229
  if (!busy) cap?.setVolume({ volumeLevel });
48911
49230
  }
48912
49231
  }),
48913
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48914
49233
  className: "tabular-nums w-8 text-right",
48915
- children: [display.volumeLevel, "%"]
49234
+ children: display.volumeLevel === null ? "—" : `${display.volumeLevel}%`
48916
49235
  })
48917
49236
  ]
48918
49237
  }),
48919
- display.isMuted !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
49238
+ (declared.mute || display.isMuted !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
48920
49239
  type: "button",
48921
49240
  "aria-label": display.isMuted ? t("uiLibrary.mediaPlayerPanel.unmute", "Unmute") : t("uiLibrary.mediaPlayerPanel.mute", "Mute"),
48922
49241
  disabled: busy,
48923
49242
  onClick: (e) => {
48924
49243
  e.stopPropagation();
48925
- if (!busy && display.isMuted !== null) {
48926
- const muted = !display.isMuted;
49244
+ if (!busy) {
49245
+ const muted = display.isMuted === null ? true : !display.isMuted;
48927
49246
  applyOptimistic({ isMuted: muted });
48928
49247
  cap?.setMute({ muted });
48929
49248
  }
48930
49249
  },
48931
- 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"),
48932
49251
  children: display.isMuted ? "🔇" : "🔊"
48933
49252
  }),
48934
49253
  display.availableSources.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -48967,7 +49286,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48967
49286
  cap?.setShuffle({ shuffle });
48968
49287
  }
48969
49288
  },
48970
- 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"),
48971
49290
  children: ["⇌ ", t("uiLibrary.mediaPlayerPanel.shuffle", "Shuffle")]
48972
49291
  }),
48973
49292
  display.repeat !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -55611,7 +55930,7 @@ function SwingToggle({ axis, on, busy, onToggle }) {
55611
55930
  * "Run N min".
55612
55931
  * - HeaterHeroCard (device-detail hero): the glowing heat backdrop (the same
55613
55932
  * rising streaks a heating air conditioner draws) while it is on, the
55614
- * on/off switch, the power draw when the plug measures one, and the timed
55933
+ * on/off switch and the timed
55615
55934
  * run (number, unit, Run, countdown, Cancel).
55616
55935
  *
55617
55936
  * Every reading may be unknown (`null`) and is drawn as a dash, never as zero.
@@ -55633,12 +55952,6 @@ function useHeater({ trpc, deviceId, optimistic }) {
55633
55952
  cap: (d) => d.runningState,
55634
55953
  slice: (d) => d.state.runningState
55635
55954
  });
55636
- const power = useDeviceCapability({
55637
- trpc,
55638
- deviceId,
55639
- cap: (d) => d.powerMeter,
55640
- slice: (d) => d.state.powerMeter
55641
- });
55642
55955
  const timer = useDeviceCapability({
55643
55956
  trpc,
55644
55957
  deviceId,
@@ -55652,7 +55965,6 @@ function useHeater({ trpc, deviceId, optimistic }) {
55652
55965
  available,
55653
55966
  applyOptimistic,
55654
55967
  running: running.slice,
55655
- power: power.slice,
55656
55968
  timer
55657
55969
  };
55658
55970
  }
@@ -55695,11 +56007,10 @@ function HeaterInlineControl(props) {
55695
56007
  }
55696
56008
  function HeaterHeroCard(props) {
55697
56009
  const t = useUiText();
55698
- const { cap, display, available, applyOptimistic, running, power, timer } = useHeater(props);
56010
+ const { cap, display, available, applyOptimistic, running, timer } = useHeater(props);
55699
56011
  if (cap === void 0 || display === void 0) return null;
55700
56012
  const on = display.on === true;
55701
56013
  const heating = running?.running ?? on;
55702
- const watts = power?.power ?? null;
55703
56014
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55704
56015
  className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
55705
56016
  children: [
@@ -55717,21 +56028,13 @@ function HeaterHeroCard(props) {
55717
56028
  })
55718
56029
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55719
56030
  className: "relative z-10 flex flex-col items-center gap-1",
55720
- children: [
55721
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Flame, {
55722
- className: cn("h-8 w-8", heating ? "text-warning" : "text-foreground-subtle"),
55723
- "aria-hidden": true
55724
- }),
55725
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55726
- className: "text-sm font-medium text-foreground",
55727
- children: heating ? t("uiLibrary.heater.heating", "Heating") : t("uiLibrary.heater.off", "Off")
55728
- }),
55729
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55730
- className: "text-xs tabular-nums text-foreground-muted",
55731
- "data-testid": "heater-watts",
55732
- children: watts === null ? "—" : `${Math.round(watts)} W`
55733
- })
55734
- ]
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
+ })]
55735
56038
  })]
55736
56039
  }),
55737
56040
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -56176,17 +56479,16 @@ function DishwasherHeroCard(props) {
56176
56479
  * Appliance card for a `DeviceType.Washer` / `DeviceType.Dryer` device.
56177
56480
  *
56178
56481
  * These appliances have no program surface: what is worth showing is whether
56179
- * the cycle is running (the `running-state` cap), for how long, and what the
56180
- * appliance draws right now (the `power-meter` cap). Both slices are read
56181
- * 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).
56182
56485
  * - ApplianceInlineControl (device-list row): the running badge round the
56183
- * drum icon, plus the live watts when there are any.
56486
+ * drum icon.
56184
56487
  * - ApplianceHeroCard (device-detail hero): the drum (turning only while a
56185
- * cycle runs), the state, the phase, the dwell and the power.
56488
+ * cycle runs), the state, the phase and the dwell.
56186
56489
  *
56187
56490
  * UNKNOWN IS NOT OFF (D393). `running: null` is drawn as "Unknown" and the
56188
- * drum stays still and unbadged; a missing watt reading is a dash, never
56189
- * "0 W". A slice that has not landed hides its row instead of faking a value.
56491
+ * drum stays still and unbadged.
56190
56492
  */
56191
56493
  var KEYFRAMES = `
56192
56494
  @keyframes apl-tumble { to { transform: rotate(360deg) } }
@@ -56195,10 +56497,6 @@ var KEYFRAMES = `
56195
56497
  .apl-drum-run { animation: none !important; }
56196
56498
  }
56197
56499
  `;
56198
- /** Whole watts, or a dash when the appliance has not reported a reading. */
56199
- function formatWatts(power) {
56200
- return power === void 0 || !Number.isFinite(power) ? "—" : `${Math.round(power)} W`;
56201
- }
56202
56500
  /** The state to a person: running / idle, or why there is none (unknown). */
56203
56501
  function applianceStateLabel(running, t) {
56204
56502
  if (running === true) return {
@@ -56221,10 +56519,7 @@ function dwellText(running, since, now, t) {
56221
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 });
56222
56520
  }
56223
56521
  function useApplianceState(props) {
56224
- return {
56225
- running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState),
56226
- power: useDeviceState(props.trpc, props.deviceId, (s) => s.powerMeter)
56227
- };
56522
+ return { running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState) };
56228
56523
  }
56229
56524
  /** The drum: a ring with a few items in it, turning only while a cycle runs. */
56230
56525
  function Drum({ running }) {
@@ -56246,7 +56541,7 @@ function Drum({ running }) {
56246
56541
  }
56247
56542
  function ApplianceHeroCard(props) {
56248
56543
  const t = useUiText();
56249
- const { running, power } = useApplianceState(props);
56544
+ const { running } = useApplianceState(props);
56250
56545
  const state = running?.running ?? null;
56251
56546
  const meta = applianceStateLabel(state, t);
56252
56547
  const phase = running?.phase?.trim() ?? "";
@@ -56258,50 +56553,35 @@ function ApplianceHeroCard(props) {
56258
56553
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Drum, { running: state }),
56259
56554
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56260
56555
  className: "flex min-w-0 flex-1 flex-col gap-2",
56261
- children: [
56262
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56263
- className: "flex flex-wrap items-baseline gap-x-2",
56264
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56265
- "data-testid": "appliance-state",
56266
- className: cn("text-3xl font-semibold", meta.tone),
56267
- children: meta.label
56268
- }), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56269
- className: "text-sm tabular-nums text-foreground-subtle",
56270
- children: dwell
56271
- })]
56272
- }),
56273
- phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56274
- className: "text-sm text-foreground-subtle",
56275
- children: phase
56276
- }),
56277
- power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
56278
- "data-testid": "appliance-power",
56279
- className: "inline-flex items-center gap-1 text-sm tabular-nums text-foreground-muted",
56280
- title: power.power === void 0 ? t("uiLibrary.appliance.powerUnknown", "Power unknown") : void 0,
56281
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
56282
- className: "h-3.5 w-3.5",
56283
- "aria-hidden": true
56284
- }), formatWatts(power.power)]
56285
- })
56286
- ]
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
+ })]
56287
56570
  })
56288
56571
  ]
56289
56572
  });
56290
56573
  }
56291
56574
  function ApplianceInlineControl(props) {
56292
- const { running, power } = useApplianceState(props);
56293
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56575
+ const { running } = useApplianceState(props);
56576
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
56294
56577
  className: "inline-flex items-center gap-1.5 max-w-full",
56295
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56578
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56296
56579
  running: running?.running ?? null,
56297
56580
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WashingMachine, {
56298
56581
  className: "h-4 w-4 text-foreground-muted",
56299
56582
  "aria-hidden": true
56300
56583
  })
56301
- }), power !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56302
- className: "text-xs tabular-nums text-foreground-subtle",
56303
- children: formatWatts(power.power)
56304
- })]
56584
+ })
56305
56585
  });
56306
56586
  }
56307
56587
  //#endregion
@@ -57266,6 +57546,106 @@ function SwitchHeroCard({ trpc, deviceId, optimistic }) {
57266
57546
  });
57267
57547
  }
57268
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
57269
57649
  //#region src/composites/device-controls/media-player-hero-card.tsx
57270
57650
  /** Which states map to play() vs pause() on the central transport button. */
57271
57651
  function toggleAction(state) {
@@ -57282,12 +57662,50 @@ function formatClock(ms) {
57282
57662
  const total = Math.floor(ms / 1e3);
57283
57663
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
57284
57664
  }
57285
- 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 }) {
57286
57704
  const t = useUiText();
57287
57705
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57288
- className: "flex flex-col items-center gap-1 text-center",
57706
+ className: "flex flex-col items-center gap-2 text-center",
57289
57707
  children: [
57290
- /* @__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, {
57291
57709
  className: "h-6 w-6 text-foreground-subtle",
57292
57710
  "aria-hidden": "true"
57293
57711
  }),
@@ -57311,18 +57729,23 @@ var STATE_LABEL$1 = {
57311
57729
  buffering: "Buffering",
57312
57730
  standby: "Standby"
57313
57731
  };
57314
- function StateLine({ state, assumed, source }) {
57732
+ function StateLine({ state, assumed, source, running }) {
57315
57733
  const t = useUiText();
57316
57734
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57317
57735
  className: "flex flex-wrap items-center justify-center gap-2 text-xs text-foreground-muted",
57318
57736
  "data-testid": "media-state",
57319
57737
  children: [
57320
- STATE_LABEL$1[state] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
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", {
57321
57744
  className: "font-medium text-foreground",
57322
57745
  children: t(`uiLibrary.mediaPlayerHero.state.${state}`, STATE_LABEL$1[state])
57323
57746
  }),
57324
57747
  assumed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57325
- className: "rounded border border-border px-1.5 py-0.5 text-[11px] text-foreground-subtle",
57748
+ className: "rounded border border-dashed border-border px-1.5 py-0.5 text-[11px] text-foreground-subtle",
57326
57749
  title: t("uiLibrary.mediaPlayerHero.assumedHint", "No feedback from the device: this is what was last sent."),
57327
57750
  "data-testid": "media-assumed",
57328
57751
  children: t("uiLibrary.mediaPlayerHero.assumed", "assumed")
@@ -57334,33 +57757,40 @@ function StateLine({ state, assumed, source }) {
57334
57757
  ]
57335
57758
  });
57336
57759
  }
57337
- function TransportButton({ label, busy, onPress, children, primary }) {
57760
+ function TransportButton({ label, busy, onPress, children, primary, active, testId }) {
57338
57761
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57339
57762
  type: "button",
57340
57763
  "aria-label": label,
57764
+ "aria-pressed": active,
57341
57765
  title: label,
57342
57766
  disabled: busy,
57767
+ "data-testid": testId,
57343
57768
  onClick: (e) => {
57344
57769
  e.stopPropagation();
57345
57770
  if (!busy) onPress();
57346
57771
  },
57347
- 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"),
57348
57773
  children
57349
57774
  });
57350
57775
  }
57351
57776
  /**
57352
- * 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`).
57353
57780
  *
57354
- * Renders (Spec §3):
57355
- * - Now-playing block: cover glyph + title + artist/album.
57356
- * - Seek GripTrack with a native range overlay (gated on DeviceFeature
57357
- * MediaPlayerSeek + a known duration).
57358
- * - Transport row: SkipBack / Play-Pause / SkipForward (lucide icons). Skip
57359
- * buttons are gated on MediaPlayerPrevious / MediaPlayerNext.
57360
- * - Volume GripTrack with a native range overlay + mute toggle (gated on a
57361
- * 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.
57362
57791
  *
57363
57792
  * Cap-gated on `media-player` — returns null when the device lacks the cap.
57793
+ * Motion respects `prefers-reduced-motion`.
57364
57794
  */
57365
57795
  function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57366
57796
  const t = useUiText();
@@ -57371,15 +57801,24 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57371
57801
  slice: (d) => d.state.mediaPlayer
57372
57802
  });
57373
57803
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
57804
+ const runningSlice = useDeviceState(trpc, deviceId, (st) => st.runningState);
57374
57805
  if (cap === void 0) return null;
57375
- if (display === void 0) return null;
57806
+ const view = deriveMediaPlayerView(display, features, { running: runningSlice?.running });
57376
57807
  const busy = !available;
57377
- const action = toggleAction(display.state);
57378
- const supportsSeek = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerSeek) ?? false) && display.duration !== null && display.duration > 0;
57379
- const supportsNext = features?.includes(_camstack_types.DeviceFeature.MediaPlayerNext) ?? false;
57380
- const supportsPrevious = features?.includes(_camstack_types.DeviceFeature.MediaPlayerPrevious) ?? false;
57381
- const volumeLevel = display.volumeLevel;
57382
- const supportsVolume = (features?.includes(_camstack_types.DeviceFeature.MediaPlayerVolume) ?? false) && 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);
57383
57822
  const handleToggle = () => {
57384
57823
  if (busy) return;
57385
57824
  if (action === "play") {
@@ -57390,25 +57829,60 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57390
57829
  cap.pause({});
57391
57830
  }
57392
57831
  };
57393
- const title = display.currentMedia?.title ?? null;
57394
- const subtitle = display.currentMedia?.artist ?? display.currentMedia?.album ?? null;
57395
- const hasNowPlaying = title !== null || subtitle !== null;
57396
- const isAssumed = display.stateSource === "assumed";
57397
- const source = typeof display.source === "string" ? display.source.trim() : "";
57398
- const seekFraction = progressFraction(display.position, display.duration);
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;
57399
57838
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57400
- 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,
57401
57842
  children: [
57402
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateLine, {
57403
- state: display.state,
57404
- assumed: isAssumed,
57405
- source
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
+ })]
57406
57857
  }),
57407
57858
  hasNowPlaying && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NowPlaying, {
57408
57859
  title,
57409
- subtitle
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
+ })]
57410
57884
  }),
57411
- supportsSeek && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57885
+ powered && view.seek && status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57412
57886
  "data-testid": "media-seek",
57413
57887
  className: "flex flex-col gap-1",
57414
57888
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -57422,10 +57896,10 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57422
57896
  "aria-label": t("uiLibrary.mediaPlayerHero.seek", "Seek"),
57423
57897
  type: "range",
57424
57898
  min: 0,
57425
- max: display.duration ?? 0,
57899
+ max: status.duration ?? 0,
57426
57900
  step: 1e3,
57427
57901
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
57428
- value: display.position ?? 0,
57902
+ value: status.position ?? 0,
57429
57903
  disabled: busy,
57430
57904
  onChange: (e) => {
57431
57905
  if (busy) return;
@@ -57436,18 +57910,19 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57436
57910
  }),
57437
57911
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57438
57912
  className: "pointer-events-none",
57439
- 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) })
57440
57914
  })
57441
57915
  ]
57442
57916
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57443
57917
  className: "flex justify-between text-[11px] tabular-nums text-foreground-subtle",
57444
- 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) })]
57445
57919
  })]
57446
57920
  }),
57447
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57921
+ powered && view.transport.playPause && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57448
57922
  className: "flex items-center justify-center gap-4",
57923
+ "data-testid": "media-transport",
57449
57924
  children: [
57450
- supportsPrevious && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57925
+ view.transport.previous && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57451
57926
  label: t("uiLibrary.mediaPlayerHero.previous", "Previous"),
57452
57927
  busy,
57453
57928
  onPress: () => void cap.previous({}),
@@ -57469,7 +57944,16 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57469
57944
  "aria-hidden": "true"
57470
57945
  })
57471
57946
  }),
57472
- 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, {
57473
57957
  label: t("uiLibrary.mediaPlayerHero.next", "Next"),
57474
57958
  busy,
57475
57959
  onPress: () => void cap.next({}),
@@ -57480,24 +57964,24 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57480
57964
  })
57481
57965
  ]
57482
57966
  }),
57483
- supportsVolume && volumeLevel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57967
+ powered && view.volume === "slider" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57484
57968
  "data-testid": "media-volume",
57485
57969
  className: "flex items-center gap-2",
57486
57970
  children: [
57487
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57971
+ view.mute && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57488
57972
  type: "button",
57489
- "aria-label": display.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57490
- 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"),
57491
57975
  disabled: busy,
57492
57976
  onClick: (e) => {
57493
57977
  e.stopPropagation();
57494
- if (busy || display.isMuted === null) return;
57495
- const muted = !display.isMuted;
57978
+ if (busy) return;
57979
+ const muted = status?.isMuted === null || status === void 0 ? true : !status.isMuted;
57496
57980
  applyOptimistic({ isMuted: muted });
57497
57981
  cap.setMute({ muted });
57498
57982
  },
57499
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"),
57500
- children: display.isMuted === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VolumeX, {
57984
+ children: status?.isMuted === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VolumeX, {
57501
57985
  className: "h-4 w-4",
57502
57986
  "aria-hidden": "true"
57503
57987
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Volume2, {
@@ -57519,7 +58003,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57519
58003
  max: 100,
57520
58004
  step: 1,
57521
58005
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
57522
- value: volumeLevel,
58006
+ value: volumeLevel ?? 0,
57523
58007
  disabled: busy,
57524
58008
  onChange: (e) => {
57525
58009
  if (busy) return;
@@ -57530,19 +58014,92 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57530
58014
  }),
57531
58015
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57532
58016
  className: "pointer-events-none",
57533
- 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
+ })
57534
58023
  })
57535
58024
  ]
57536
58025
  }),
57537
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
58026
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57538
58027
  className: "w-9 text-right text-[11px] tabular-nums text-foreground-subtle",
57539
- children: [volumeLevel, "%"]
58028
+ children: volumeLevel === null ? "—" : `${volumeLevel}%`
57540
58029
  })
57541
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
+ })]
57542
58094
  })
57543
58095
  ]
57544
58096
  });
57545
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
+ }
57546
58103
  //#endregion
57547
58104
  //#region src/composites/device-controls/alarm-hero-card.tsx
57548
58105
  var ALARM_STATE_CONFIG = {
@@ -73116,7 +73673,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
73116
73673
  }
73117
73674
  //#endregion
73118
73675
  //#region src/ui-library-version.ts
73119
- var uiLibraryVersion = "1.2.376";
73676
+ var uiLibraryVersion = "1.2.377";
73120
73677
  //#endregion
73121
73678
  //#region src/hooks/use-account-unit-preferences.ts
73122
73679
  /**
@@ -74481,6 +75038,7 @@ exports.PopoverContent = PopoverContent;
74481
75038
  exports.PopoverRowAction = PopoverRowAction;
74482
75039
  exports.PopoverTrigger = PopoverTrigger;
74483
75040
  exports.Power = Power;
75041
+ exports.PowerMeterLine = PowerMeterLine;
74484
75042
  exports.PowerOff = PowerOff;
74485
75043
  exports.PrimaryChildPicker = PrimaryChildPicker;
74486
75044
  exports.PrivacyMaskSettings = PrivacyMaskSettings;
@@ -74507,6 +75065,8 @@ exports.RecordEntriesPanel = RecordEntriesPanel;
74507
75065
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
74508
75066
  exports.RecordingPanel = RecordingPanel;
74509
75067
  exports.RefreshCw = RefreshCw;
75068
+ exports.Repeat = Repeat;
75069
+ exports.Repeat1 = Repeat1;
74510
75070
  exports.ResponseLog = ResponseLog;
74511
75071
  exports.RetrainReviewBadge = RetrainReviewBadge;
74512
75072
  exports.RoleDisplayDefaultsFromServer = RoleDisplayDefaultsFromServer;
@@ -74590,6 +75150,7 @@ exports.ShieldAlert = ShieldAlert;
74590
75150
  exports.ShieldCheck = ShieldCheck;
74591
75151
  exports.ShieldOff = ShieldOff;
74592
75152
  exports.Ship = Ship;
75153
+ exports.Shuffle = Shuffle;
74593
75154
  exports.Sidebar = Sidebar;
74594
75155
  exports.SidebarItem = SidebarItem;
74595
75156
  exports.Siren = Siren;