@camstack/ui-library 1.2.376 → 1.2.378

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) => {
@@ -11135,6 +11215,8 @@ var useCoreBlocksGetCustomization = /*#__PURE__*/ alias(() => trpc.coreBlocks.ge
11135
11215
  var useCoreBlocksListComposableCaps = /*#__PURE__*/ alias(() => trpc.coreBlocks.listComposableCaps.useQuery);
11136
11216
  /** Generated alias around `trpc.coreBlocks.validateComposition.useMutation`. */
11137
11217
  var useCoreBlocksValidateComposition = /*#__PURE__*/ alias(() => trpc.coreBlocks.validateComposition.useMutation);
11218
+ /** Generated alias around `trpc.coreBlocks.proposeRunningThreshold.useQuery`. */
11219
+ var useCoreBlocksProposeRunningThreshold = /*#__PURE__*/ alias(() => trpc.coreBlocks.proposeRunningThreshold.useQuery);
11138
11220
  /** Generated alias around `trpc.coreBlocks.reportAutomationRun.useMutation`. */
11139
11221
  var useCoreBlocksReportAutomationRun = /*#__PURE__*/ alias(() => trpc.coreBlocks.reportAutomationRun.useMutation);
11140
11222
  /** Generated alias around `trpc.coreBlocks.getAutomationRunState.useQuery`. */
@@ -27761,6 +27843,92 @@ function NavigationPanel({ deviceId }) {
27761
27843
  });
27762
27844
  }
27763
27845
  //#endregion
27846
+ //#region src/composites/cap-settings/PowerMeterLine.tsx
27847
+ /**
27848
+ * PowerMeterLine — the ONE place a device's electrical draw is shown (D1004):
27849
+ * a slim line directly under the device page's hero card, for every device
27850
+ * bound to the `power-meter` capability, whatever its type. The cap declares
27851
+ * `ui: { kind:'widget', widgetId:'host/power-meter-line', placement:'hero' }`;
27852
+ * no device-controls hero card draws watts itself
27853
+ * (`scripts/check-no-hero-self-drawn-power.ts`).
27854
+ *
27855
+ * A reading the device never reported is not drawn as a number: no slice, or
27856
+ * a slice with neither power nor energy, renders nothing; a missing field
27857
+ * (power but no energy, or the reverse) is simply absent, never `0` or `NaN`.
27858
+ */
27859
+ /** A finite number, or null: the one gate between a slice field and the screen. */
27860
+ function known(value) {
27861
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
27862
+ }
27863
+ /** Whole watts below a kilowatt, one decimal above. */
27864
+ function formatPowerWatts(watts) {
27865
+ return Math.abs(watts) >= 1e3 ? {
27866
+ value: (watts / 1e3).toFixed(1),
27867
+ unit: "kW"
27868
+ } : {
27869
+ value: String(Math.round(watts)),
27870
+ unit: "W"
27871
+ };
27872
+ }
27873
+ /** Two decimals under 100 kWh, whole kWh above. */
27874
+ function formatEnergyKwh(kwh) {
27875
+ return Math.abs(kwh) >= 100 ? String(Math.round(kwh)) : kwh.toFixed(2);
27876
+ }
27877
+ function PowerMeterLine({ deviceId }) {
27878
+ const t = useUiText();
27879
+ const { slice } = useDeviceCapability({
27880
+ trpc: useSystem().trpcClient,
27881
+ deviceId,
27882
+ cap: (d) => d.powerMeter,
27883
+ slice: (d) => d.state.powerMeter
27884
+ });
27885
+ const power = known(slice?.power);
27886
+ const energy = known(slice?.energy);
27887
+ if (power === null && energy === null) return null;
27888
+ const drawing = power !== null && power > 0;
27889
+ const shown = power === null ? null : formatPowerWatts(power);
27890
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27891
+ "data-testid": "power-meter-line",
27892
+ className: "flex items-center gap-3 rounded-lg border border-border bg-surface px-3 py-2",
27893
+ children: [
27894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27895
+ className: "relative flex h-5 w-5 shrink-0 items-center justify-center",
27896
+ children: [drawing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27897
+ "aria-hidden": "true",
27898
+ "data-testid": "power-meter-pulse",
27899
+ className: "absolute inset-0 rounded-full bg-primary/20 motion-safe:animate-pulse"
27900
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, {
27901
+ className: cn("relative h-3.5 w-3.5", drawing ? "text-primary" : "text-foreground-subtle"),
27902
+ "aria-hidden": true
27903
+ })]
27904
+ }),
27905
+ shown !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27906
+ "data-testid": "power-meter-power",
27907
+ className: "flex items-baseline gap-1 tabular-nums",
27908
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27909
+ className: "text-lg font-semibold text-foreground",
27910
+ children: shown.value
27911
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27912
+ className: "text-xs text-foreground-muted",
27913
+ children: shown.unit
27914
+ })]
27915
+ }),
27916
+ energy !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27917
+ "data-testid": "power-meter-energy",
27918
+ className: "ml-auto flex items-baseline gap-1 tabular-nums",
27919
+ title: t("uiLibrary.powerMeterLine.energy", "Energy"),
27920
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27921
+ className: "text-sm text-foreground-muted",
27922
+ children: formatEnergyKwh(energy)
27923
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27924
+ className: "text-xs text-foreground-subtle",
27925
+ children: "kWh"
27926
+ })]
27927
+ })
27928
+ ]
27929
+ });
27930
+ }
27931
+ //#endregion
27764
27932
  //#region src/lib/cap-error.ts
27765
27933
  /**
27766
27934
  * Shared capability-error utilities for the admin UI.
@@ -30739,6 +30907,7 @@ var HOST_WIDGETS = {
30739
30907
  "host/ptz-panel": PtzPanel,
30740
30908
  "host/navigation-panel": NavigationPanel,
30741
30909
  "host/consumables-panel": ConsumablesPanel,
30910
+ "host/power-meter-line": PowerMeterLine,
30742
30911
  "host/ptz-autotrack": AutotrackSection,
30743
30912
  "host/recording-panel": RecordingPanel,
30744
30913
  "host/camera-grid-layout": CameraGridLayoutEditor,
@@ -37075,8 +37244,8 @@ function pickWifiIcon(signalPercent) {
37075
37244
  }
37076
37245
  function signalColor(signalPercent) {
37077
37246
  if (signalPercent === null) return "text-foreground-subtle";
37078
- if (signalPercent <= 20) return "text-rose-400";
37079
- if (signalPercent <= 50) return "text-amber-400";
37247
+ if (signalPercent <= 20) return "text-danger";
37248
+ if (signalPercent <= 50) return "text-warning";
37080
37249
  return "text-foreground-subtle";
37081
37250
  }
37082
37251
  function NetworkLinkBadge({ status, className }) {
@@ -37087,15 +37256,16 @@ function NetworkLinkBadge({ status, className }) {
37087
37256
  title: t("uiLibrary.networkLinkBadge.ethernet", "Network: ethernet"),
37088
37257
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cable, { className: "h-3.5 w-3.5" })
37089
37258
  });
37090
- const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(status.signal);
37091
- const signal = status.signal === null ? null : Math.round(status.signal);
37259
+ const reading = typeof status.signal === "number" && Number.isFinite(status.signal) ? status.signal : null;
37260
+ const Icon = status.type === "cellular" ? RadioTower : pickWifiIcon(reading);
37261
+ const signal = reading === null ? null : Math.round(reading);
37092
37262
  const networkText = status.ssid ? t("uiLibrary.networkLinkBadge.typeSsid", "Network: {{type}} ({{ssid}})", {
37093
37263
  type: status.type,
37094
37264
  ssid: status.ssid
37095
37265
  }) : t("uiLibrary.networkLinkBadge.type", "Network: {{type}}", { type: status.type });
37096
37266
  const title = signal === null ? networkText : `${networkText} · ${t("uiLibrary.networkLinkBadge.signal", "signal {{percent}}%", { percent: signal })}`;
37097
37267
  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),
37268
+ className: cn("inline-flex items-center gap-1 text-[10px] font-medium", signalColor(reading), className),
37099
37269
  title,
37100
37270
  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
37271
  });
@@ -48782,29 +48952,179 @@ function ClimatePanel({ trpc, deviceId, features: _features, layout, optimistic
48782
48952
  });
48783
48953
  }
48784
48954
  //#endregion
48955
+ //#region src/composites/device-controls/media-player-view.ts
48956
+ /**
48957
+ * The state → view mapping of the media-player hero card, kept pure so it can
48958
+ * be specified without rendering.
48959
+ *
48960
+ * Two questions are answered separately. POWER is whether the device is on
48961
+ * (`off` / `standby` / on); RUNNING is what a powered device is doing
48962
+ * (`playing` / `paused` / `buffering` / `idle`). A status that never arrived
48963
+ * is `unknown` for both, and an unknown state is never drawn as off: it is
48964
+ * its own look. A value the driver only guesses (infrared has no feedback)
48965
+ * carries an `assumed` flag so the card can draw it as a guess.
48966
+ *
48967
+ * The controls are derived from what the device exposes — the status fields
48968
+ * it fills and the `DeviceFeature` flags it declares — never from a vendor.
48969
+ */
48970
+ var NO_TRANSPORT = {
48971
+ playPause: false,
48972
+ previous: false,
48973
+ next: false,
48974
+ stop: false
48975
+ };
48976
+ /** Whether the status carries any activity the device actually reported. */
48977
+ function runningOf(state) {
48978
+ switch (state) {
48979
+ case "playing":
48980
+ case "paused":
48981
+ case "buffering":
48982
+ case "idle": return state;
48983
+ case void 0: return "unknown";
48984
+ default: return "none";
48985
+ }
48986
+ }
48987
+ function artworkOf(status) {
48988
+ const url = status.currentMedia?.imageUrl?.trim() ?? "";
48989
+ return url === "" ? null : url;
48990
+ }
48991
+ function powerOf(state) {
48992
+ if (state === void 0) return "unknown";
48993
+ if (state === "off") return "off";
48994
+ if (state === "standby") return "standby";
48995
+ return "on";
48996
+ }
48997
+ /**
48998
+ * A slice from before `assumedFields` existed (stateSource assumed, no list)
48999
+ * means the whole state is a guess; with a list, only the named fields are.
49000
+ */
49001
+ function isAssumed(status, field) {
49002
+ if (status.stateSource !== "assumed") return false;
49003
+ const listed = status.assumedFields ?? [];
49004
+ return listed.length === 0 || listed.includes(field);
49005
+ }
49006
+ var UNKNOWN_VIEW = {
49007
+ power: "unknown",
49008
+ runningReading: "unknown",
49009
+ running: "unknown",
49010
+ stateAssumed: false,
49011
+ canSwitchPower: false,
49012
+ transport: NO_TRANSPORT,
49013
+ seek: false,
49014
+ volume: "none",
49015
+ volumeAssumed: false,
49016
+ volumeKnown: false,
49017
+ muteKnown: false,
49018
+ artworkUrl: null,
49019
+ mute: false,
49020
+ muteAssumed: false,
49021
+ source: null,
49022
+ sourceAssumed: false,
49023
+ shuffle: false,
49024
+ repeat: false
49025
+ };
49026
+ function readingOf(running) {
49027
+ if (running === true) return "running";
49028
+ if (running === false) return "idle";
49029
+ return "unknown";
49030
+ }
49031
+ function deriveMediaPlayerView(status, features, extras = {}) {
49032
+ const runningReading = readingOf(extras.running);
49033
+ const view = deriveFromStatus(status, features);
49034
+ const guessed = status === void 0 || status.state === void 0 || view.stateAssumed;
49035
+ if (runningReading === "unknown" || !guessed) return {
49036
+ ...view,
49037
+ runningReading
49038
+ };
49039
+ if (runningReading === "idle") return {
49040
+ ...UNKNOWN_VIEW,
49041
+ power: "off",
49042
+ running: "none",
49043
+ canSwitchPower: view.canSwitchPower,
49044
+ runningReading
49045
+ };
49046
+ return {
49047
+ ...status === void 0 ? view : deriveFromStatus(status.state === void 0 || status.state === "off" || status.state === "standby" ? {
49048
+ ...status,
49049
+ state: "on"
49050
+ } : status, features),
49051
+ power: "on",
49052
+ stateAssumed: false,
49053
+ runningReading
49054
+ };
49055
+ }
49056
+ function deriveFromStatus(status, features) {
49057
+ const has = (f) => features?.includes(f) ?? false;
49058
+ const canSwitchPower = has(_camstack_types.DeviceFeature.MediaPlayerPower);
49059
+ if (status === void 0 || status.state === void 0) return {
49060
+ ...UNKNOWN_VIEW,
49061
+ canSwitchPower
49062
+ };
49063
+ const power = powerOf(status.state);
49064
+ const running = runningOf(status.state);
49065
+ const base = {
49066
+ ...UNKNOWN_VIEW,
49067
+ power,
49068
+ running,
49069
+ stateAssumed: isAssumed(status, "state"),
49070
+ canSwitchPower
49071
+ };
49072
+ if (power !== "on") return base;
49073
+ const hasTransportFeature = has(_camstack_types.DeviceFeature.MediaPlayerNext) || has(_camstack_types.DeviceFeature.MediaPlayerPrevious) || has(_camstack_types.DeviceFeature.MediaPlayerStop) || has(_camstack_types.DeviceFeature.MediaPlayerSeek);
49074
+ const volume = has(_camstack_types.DeviceFeature.MediaPlayerVolume) ? "slider" : has(_camstack_types.DeviceFeature.MediaPlayerVolumeStep) ? "steps" : "none";
49075
+ const sourceName = typeof status.source === "string" ? status.source.trim() : "";
49076
+ const options = status.availableSources ?? [];
49077
+ const hasSource = sourceName !== "" || options.length > 0;
49078
+ return {
49079
+ ...base,
49080
+ transport: {
49081
+ playPause: running !== "none" || hasTransportFeature,
49082
+ previous: has(_camstack_types.DeviceFeature.MediaPlayerPrevious),
49083
+ next: has(_camstack_types.DeviceFeature.MediaPlayerNext),
49084
+ stop: has(_camstack_types.DeviceFeature.MediaPlayerStop)
49085
+ },
49086
+ seek: has(_camstack_types.DeviceFeature.MediaPlayerSeek) && status.duration !== null && status.duration > 0,
49087
+ volume,
49088
+ volumeAssumed: isAssumed(status, "volumeLevel"),
49089
+ mute: has(_camstack_types.DeviceFeature.MediaPlayerMute) || volume === "slider" && status.isMuted !== null,
49090
+ volumeKnown: status.volumeLevel !== null,
49091
+ muteKnown: status.isMuted !== null,
49092
+ artworkUrl: artworkOf(status),
49093
+ muteAssumed: isAssumed(status, "isMuted"),
49094
+ source: hasSource ? {
49095
+ current: sourceName,
49096
+ options,
49097
+ selectable: has(_camstack_types.DeviceFeature.MediaPlayerSelectSource) && options.length > 1
49098
+ } : null,
49099
+ sourceAssumed: isAssumed(status, "source"),
49100
+ shuffle: has(_camstack_types.DeviceFeature.MediaPlayerShuffle) && status.shuffle !== null,
49101
+ repeat: has(_camstack_types.DeviceFeature.MediaPlayerRepeat) && status.repeat !== null
49102
+ };
49103
+ }
49104
+ //#endregion
48785
49105
  //#region src/composites/device-controls/media-player-panel.tsx
48786
49106
  /**
48787
49107
  * Exhaustive colour map — one entry per MediaPlayerState.
48788
49108
  * Build fails if a state is added and not reflected here.
48789
49109
  */
48790
49110
  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"
49111
+ playing: "border-success/50 bg-success/10",
49112
+ paused: "border-warning/50 bg-warning/10",
49113
+ buffering: "border-info/50 bg-info/10",
49114
+ on: "border-foreground-subtle/40 bg-surface-hover",
49115
+ idle: "border-foreground-subtle/40 bg-surface-hover",
49116
+ off: "border-border bg-surface",
49117
+ standby: "border-border bg-surface"
48798
49118
  };
48799
49119
  /** Dot indicator colour (small circle) per state. */
48800
49120
  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"
49121
+ playing: "bg-success",
49122
+ paused: "bg-warning",
49123
+ buffering: "bg-info",
49124
+ on: "bg-foreground-subtle",
49125
+ idle: "bg-foreground-subtle",
49126
+ off: "bg-border",
49127
+ standby: "bg-border"
48808
49128
  };
48809
49129
  /** Exhaustive tuple of valid repeat values — single source of truth. */
48810
49130
  var REPEAT_VALUES = [
@@ -48828,7 +49148,7 @@ function toggleAction$1(state) {
48828
49148
  if (state === "playing" || state === "buffering") return "pause";
48829
49149
  return "play";
48830
49150
  }
48831
- function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
49151
+ function MediaPlayerPanel({ trpc, deviceId, features, layout, optimistic }) {
48832
49152
  const t = useUiText();
48833
49153
  const { cap, slice, available } = useDeviceCapability({
48834
49154
  trpc,
@@ -48838,6 +49158,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48838
49158
  });
48839
49159
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48840
49160
  if (display === void 0) return null;
49161
+ const declared = deriveMediaPlayerView(display, features, {});
48841
49162
  const busy = !available;
48842
49163
  const action = toggleAction$1(display.state);
48843
49164
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -48892,7 +49213,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48892
49213
  children: "⏭"
48893
49214
  })]
48894
49215
  }),
48895
- display.volumeLevel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
49216
+ (declared.volume === "slider" || display.volumeLevel !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48896
49217
  className: "flex items-center gap-2 text-xs text-foreground-subtle",
48897
49218
  children: [
48898
49219
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("uiLibrary.mediaPlayerPanel.vol", "Vol") }),
@@ -48902,7 +49223,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48902
49223
  min: 0,
48903
49224
  max: 100,
48904
49225
  step: 1,
48905
- value: display.volumeLevel,
49226
+ value: display.volumeLevel ?? 0,
48906
49227
  disabled: busy,
48907
49228
  onChange: (e) => {
48908
49229
  const volumeLevel = Number(e.target.value);
@@ -48910,25 +49231,25 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48910
49231
  if (!busy) cap?.setVolume({ volumeLevel });
48911
49232
  }
48912
49233
  }),
48913
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48914
49235
  className: "tabular-nums w-8 text-right",
48915
- children: [display.volumeLevel, "%"]
49236
+ children: display.volumeLevel === null ? "—" : `${display.volumeLevel}%`
48916
49237
  })
48917
49238
  ]
48918
49239
  }),
48919
- display.isMuted !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
49240
+ (declared.mute || display.isMuted !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
48920
49241
  type: "button",
48921
49242
  "aria-label": display.isMuted ? t("uiLibrary.mediaPlayerPanel.unmute", "Unmute") : t("uiLibrary.mediaPlayerPanel.mute", "Mute"),
48922
49243
  disabled: busy,
48923
49244
  onClick: (e) => {
48924
49245
  e.stopPropagation();
48925
- if (!busy && display.isMuted !== null) {
48926
- const muted = !display.isMuted;
49246
+ if (!busy) {
49247
+ const muted = display.isMuted === null ? true : !display.isMuted;
48927
49248
  applyOptimistic({ isMuted: muted });
48928
49249
  cap?.setMute({ muted });
48929
49250
  }
48930
49251
  },
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"),
49252
+ 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
49253
  children: display.isMuted ? "🔇" : "🔊"
48933
49254
  }),
48934
49255
  display.availableSources.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -48967,7 +49288,7 @@ function MediaPlayerPanel({ trpc, deviceId, features: _features, layout, optimis
48967
49288
  cap?.setShuffle({ shuffle });
48968
49289
  }
48969
49290
  },
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"),
49291
+ 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
49292
  children: ["⇌ ", t("uiLibrary.mediaPlayerPanel.shuffle", "Shuffle")]
48972
49293
  }),
48973
49294
  display.repeat !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -55611,7 +55932,7 @@ function SwingToggle({ axis, on, busy, onToggle }) {
55611
55932
  * "Run N min".
55612
55933
  * - HeaterHeroCard (device-detail hero): the glowing heat backdrop (the same
55613
55934
  * 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
55935
+ * on/off switch and the timed
55615
55936
  * run (number, unit, Run, countdown, Cancel).
55616
55937
  *
55617
55938
  * Every reading may be unknown (`null`) and is drawn as a dash, never as zero.
@@ -55633,12 +55954,6 @@ function useHeater({ trpc, deviceId, optimistic }) {
55633
55954
  cap: (d) => d.runningState,
55634
55955
  slice: (d) => d.state.runningState
55635
55956
  });
55636
- const power = useDeviceCapability({
55637
- trpc,
55638
- deviceId,
55639
- cap: (d) => d.powerMeter,
55640
- slice: (d) => d.state.powerMeter
55641
- });
55642
55957
  const timer = useDeviceCapability({
55643
55958
  trpc,
55644
55959
  deviceId,
@@ -55652,7 +55967,6 @@ function useHeater({ trpc, deviceId, optimistic }) {
55652
55967
  available,
55653
55968
  applyOptimistic,
55654
55969
  running: running.slice,
55655
- power: power.slice,
55656
55970
  timer
55657
55971
  };
55658
55972
  }
@@ -55695,11 +56009,10 @@ function HeaterInlineControl(props) {
55695
56009
  }
55696
56010
  function HeaterHeroCard(props) {
55697
56011
  const t = useUiText();
55698
- const { cap, display, available, applyOptimistic, running, power, timer } = useHeater(props);
56012
+ const { cap, display, available, applyOptimistic, running, timer } = useHeater(props);
55699
56013
  if (cap === void 0 || display === void 0) return null;
55700
56014
  const on = display.on === true;
55701
56015
  const heating = running?.running ?? on;
55702
- const watts = power?.power ?? null;
55703
56016
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55704
56017
  className: "relative flex flex-col items-stretch gap-3 overflow-hidden rounded-lg border border-border bg-surface p-4",
55705
56018
  children: [
@@ -55717,21 +56030,13 @@ function HeaterHeroCard(props) {
55717
56030
  })
55718
56031
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55719
56032
  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
- ]
56033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Flame, {
56034
+ className: cn("h-8 w-8", heating ? "text-warning" : "text-foreground-subtle"),
56035
+ "aria-hidden": true
56036
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56037
+ className: "text-sm font-medium text-foreground",
56038
+ children: heating ? t("uiLibrary.heater.heating", "Heating") : t("uiLibrary.heater.off", "Off")
56039
+ })]
55735
56040
  })]
55736
56041
  }),
55737
56042
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -56176,17 +56481,16 @@ function DishwasherHeroCard(props) {
56176
56481
  * Appliance card for a `DeviceType.Washer` / `DeviceType.Dryer` device.
56177
56482
  *
56178
56483
  * 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.
56484
+ * the cycle is running (the `running-state` cap) and for how long. The draw
56485
+ * is NOT drawn here: the `power-meter` cap's hero widget shows it under every
56486
+ * hero card (D1004).
56182
56487
  * - ApplianceInlineControl (device-list row): the running badge round the
56183
- * drum icon, plus the live watts when there are any.
56488
+ * drum icon.
56184
56489
  * - ApplianceHeroCard (device-detail hero): the drum (turning only while a
56185
- * cycle runs), the state, the phase, the dwell and the power.
56490
+ * cycle runs), the state, the phase and the dwell.
56186
56491
  *
56187
56492
  * 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.
56493
+ * drum stays still and unbadged.
56190
56494
  */
56191
56495
  var KEYFRAMES = `
56192
56496
  @keyframes apl-tumble { to { transform: rotate(360deg) } }
@@ -56195,10 +56499,6 @@ var KEYFRAMES = `
56195
56499
  .apl-drum-run { animation: none !important; }
56196
56500
  }
56197
56501
  `;
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
56502
  /** The state to a person: running / idle, or why there is none (unknown). */
56203
56503
  function applianceStateLabel(running, t) {
56204
56504
  if (running === true) return {
@@ -56221,10 +56521,7 @@ function dwellText(running, since, now, t) {
56221
56521
  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
56522
  }
56223
56523
  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
- };
56524
+ return { running: useDeviceState(props.trpc, props.deviceId, (s) => s.runningState) };
56228
56525
  }
56229
56526
  /** The drum: a ring with a few items in it, turning only while a cycle runs. */
56230
56527
  function Drum({ running }) {
@@ -56246,7 +56543,7 @@ function Drum({ running }) {
56246
56543
  }
56247
56544
  function ApplianceHeroCard(props) {
56248
56545
  const t = useUiText();
56249
- const { running, power } = useApplianceState(props);
56546
+ const { running } = useApplianceState(props);
56250
56547
  const state = running?.running ?? null;
56251
56548
  const meta = applianceStateLabel(state, t);
56252
56549
  const phase = running?.phase?.trim() ?? "";
@@ -56258,50 +56555,35 @@ function ApplianceHeroCard(props) {
56258
56555
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Drum, { running: state }),
56259
56556
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56260
56557
  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
- ]
56558
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56559
+ className: "flex flex-wrap items-baseline gap-x-2",
56560
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56561
+ "data-testid": "appliance-state",
56562
+ className: cn("text-3xl font-semibold", meta.tone),
56563
+ children: meta.label
56564
+ }), dwell !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56565
+ className: "text-sm tabular-nums text-foreground-subtle",
56566
+ children: dwell
56567
+ })]
56568
+ }), phase !== "" && state !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
56569
+ className: "text-sm text-foreground-subtle",
56570
+ children: phase
56571
+ })]
56287
56572
  })
56288
56573
  ]
56289
56574
  });
56290
56575
  }
56291
56576
  function ApplianceInlineControl(props) {
56292
- const { running, power } = useApplianceState(props);
56293
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
56577
+ const { running } = useApplianceState(props);
56578
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
56294
56579
  className: "inline-flex items-center gap-1.5 max-w-full",
56295
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56580
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56296
56581
  running: running?.running ?? null,
56297
56582
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WashingMachine, {
56298
56583
  className: "h-4 w-4 text-foreground-muted",
56299
56584
  "aria-hidden": true
56300
56585
  })
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
- })]
56586
+ })
56305
56587
  });
56306
56588
  }
56307
56589
  //#endregion
@@ -57266,6 +57548,106 @@ function SwitchHeroCard({ trpc, deviceId, optimistic }) {
57266
57548
  });
57267
57549
  }
57268
57550
  //#endregion
57551
+ //#region src/composites/device-controls/media-player-hero-parts.tsx
57552
+ /**
57553
+ * The animated parts of the media-player hero: the power orb and the running
57554
+ * indicator. Motion is transform / opacity only, plays only while the state
57555
+ * says so, and stops under `prefers-reduced-motion` (the state is still told
57556
+ * by colour, icon and text, never by motion alone).
57557
+ */
57558
+ var MEDIA_HERO_KEYFRAMES = `
57559
+ @keyframes mph-eq { 0%, 100% { transform: scaleY(.3) } 50% { transform: scaleY(1) } }
57560
+ @keyframes mph-halo { 0% { transform: scale(.92); opacity: .55 } 100% { transform: scale(1.4); opacity: 0 } }
57561
+ .mph-eq { transform-origin: bottom; animation: mph-eq 1s ease-in-out infinite; }
57562
+ .mph-eq:nth-child(2) { animation-duration: .8s; animation-delay: -.3s; }
57563
+ .mph-eq:nth-child(3) { animation-duration: 1.2s; animation-delay: -.6s; }
57564
+ .mph-eq:nth-child(4) { animation-duration: .9s; animation-delay: -.1s; }
57565
+ .mph-halo { animation: mph-halo 2.8s ease-out infinite; }
57566
+ @keyframes mph-in { from { opacity: 0 } to { opacity: 1 } }
57567
+ @keyframes mph-out { from { opacity: 1 } to { opacity: 0 } }
57568
+ .mph-fade-in { animation: mph-in .6s ease-out both; }
57569
+ .mph-fade-out { animation: mph-out .6s ease-out both; }
57570
+ @media (prefers-reduced-motion: reduce) {
57571
+ .mph-eq, .mph-halo, .mph-fade-in, .mph-fade-out { animation: none !important; }
57572
+ .mph-fade-out { opacity: 0; }
57573
+ .mph-eq:nth-child(odd) { transform: scaleY(.6); }
57574
+ .mph-halo { opacity: 0; }
57575
+ }
57576
+ `;
57577
+ var ORB_TONE = {
57578
+ on: "border-success bg-success/15 text-success shadow-lg shadow-success/30",
57579
+ standby: "border-warning bg-warning/10 text-warning",
57580
+ off: "border-border bg-surface-hover text-foreground-subtle",
57581
+ unknown: "border-dashed border-border bg-transparent text-foreground-subtle"
57582
+ };
57583
+ /** The device's power: a glowing ring when on, amber in standby, flat when off, dashed when unknown. */
57584
+ function PowerOrb({ power, onPress, label, busy }) {
57585
+ const body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: power === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CircleQuestionMark, {
57586
+ className: "h-7 w-7",
57587
+ "aria-hidden": "true"
57588
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, {
57589
+ className: "h-7 w-7",
57590
+ "aria-hidden": "true"
57591
+ }) });
57592
+ 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]);
57593
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57594
+ className: "relative flex h-20 w-20 items-center justify-center",
57595
+ "data-power": power,
57596
+ children: [power === "on" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57597
+ "aria-hidden": "true",
57598
+ "data-testid": "media-power-halo",
57599
+ className: "mph-halo absolute h-16 w-16 rounded-full bg-success/25"
57600
+ }), onPress === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57601
+ className: classes,
57602
+ "data-testid": "media-power",
57603
+ role: "img",
57604
+ "aria-label": label,
57605
+ children: body
57606
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57607
+ type: "button",
57608
+ "aria-label": label,
57609
+ title: label,
57610
+ disabled: busy,
57611
+ "data-testid": "media-power",
57612
+ onClick: (e) => {
57613
+ e.stopPropagation();
57614
+ if (!busy) onPress();
57615
+ },
57616
+ className: cn(classes, "hover:brightness-110 active:scale-95", busy && "cursor-not-allowed opacity-40"),
57617
+ children: body
57618
+ })]
57619
+ });
57620
+ }
57621
+ var EQ_BARS = [
57622
+ 0,
57623
+ 1,
57624
+ 2,
57625
+ 3
57626
+ ];
57627
+ /** Equalizer while playing, a spinner while buffering, a pause glyph when paused; nothing otherwise. */
57628
+ function RunningIndicator({ running }) {
57629
+ if (running === "playing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57630
+ className: "inline-flex h-4 items-end gap-0.5 text-primary",
57631
+ "data-testid": "media-running",
57632
+ "data-running": "playing",
57633
+ "aria-hidden": "true",
57634
+ children: EQ_BARS.map((bar) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "mph-eq h-4 w-1 rounded-full bg-primary" }, bar))
57635
+ });
57636
+ if (running === "buffering") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, {
57637
+ className: "h-4 w-4 animate-spin text-foreground-muted motion-reduce:animate-none",
57638
+ "data-testid": "media-running",
57639
+ "data-running": "buffering",
57640
+ "aria-hidden": "true"
57641
+ });
57642
+ if (running === "paused") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, {
57643
+ className: "h-4 w-4 text-foreground-muted",
57644
+ "data-testid": "media-running",
57645
+ "data-running": "paused",
57646
+ "aria-hidden": "true"
57647
+ });
57648
+ return null;
57649
+ }
57650
+ //#endregion
57269
57651
  //#region src/composites/device-controls/media-player-hero-card.tsx
57270
57652
  /** Which states map to play() vs pause() on the central transport button. */
57271
57653
  function toggleAction(state) {
@@ -57282,12 +57664,50 @@ function formatClock(ms) {
57282
57664
  const total = Math.floor(ms / 1e3);
57283
57665
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
57284
57666
  }
57285
- function NowPlaying({ title, subtitle }) {
57667
+ var NEXT_REPEAT = {
57668
+ off: "all",
57669
+ all: "one",
57670
+ one: "off"
57671
+ };
57672
+ /** The artwork of what is playing; a change cross-fades from the previous cover. */
57673
+ function Artwork({ url }) {
57674
+ const [shown, setShown] = (0, react$1.useState)(url);
57675
+ const [previous, setPrevious] = (0, react$1.useState)(null);
57676
+ const [failed, setFailed] = (0, react$1.useState)(null);
57677
+ const last = (0, react$1.useRef)(url);
57678
+ (0, react$1.useEffect)(() => {
57679
+ if (last.current === url) return;
57680
+ setPrevious(last.current);
57681
+ setShown(url);
57682
+ last.current = url;
57683
+ const timer = setTimeout(() => setPrevious(null), 700);
57684
+ return () => clearTimeout(timer);
57685
+ }, [url]);
57686
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57687
+ "data-testid": "media-artwork",
57688
+ className: "relative h-40 w-40 overflow-hidden rounded-lg border border-border bg-surface-hover",
57689
+ children: [previous !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
57690
+ src: previous,
57691
+ alt: "",
57692
+ className: "mph-fade-out absolute inset-0 h-full w-full object-cover"
57693
+ }), shown !== null && failed !== shown ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
57694
+ src: shown,
57695
+ alt: "",
57696
+ "data-testid": "media-artwork-img",
57697
+ className: "mph-fade-in absolute inset-0 h-full w-full object-cover",
57698
+ onError: () => setFailed(shown)
57699
+ }, shown) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Music, {
57700
+ className: "absolute inset-0 m-auto h-8 w-8 text-foreground-subtle",
57701
+ "aria-hidden": "true"
57702
+ })]
57703
+ });
57704
+ }
57705
+ function NowPlaying({ title, subtitle, artworkUrl }) {
57286
57706
  const t = useUiText();
57287
57707
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57288
- className: "flex flex-col items-center gap-1 text-center",
57708
+ className: "flex flex-col items-center gap-2 text-center",
57289
57709
  children: [
57290
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Music, {
57710
+ artworkUrl !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Artwork, { url: artworkUrl }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Music, {
57291
57711
  className: "h-6 w-6 text-foreground-subtle",
57292
57712
  "aria-hidden": "true"
57293
57713
  }),
@@ -57311,18 +57731,23 @@ var STATE_LABEL$1 = {
57311
57731
  buffering: "Buffering",
57312
57732
  standby: "Standby"
57313
57733
  };
57314
- function StateLine({ state, assumed, source }) {
57734
+ function StateLine({ state, assumed, source, running }) {
57315
57735
  const t = useUiText();
57316
57736
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57317
57737
  className: "flex flex-wrap items-center justify-center gap-2 text-xs text-foreground-muted",
57318
57738
  "data-testid": "media-state",
57319
57739
  children: [
57320
- STATE_LABEL$1[state] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57740
+ running,
57741
+ state === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57742
+ className: "font-medium text-foreground-subtle",
57743
+ "data-testid": "media-state-unknown",
57744
+ children: t("uiLibrary.mediaPlayerHero.state.unknown", "State unknown")
57745
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57321
57746
  className: "font-medium text-foreground",
57322
57747
  children: t(`uiLibrary.mediaPlayerHero.state.${state}`, STATE_LABEL$1[state])
57323
57748
  }),
57324
57749
  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",
57750
+ className: "rounded border border-dashed border-border px-1.5 py-0.5 text-[11px] text-foreground-subtle",
57326
57751
  title: t("uiLibrary.mediaPlayerHero.assumedHint", "No feedback from the device: this is what was last sent."),
57327
57752
  "data-testid": "media-assumed",
57328
57753
  children: t("uiLibrary.mediaPlayerHero.assumed", "assumed")
@@ -57334,33 +57759,40 @@ function StateLine({ state, assumed, source }) {
57334
57759
  ]
57335
57760
  });
57336
57761
  }
57337
- function TransportButton({ label, busy, onPress, children, primary }) {
57762
+ function TransportButton({ label, busy, onPress, children, primary, active, testId }) {
57338
57763
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57339
57764
  type: "button",
57340
57765
  "aria-label": label,
57766
+ "aria-pressed": active,
57341
57767
  title: label,
57342
57768
  disabled: busy,
57769
+ "data-testid": testId,
57343
57770
  onClick: (e) => {
57344
57771
  e.stopPropagation();
57345
57772
  if (!busy) onPress();
57346
57773
  },
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"),
57774
+ 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
57775
  children
57349
57776
  });
57350
57777
  }
57351
57778
  /**
57352
- * MediaPlayerHeroCard — now-playing hero for MediaPlayer devices.
57779
+ * MediaPlayerHeroCard — the state of a MediaPlayer device and every control it
57780
+ * exposes, drawn from `deriveMediaPlayerView` (the state → view mapping and
57781
+ * its spec live in `media-player-view.ts`).
57353
57782
  *
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).
57783
+ * - Power orb: glowing when on, amber in standby, flat when off, dashed with a
57784
+ * question mark while the state is unknown (never drawn as off). It is the
57785
+ * power switch when the device declares `MediaPlayerPower`.
57786
+ * - Running line: equalizer while playing, spinner while buffering, pause
57787
+ * glyph when paused; a value the driver only guesses carries an "assumed" mark.
57788
+ * - Only while the device is on: now-playing, seek, transport (previous /
57789
+ * play-pause / stop / next), volume (slider with mute, or steps when the
57790
+ * device has no level reading), the source (a picker when it can switch),
57791
+ * shuffle and repeat. Each appears only when the status fills it or a
57792
+ * `DeviceFeature` declares it — no per-vendor list.
57362
57793
  *
57363
57794
  * Cap-gated on `media-player` — returns null when the device lacks the cap.
57795
+ * Motion respects `prefers-reduced-motion`.
57364
57796
  */
57365
57797
  function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57366
57798
  const t = useUiText();
@@ -57371,15 +57803,24 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57371
57803
  slice: (d) => d.state.mediaPlayer
57372
57804
  });
57373
57805
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
57806
+ const runningSlice = useDeviceState(trpc, deviceId, (st) => st.runningState);
57374
57807
  if (cap === void 0) return null;
57375
- if (display === void 0) return null;
57808
+ const view = deriveMediaPlayerView(display, features, { running: runningSlice?.running });
57376
57809
  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;
57810
+ const status = display;
57811
+ const powered = status !== void 0 && view.power === "on";
57812
+ const action = toggleAction(status?.state ?? "off");
57813
+ const switchPower = () => {
57814
+ if (busy) return;
57815
+ if (view.power === "on") {
57816
+ applyOptimistic({ state: "off" });
57817
+ cap.turnOff({});
57818
+ } else {
57819
+ applyOptimistic({ state: "on" });
57820
+ cap.turnOn({});
57821
+ }
57822
+ };
57823
+ const powerLabel = powerLabelOf(view.power, t);
57383
57824
  const handleToggle = () => {
57384
57825
  if (busy) return;
57385
57826
  if (action === "play") {
@@ -57390,25 +57831,60 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57390
57831
  cap.pause({});
57391
57832
  }
57392
57833
  };
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);
57834
+ const title = status?.currentMedia?.title ?? null;
57835
+ const subtitle = status?.currentMedia?.artist ?? status?.currentMedia?.album ?? null;
57836
+ const hasNowPlaying = powered && (title !== null || subtitle !== null || view.artworkUrl !== null);
57837
+ const assumedAny = view.stateAssumed || view.volumeAssumed || view.muteAssumed || view.sourceAssumed;
57838
+ const sourceText = powered && view.source !== null && !view.source.selectable ? view.source.current : "";
57839
+ const volumeLevel = status?.volumeLevel ?? null;
57399
57840
  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",
57841
+ 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"),
57842
+ "data-testid": "media-hero",
57843
+ "data-power": view.power,
57401
57844
  children: [
57402
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateLine, {
57403
- state: display.state,
57404
- assumed: isAssumed,
57405
- source
57845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: MEDIA_HERO_KEYFRAMES }),
57846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57847
+ className: "flex flex-col items-center gap-2",
57848
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PowerOrb, {
57849
+ power: view.power,
57850
+ label: powerLabel,
57851
+ busy,
57852
+ onPress: view.canSwitchPower ? switchPower : void 0
57853
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateLine, {
57854
+ state: status?.state,
57855
+ assumed: assumedAny,
57856
+ source: sourceText,
57857
+ running: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningIndicator, { running: view.running })
57858
+ })]
57406
57859
  }),
57407
57860
  hasNowPlaying && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NowPlaying, {
57408
57861
  title,
57409
- subtitle
57862
+ subtitle,
57863
+ artworkUrl: view.artworkUrl
57864
+ }),
57865
+ powered && view.source !== null && view.source.selectable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57866
+ "data-testid": "media-source-select",
57867
+ className: "flex items-center gap-2",
57868
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57869
+ className: "text-xs text-foreground-subtle",
57870
+ children: t("uiLibrary.mediaPlayerHero.source", "Source")
57871
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
57872
+ "aria-label": t("uiLibrary.mediaPlayerHero.source", "Source"),
57873
+ value: view.source.current,
57874
+ disabled: busy,
57875
+ wrapperClassName: "relative min-w-0 flex-1",
57876
+ options: view.source.options.map((o) => ({
57877
+ value: o,
57878
+ label: o
57879
+ })),
57880
+ onChange: (e) => {
57881
+ if (busy) return;
57882
+ applyOptimistic({ source: e.target.value });
57883
+ cap.selectSource({ source: e.target.value });
57884
+ }
57885
+ })]
57410
57886
  }),
57411
- supportsSeek && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57887
+ powered && view.seek && status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57412
57888
  "data-testid": "media-seek",
57413
57889
  className: "flex flex-col gap-1",
57414
57890
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -57422,10 +57898,10 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57422
57898
  "aria-label": t("uiLibrary.mediaPlayerHero.seek", "Seek"),
57423
57899
  type: "range",
57424
57900
  min: 0,
57425
- max: display.duration ?? 0,
57901
+ max: status.duration ?? 0,
57426
57902
  step: 1e3,
57427
57903
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
57428
- value: display.position ?? 0,
57904
+ value: status.position ?? 0,
57429
57905
  disabled: busy,
57430
57906
  onChange: (e) => {
57431
57907
  if (busy) return;
@@ -57436,18 +57912,19 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57436
57912
  }),
57437
57913
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57438
57914
  className: "pointer-events-none",
57439
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: seekFraction })
57915
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: progressFraction(status.position, status.duration) })
57440
57916
  })
57441
57917
  ]
57442
57918
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57443
57919
  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) })]
57920
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(status.position) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatClock(status.duration) })]
57445
57921
  })]
57446
57922
  }),
57447
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57923
+ powered && view.transport.playPause && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57448
57924
  className: "flex items-center justify-center gap-4",
57925
+ "data-testid": "media-transport",
57449
57926
  children: [
57450
- supportsPrevious && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57927
+ view.transport.previous && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57451
57928
  label: t("uiLibrary.mediaPlayerHero.previous", "Previous"),
57452
57929
  busy,
57453
57930
  onPress: () => void cap.previous({}),
@@ -57469,7 +57946,16 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57469
57946
  "aria-hidden": "true"
57470
57947
  })
57471
57948
  }),
57472
- supportsNext && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57949
+ view.transport.stop && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57950
+ label: t("uiLibrary.mediaPlayerHero.stop", "Stop"),
57951
+ busy,
57952
+ onPress: () => void cap.stop({}),
57953
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, {
57954
+ className: "h-4 w-4",
57955
+ "aria-hidden": "true"
57956
+ })
57957
+ }),
57958
+ view.transport.next && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
57473
57959
  label: t("uiLibrary.mediaPlayerHero.next", "Next"),
57474
57960
  busy,
57475
57961
  onPress: () => void cap.next({}),
@@ -57480,24 +57966,24 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57480
57966
  })
57481
57967
  ]
57482
57968
  }),
57483
- supportsVolume && volumeLevel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57969
+ powered && view.volume === "slider" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57484
57970
  "data-testid": "media-volume",
57485
57971
  className: "flex items-center gap-2",
57486
57972
  children: [
57487
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57973
+ view.mute && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
57488
57974
  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"),
57975
+ "aria-label": status?.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57976
+ title: status?.isMuted === true ? t("uiLibrary.mediaPlayerHero.unmute", "Unmute") : t("uiLibrary.mediaPlayerHero.mute", "Mute"),
57491
57977
  disabled: busy,
57492
57978
  onClick: (e) => {
57493
57979
  e.stopPropagation();
57494
- if (busy || display.isMuted === null) return;
57495
- const muted = !display.isMuted;
57980
+ if (busy) return;
57981
+ const muted = status?.isMuted === null || status === void 0 ? true : !status.isMuted;
57496
57982
  applyOptimistic({ isMuted: muted });
57497
57983
  cap.setMute({ muted });
57498
57984
  },
57499
57985
  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, {
57986
+ children: status?.isMuted === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VolumeX, {
57501
57987
  className: "h-4 w-4",
57502
57988
  "aria-hidden": "true"
57503
57989
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Volume2, {
@@ -57519,7 +58005,7 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57519
58005
  max: 100,
57520
58006
  step: 1,
57521
58007
  className: "absolute inset-0 h-4 w-full cursor-pointer opacity-0",
57522
- value: volumeLevel,
58008
+ value: volumeLevel ?? 0,
57523
58009
  disabled: busy,
57524
58010
  onChange: (e) => {
57525
58011
  if (busy) return;
@@ -57530,19 +58016,92 @@ function MediaPlayerHeroCard({ trpc, deviceId, features, optimistic }) {
57530
58016
  }),
57531
58017
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57532
58018
  className: "pointer-events-none",
57533
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: volumeLevel / 100 })
58019
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58020
+ className: cn(volumeLevel === null && "opacity-40"),
58021
+ "data-testid": "media-volume-track",
58022
+ "data-known": volumeLevel !== null,
58023
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripTrack, { fraction: status?.isMuted === true || volumeLevel === null ? 0 : volumeLevel / 100 })
58024
+ })
57534
58025
  })
57535
58026
  ]
57536
58027
  }),
57537
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
58028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
57538
58029
  className: "w-9 text-right text-[11px] tabular-nums text-foreground-subtle",
57539
- children: [volumeLevel, "%"]
58030
+ children: volumeLevel === null ? "—" : `${volumeLevel}%`
57540
58031
  })
57541
58032
  ]
58033
+ }),
58034
+ powered && view.volume === "steps" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58035
+ "data-testid": "media-volume-steps",
58036
+ className: "flex items-center justify-center gap-3 text-xs text-foreground-subtle",
58037
+ children: [
58038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58039
+ label: t("uiLibrary.mediaPlayerHero.volumeDown", "Volume down"),
58040
+ busy,
58041
+ onPress: () => void cap.volumeDown({ steps: 1 }),
58042
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minus, {
58043
+ className: "h-4 w-4",
58044
+ "aria-hidden": "true"
58045
+ })
58046
+ }),
58047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Volume2, {
58048
+ className: "h-4 w-4",
58049
+ "aria-hidden": "true"
58050
+ }),
58051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58052
+ label: t("uiLibrary.mediaPlayerHero.volumeUp", "Volume up"),
58053
+ busy,
58054
+ onPress: () => void cap.volumeUp({ steps: 1 }),
58055
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, {
58056
+ className: "h-4 w-4",
58057
+ "aria-hidden": "true"
58058
+ })
58059
+ })
58060
+ ]
58061
+ }),
58062
+ powered && status !== void 0 && (view.shuffle || view.repeat) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58063
+ className: "flex items-center justify-center gap-2",
58064
+ "data-testid": "media-modes",
58065
+ children: [view.shuffle && status.shuffle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58066
+ label: t("uiLibrary.mediaPlayerHero.shuffle", "Shuffle"),
58067
+ busy,
58068
+ active: status.shuffle,
58069
+ onPress: () => {
58070
+ const shuffle = !status.shuffle;
58071
+ applyOptimistic({ shuffle });
58072
+ cap.setShuffle({ shuffle });
58073
+ },
58074
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Shuffle, {
58075
+ className: "h-4 w-4",
58076
+ "aria-hidden": "true"
58077
+ })
58078
+ }), view.repeat && status.repeat !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransportButton, {
58079
+ label: t("uiLibrary.mediaPlayerHero.repeat", "Repeat"),
58080
+ busy,
58081
+ active: status.repeat !== "off",
58082
+ onPress: () => {
58083
+ if (status.repeat === null) return;
58084
+ const repeat = NEXT_REPEAT[status.repeat];
58085
+ applyOptimistic({ repeat });
58086
+ cap.setRepeat({ repeat });
58087
+ },
58088
+ children: status.repeat === "one" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Repeat1, {
58089
+ className: "h-4 w-4",
58090
+ "aria-hidden": "true"
58091
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Repeat, {
58092
+ className: "h-4 w-4",
58093
+ "aria-hidden": "true"
58094
+ })
58095
+ })]
57542
58096
  })
57543
58097
  ]
57544
58098
  });
57545
58099
  }
58100
+ function powerLabelOf(power, t) {
58101
+ if (power === "on") return t("uiLibrary.mediaPlayerHero.turnOff", "Turn off");
58102
+ if (power === "unknown") return t("uiLibrary.mediaPlayerHero.powerUnknown", "Power state unknown");
58103
+ return t("uiLibrary.mediaPlayerHero.turnOn", "Turn on");
58104
+ }
57546
58105
  //#endregion
57547
58106
  //#region src/composites/device-controls/alarm-hero-card.tsx
57548
58107
  var ALARM_STATE_CONFIG = {
@@ -73116,7 +73675,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
73116
73675
  }
73117
73676
  //#endregion
73118
73677
  //#region src/ui-library-version.ts
73119
- var uiLibraryVersion = "1.2.376";
73678
+ var uiLibraryVersion = "1.2.378";
73120
73679
  //#endregion
73121
73680
  //#region src/hooks/use-account-unit-preferences.ts
73122
73681
  /**
@@ -74481,6 +75040,7 @@ exports.PopoverContent = PopoverContent;
74481
75040
  exports.PopoverRowAction = PopoverRowAction;
74482
75041
  exports.PopoverTrigger = PopoverTrigger;
74483
75042
  exports.Power = Power;
75043
+ exports.PowerMeterLine = PowerMeterLine;
74484
75044
  exports.PowerOff = PowerOff;
74485
75045
  exports.PrimaryChildPicker = PrimaryChildPicker;
74486
75046
  exports.PrivacyMaskSettings = PrivacyMaskSettings;
@@ -74507,6 +75067,8 @@ exports.RecordEntriesPanel = RecordEntriesPanel;
74507
75067
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
74508
75068
  exports.RecordingPanel = RecordingPanel;
74509
75069
  exports.RefreshCw = RefreshCw;
75070
+ exports.Repeat = Repeat;
75071
+ exports.Repeat1 = Repeat1;
74510
75072
  exports.ResponseLog = ResponseLog;
74511
75073
  exports.RetrainReviewBadge = RetrainReviewBadge;
74512
75074
  exports.RoleDisplayDefaultsFromServer = RoleDisplayDefaultsFromServer;
@@ -74590,6 +75152,7 @@ exports.ShieldAlert = ShieldAlert;
74590
75152
  exports.ShieldCheck = ShieldCheck;
74591
75153
  exports.ShieldOff = ShieldOff;
74592
75154
  exports.Ship = Ship;
75155
+ exports.Shuffle = Shuffle;
74593
75156
  exports.Sidebar = Sidebar;
74594
75157
  exports.SidebarItem = SidebarItem;
74595
75158
  exports.Siren = Siren;
@@ -75273,6 +75836,7 @@ exports.useCoreBlocksList = useCoreBlocksList;
75273
75836
  exports.useCoreBlocksListComposableCaps = useCoreBlocksListComposableCaps;
75274
75837
  exports.useCoreBlocksListCompositions = useCoreBlocksListCompositions;
75275
75838
  exports.useCoreBlocksMigrateRawSecrets = useCoreBlocksMigrateRawSecrets;
75839
+ exports.useCoreBlocksProposeRunningThreshold = useCoreBlocksProposeRunningThreshold;
75276
75840
  exports.useCoreBlocksReportAutomationRun = useCoreBlocksReportAutomationRun;
75277
75841
  exports.useCoreBlocksRestart = useCoreBlocksRestart;
75278
75842
  exports.useCoreBlocksSetEnabled = useCoreBlocksSetEnabled;