@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/CHANGELOG.md +10 -0
- package/dist/{MotionZonesSettings-Zcfa5c9l.js → MotionZonesSettings-BvpjmW6y.js} +1 -1
- package/dist/{PrivacyMaskSettings-B_X42yzN.js → PrivacyMaskSettings-D7o4tx8e.js} +1 -1
- package/dist/{SceneMonitorEditor-C8YXVzYI.js → SceneMonitorEditor-BwelmHlF.js} +1 -1
- package/dist/composites/cap-settings/PowerMeterLine.d.ts +10 -0
- package/dist/composites/cap-settings/index.d.ts +1 -0
- package/dist/composites/device-controls/appliance-control.d.ts +0 -2
- package/dist/composites/device-controls/media-player-hero-card.d.ts +15 -10
- package/dist/composites/device-controls/media-player-hero-parts.d.ts +18 -0
- package/dist/composites/device-controls/media-player-panel.d.ts +1 -1
- package/dist/composites/device-controls/media-player-view.d.ts +66 -0
- package/dist/composites/index.d.ts +1 -0
- package/dist/{icons-DXwPEKdJ.cjs → icons-BWpVdTJu.cjs} +3 -83
- package/dist/{icons-e-xohmFb.js → icons-cIFSolkx.js} +1 -81
- package/dist/index.cjs +721 -160
- package/dist/index.js +723 -166
- package/package.json +1 -1
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-
|
|
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-
|
|
37079
|
-
if (signalPercent <= 50) return "text-
|
|
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
|
|
37091
|
-
const
|
|
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(
|
|
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-
|
|
48792
|
-
paused: "border-
|
|
48793
|
-
buffering: "border-
|
|
48794
|
-
on: "border-
|
|
48795
|
-
idle: "border-
|
|
48796
|
-
off: "border-
|
|
48797
|
-
standby: "border-
|
|
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-
|
|
48802
|
-
paused: "bg-
|
|
48803
|
-
buffering: "bg-
|
|
48804
|
-
on: "bg-
|
|
48805
|
-
idle: "bg-
|
|
48806
|
-
off: "bg-
|
|
48807
|
-
standby: "bg-
|
|
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
|
|
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.
|
|
49232
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
48914
49233
|
className: "tabular-nums w-8 text-right",
|
|
48915
|
-
children:
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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,
|
|
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
|
-
|
|
55722
|
-
|
|
55723
|
-
|
|
55724
|
-
|
|
55725
|
-
|
|
55726
|
-
|
|
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)
|
|
56180
|
-
*
|
|
56181
|
-
*
|
|
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
|
|
56486
|
+
* drum icon.
|
|
56184
56487
|
* - ApplianceHeroCard (device-detail hero): the drum (turning only while a
|
|
56185
|
-
* cycle runs), the state, the phase
|
|
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
|
|
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
|
|
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
|
-
|
|
56263
|
-
|
|
56264
|
-
|
|
56265
|
-
|
|
56266
|
-
|
|
56267
|
-
|
|
56268
|
-
|
|
56269
|
-
|
|
56270
|
-
|
|
56271
|
-
|
|
56272
|
-
|
|
56273
|
-
phase
|
|
56274
|
-
|
|
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
|
|
56293
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
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:
|
|
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
|
-
})
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
*
|
|
57355
|
-
*
|
|
57356
|
-
*
|
|
57357
|
-
*
|
|
57358
|
-
*
|
|
57359
|
-
*
|
|
57360
|
-
*
|
|
57361
|
-
*
|
|
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
|
-
|
|
57806
|
+
const view = deriveMediaPlayerView(display, features, { running: runningSlice?.running });
|
|
57376
57807
|
const busy = !available;
|
|
57377
|
-
const
|
|
57378
|
-
const
|
|
57379
|
-
const
|
|
57380
|
-
const
|
|
57381
|
-
|
|
57382
|
-
|
|
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 =
|
|
57394
|
-
const subtitle =
|
|
57395
|
-
const hasNowPlaying = title !== null || subtitle !== null;
|
|
57396
|
-
const
|
|
57397
|
-
const
|
|
57398
|
-
const
|
|
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
|
|
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)(
|
|
57403
|
-
|
|
57404
|
-
|
|
57405
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
57490
|
-
title:
|
|
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
|
|
57495
|
-
const muted = !
|
|
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:
|
|
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)(
|
|
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.
|
|
58026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
57538
58027
|
className: "w-9 text-right text-[11px] tabular-nums text-foreground-subtle",
|
|
57539
|
-
children:
|
|
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.
|
|
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;
|