@gentomiyano/optimized-web-audio-player 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +159 -0
  2. package/dist/config/index.d.ts +37 -0
  3. package/dist/config/index.js +280 -0
  4. package/dist/config/index.js.map +1 -0
  5. package/dist/consumer/index.d.ts +27 -0
  6. package/dist/consumer/index.js +38 -0
  7. package/dist/consumer/index.js.map +1 -0
  8. package/dist/consumer-smoke/node_modules/.modules.yaml +30 -0
  9. package/dist/consumer-smoke/node_modules/.package-map.json +1 -0
  10. package/dist/consumer-smoke/node_modules/.pnpm/lock.yaml +57 -0
  11. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/LICENSE +21 -0
  12. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/README.md +9 -0
  13. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.development.js +414 -0
  14. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.production.js +406 -0
  15. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.development.js +150 -0
  16. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.production.js +140 -0
  17. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.development.js +364 -0
  18. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.development.js +350 -0
  19. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.production.js +330 -0
  20. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.production.js +340 -0
  21. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.js +7 -0
  22. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.native.js +7 -0
  23. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/package.json +27 -0
  24. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_mock.js +7 -0
  25. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_post_task.js +7 -0
  26. package/dist/consumer-smoke/node_modules/.pnpm-workspace-state-v1.json +29 -0
  27. package/dist/contracts/index.d.ts +15 -0
  28. package/dist/contracts/index.js +3 -0
  29. package/dist/contracts/index.js.map +1 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +368 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/media-delivery/index.d.ts +185 -0
  34. package/dist/media-delivery/index.js +91 -0
  35. package/dist/media-delivery/index.js.map +1 -0
  36. package/dist/media-element-C9dis4aG.d.ts +40 -0
  37. package/dist/player-DE9Vqztu.d.ts +212 -0
  38. package/dist/ports/index.d.ts +32 -0
  39. package/dist/ports/index.js +3 -0
  40. package/dist/ports/index.js.map +1 -0
  41. package/dist/react/index.d.ts +86 -0
  42. package/dist/react/index.js +2393 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/testing/index.d.ts +36 -0
  45. package/dist/testing/index.js +365 -0
  46. package/dist/testing/index.js.map +1 -0
  47. package/dist/ui/index.d.ts +143 -0
  48. package/dist/ui/index.js +1344 -0
  49. package/dist/ui/index.js.map +1 -0
  50. package/dist/ui/player-tokens.css +572 -0
  51. package/dist/ui/player-views.css +654 -0
  52. package/dist/ui/styles.css +2 -0
  53. package/package.json +97 -0
@@ -0,0 +1,1344 @@
1
+ import { useState, useRef, useId, useEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/ui/player-primitives.tsx
5
+ function IconFrame({
6
+ children,
7
+ size = 20,
8
+ ...props
9
+ }) {
10
+ return /* @__PURE__ */ jsx(
11
+ "svg",
12
+ {
13
+ "aria-hidden": "true",
14
+ focusable: "false",
15
+ viewBox: "0 0 24 24",
16
+ width: size,
17
+ height: size,
18
+ fill: "currentColor",
19
+ ...props,
20
+ children
21
+ }
22
+ );
23
+ }
24
+ function PlayIcon(props) {
25
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M7 4.8c0-1.05 1.16-1.68 2.04-1.1l11 7.2a1.3 1.3 0 0 1 0 2.2l-11 7.2A1.31 1.31 0 0 1 7 19.2V4.8Z" }) });
26
+ }
27
+ function PauseIcon(props) {
28
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
29
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "4", width: "5", height: "16", rx: "0.8" }),
30
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "4", width: "5", height: "16", rx: "0.8" })
31
+ ] });
32
+ }
33
+ function PreviousIcon(props) {
34
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
35
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "5", width: "3", height: "14", rx: "0.6" }),
36
+ /* @__PURE__ */ jsx("path", { d: "M19 5.3v13.4c0 .93-1.02 1.5-1.82 1.02l-9.7-6.7a1.2 1.2 0 0 1 0-2.04l9.7-6.7A1.19 1.19 0 0 1 19 5.3Z" })
37
+ ] });
38
+ }
39
+ function NextIcon(props) {
40
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
41
+ /* @__PURE__ */ jsx("rect", { x: "17", y: "5", width: "3", height: "14", rx: "0.6" }),
42
+ /* @__PURE__ */ jsx("path", { d: "M5 5.3v13.4c0 .93 1.02 1.5 1.82 1.02l9.7-6.7a1.2 1.2 0 0 0 0-2.04l-9.7-6.7A1.19 1.19 0 0 0 5 5.3Z" })
43
+ ] });
44
+ }
45
+ function ShuffleIcon(props) {
46
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M17.2 3.7 22 7.5l-4.8 3.8V8.8h-1.9c-1.5 0-2.4.5-3.2 1.6l-3.3 4.5c-1.35 1.84-3.1 2.8-5.8 2.8H2v-3h1c1.7 0 2.55-.43 3.36-1.53l3.32-4.52C11.02 6.82 12.8 5.8 15.3 5.8h1.9V3.7ZM2 6.3h1c2.7 0 4.43.94 5.8 2.8l1.05 1.43-1.87 2.55-1.62-2.25C5.55 9.73 4.7 9.3 3 9.3H2v-3Zm12.08 5.17c.35-.13.75-.2 1.22-.2h1.9V8.7l4.8 3.8-4.8 3.8v-2.03h-1.9c-1.24 0-2.05.34-2.73 1.04l1.51-3.84Z" }) });
47
+ }
48
+ function RepeatIcon(props) {
49
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M7.3 4h9.2A4.5 4.5 0 0 1 21 8.5V10h-3V8.5A1.5 1.5 0 0 0 16.5 7H7.3v3.1L2 5.55 7.3 1v3Zm9.4 16H7.5A4.5 4.5 0 0 1 3 15.5V14h3v1.5A1.5 1.5 0 0 0 7.5 17h9.2v-3.1l5.3 4.55L16.7 23v-3Z" }) });
50
+ }
51
+ function RepeatOneIcon(props) {
52
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
53
+ /* @__PURE__ */ jsx("path", { d: "M7.3 3h9.2A4.5 4.5 0 0 1 21 7.5V9h-3V7.5A1.5 1.5 0 0 0 16.5 6H7.3v2.6L2 4.55 7.3.5V3Zm9.4 18H7.5A4.5 4.5 0 0 1 3 16.5V15h3v1.5A1.5 1.5 0 0 0 7.5 18h9.2v-2.6l5.3 4.05-5.3 4.05V21Z" }),
54
+ /* @__PURE__ */ jsx("path", { d: "M10.25 9h3.35v7h-2.35v-4.8h-1V9Z" })
55
+ ] });
56
+ }
57
+ function QueueIcon(props) {
58
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
59
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "6", r: "2" }),
60
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "12", r: "2" }),
61
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "18", r: "2" }),
62
+ /* @__PURE__ */ jsx("rect", { x: "8", y: "4.5", width: "14", height: "3", rx: "0.7" }),
63
+ /* @__PURE__ */ jsx("rect", { x: "8", y: "10.5", width: "14", height: "3", rx: "0.7" }),
64
+ /* @__PURE__ */ jsx("rect", { x: "8", y: "16.5", width: "14", height: "3", rx: "0.7" })
65
+ ] });
66
+ }
67
+ function VolumeIcon(props) {
68
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M3 9h4l5-4.4c.78-.68 2-.13 2 .9v13c0 1.03-1.22 1.58-2 .9L7 15H3V9Zm14.3-1.8a1.5 1.5 0 0 1 2.1.25A7.4 7.4 0 0 1 21 12a7.4 7.4 0 0 1-1.6 4.55 1.5 1.5 0 1 1-2.35-1.86A4.4 4.4 0 0 0 18 12c0-1-.33-1.93-.95-2.69a1.5 1.5 0 0 1 .25-2.11Z" }) });
69
+ }
70
+ function MuteIcon(props) {
71
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M2 9h4l5-4.4c.78-.68 2-.13 2 .9v13c0 1.03-1.22 1.58-2 .9L6 15H2V9Zm13.9-.9L18 10.2l2.1-2.1L22 10l-2.1 2.1L22 14l-1.9 1.9-2.1-2.1-2.1 2.1L14 14l2.1-1.9L14 10l1.9-1.9Z" }) });
72
+ }
73
+ function PodcastSkipBackIcon(props) {
74
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
75
+ /* @__PURE__ */ jsx("path", { d: "M8.2 3.2 3 7.4l5.2 4.2V9a7 7 0 1 1-1.55 8.5l-2.73 1.25A10 10 0 1 0 8.2 6V3.2Z" }),
76
+ /* @__PURE__ */ jsx("rect", { x: "8.5", y: "10", width: "2.2", height: "6", rx: "0.4" }),
77
+ /* @__PURE__ */ jsx("rect", { x: "12.2", y: "10", width: "3.3", height: "2", rx: "0.4" }),
78
+ /* @__PURE__ */ jsx("rect", { x: "13.3", y: "11", width: "2.2", height: "5", rx: "0.4" }),
79
+ /* @__PURE__ */ jsx("rect", { x: "12.2", y: "14", width: "3.3", height: "2", rx: "0.4" })
80
+ ] });
81
+ }
82
+ function PodcastSkipForwardIcon(props) {
83
+ return /* @__PURE__ */ jsxs(IconFrame, { ...props, children: [
84
+ /* @__PURE__ */ jsx("path", { d: "m15.8 3.2 5.2 4.2-5.2 4.2V9a7 7 0 1 0 1.55 8.5l2.73 1.25A10 10 0 1 1 15.8 6V3.2Z" }),
85
+ /* @__PURE__ */ jsx("path", { d: "M7.5 10h3.2v2H9.5v1h1.2v3H7.5v-2h1.2v-2H7.5v-2Zm4.3 0H15v6h-3.2v-6Zm2 2h-.8v2h.8v-2Z" })
86
+ ] });
87
+ }
88
+ function CloseIcon(props) {
89
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "m5.05 3 6.95 6.95L18.95 3 21 5.05 14.05 12 21 18.95 18.95 21 12 14.05 5.05 21 3 18.95 9.95 12 3 5.05 5.05 3Z" }) });
90
+ }
91
+ function RetryIcon(props) {
92
+ return /* @__PURE__ */ jsx(IconFrame, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M19.7 4.3V1L15.5 5.2 19.7 9V7.3A8 8 0 1 1 12 4a1.5 1.5 0 0 0 0-3 11 11 0 1 0 10.7 8.5l-2.88.84A8 8 0 0 1 19.7 4.3Z" }) });
93
+ }
94
+
95
+ // src/ui/player-utils.ts
96
+ function clamp(value, min, max) {
97
+ return Math.min(Math.max(value, min), max);
98
+ }
99
+ function formatTime(seconds) {
100
+ if (seconds === null || !Number.isFinite(seconds) || seconds < 0) {
101
+ return "--:--";
102
+ }
103
+ const wholeSeconds = Math.floor(seconds);
104
+ const hours = Math.floor(wholeSeconds / 3600);
105
+ const minutes = Math.floor(wholeSeconds % 3600 / 60);
106
+ const remainder = wholeSeconds % 60;
107
+ if (hours > 0) {
108
+ return `${String(hours)}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
109
+ }
110
+ return `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
111
+ }
112
+ function formatTimeAria(seconds) {
113
+ const safeSeconds = Math.max(0, Math.floor(seconds));
114
+ const hours = Math.floor(safeSeconds / 3600);
115
+ const minutes = Math.floor(safeSeconds % 3600 / 60);
116
+ const remainder = safeSeconds % 60;
117
+ const parts = [
118
+ hours > 0 ? `${String(hours)} hour${hours === 1 ? "" : "s"}` : "",
119
+ minutes > 0 ? `${String(minutes)} minute${minutes === 1 ? "" : "s"}` : "",
120
+ `${String(remainder)} second${remainder === 1 ? "" : "s"}`
121
+ ].filter(Boolean);
122
+ return parts.join(" ");
123
+ }
124
+ function getRepeatLabel(mode) {
125
+ switch (mode) {
126
+ case "off":
127
+ return "Repeat off";
128
+ case "all":
129
+ return "Repeat all";
130
+ case "one":
131
+ return "Repeat one";
132
+ }
133
+ }
134
+ function getNextRepeatMode(mode) {
135
+ switch (mode) {
136
+ case "off":
137
+ return "all";
138
+ case "all":
139
+ return "one";
140
+ case "one":
141
+ return "off";
142
+ }
143
+ }
144
+ function getStatusLabel(state) {
145
+ if (state.needsUserGesture) {
146
+ return "Ready to play";
147
+ }
148
+ switch (state.status) {
149
+ case "idle":
150
+ return "Idle";
151
+ case "loading":
152
+ return "Loading";
153
+ case "playing":
154
+ return "Playing";
155
+ case "paused":
156
+ return "Paused";
157
+ case "seeking":
158
+ return "Seeking";
159
+ case "ended":
160
+ return "Ended";
161
+ case "error":
162
+ return "Playback unavailable";
163
+ }
164
+ }
165
+ function getErrorMessage(error) {
166
+ switch (error.code) {
167
+ case "autoplay-blocked":
168
+ return "Playback needs a direct action.";
169
+ case "network":
170
+ return "The media could not be reached.";
171
+ case "cors":
172
+ return "The media source is not available.";
173
+ case "decode":
174
+ return "This media could not be decoded.";
175
+ case "unsupported":
176
+ return "This media format is not supported.";
177
+ case "audio-context":
178
+ return "Audio output is not available.";
179
+ case "unknown":
180
+ return "Playback is currently unavailable.";
181
+ }
182
+ }
183
+ function PlayerSurface({
184
+ appearance,
185
+ children,
186
+ className,
187
+ label
188
+ }) {
189
+ return /* @__PURE__ */ jsx(
190
+ "section",
191
+ {
192
+ "aria-label": label,
193
+ className: ["player-surface", className].filter(Boolean).join(" "),
194
+ "data-appearance": appearance,
195
+ children
196
+ }
197
+ );
198
+ }
199
+ function Artwork({ className, item }) {
200
+ const classes = ["player-artwork", className].filter(Boolean).join(" ");
201
+ if (item?.artwork !== void 0) {
202
+ return /* @__PURE__ */ jsx("span", { className: `${classes} player-artwork--image`, children: /* @__PURE__ */ jsx(
203
+ "img",
204
+ {
205
+ alt: item.artwork.alt,
206
+ className: "player-artwork__image",
207
+ decoding: "async",
208
+ src: item.artwork.url
209
+ }
210
+ ) });
211
+ }
212
+ return /* @__PURE__ */ jsx(
213
+ "div",
214
+ {
215
+ "aria-label": "Artwork unavailable",
216
+ className: `${classes} player-artwork--fallback`,
217
+ role: "img",
218
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true" })
219
+ }
220
+ );
221
+ }
222
+ function ScrollingTitle({
223
+ enabled,
224
+ title
225
+ }) {
226
+ const containsJapanese = /[\u3040-\u30ff\u3400-\u9fff]/u.test(title);
227
+ const viewportRef = useRef(null);
228
+ const contentRef = useRef(null);
229
+ const [marquee, setMarquee] = useState({
230
+ distance: 0,
231
+ duration: 10,
232
+ overflow: false
233
+ });
234
+ useEffect(() => {
235
+ const viewport = viewportRef.current;
236
+ const content = contentRef.current;
237
+ if (viewport === null || content === null) {
238
+ return;
239
+ }
240
+ const viewportElement = viewport;
241
+ const contentElement = content;
242
+ function measure() {
243
+ const viewportWidth = Math.max(
244
+ viewportElement.getBoundingClientRect().width,
245
+ viewportElement.clientWidth
246
+ );
247
+ const contentWidth = Math.max(
248
+ contentElement.getBoundingClientRect().width,
249
+ contentElement.scrollWidth
250
+ );
251
+ const overflow = contentWidth - viewportWidth > 1;
252
+ const distance = enabled ? contentWidth + 12 : 0;
253
+ setMarquee({
254
+ distance,
255
+ duration: Math.max(10, distance / 22 + 4),
256
+ overflow
257
+ });
258
+ }
259
+ measure();
260
+ if (typeof ResizeObserver === "undefined") {
261
+ window.addEventListener("resize", measure);
262
+ return () => {
263
+ window.removeEventListener("resize", measure);
264
+ };
265
+ }
266
+ const observer = new ResizeObserver(measure);
267
+ observer.observe(viewportElement);
268
+ observer.observe(contentElement);
269
+ return () => {
270
+ observer.disconnect();
271
+ };
272
+ }, [enabled, title]);
273
+ const style = {
274
+ "--player-title-scroll-offset": `${String(-marquee.distance)}px`,
275
+ "--player-title-scroll-duration": `${String(marquee.duration)}s`
276
+ };
277
+ return /* @__PURE__ */ jsx(
278
+ "strong",
279
+ {
280
+ className: "player-title",
281
+ "data-overflow": marquee.overflow ? "true" : "false",
282
+ "data-scroll": enabled ? "true" : "false",
283
+ "data-script": containsJapanese ? "japanese" : "latin",
284
+ ref: viewportRef,
285
+ style,
286
+ children: /* @__PURE__ */ jsx("span", { className: "player-title__text", ref: contentRef, children: title })
287
+ }
288
+ );
289
+ }
290
+ function TrackMetadata({
291
+ className,
292
+ item,
293
+ scrollTitle = false,
294
+ status
295
+ }) {
296
+ return /* @__PURE__ */ jsxs(
297
+ "div",
298
+ {
299
+ className: ["player-metadata", className].filter(Boolean).join(" "),
300
+ children: [
301
+ status === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-kicker", children: status }),
302
+ /* @__PURE__ */ jsx(
303
+ ScrollingTitle,
304
+ {
305
+ enabled: item !== null && scrollTitle,
306
+ title: item?.title ?? "No media selected"
307
+ },
308
+ item?.title ?? "No media selected"
309
+ ),
310
+ /* @__PURE__ */ jsx("span", { className: "player-creator", children: item === null ? "Select media to begin" : item.creatorName ?? "Artist unavailable" }),
311
+ item?.collectionTitle === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "player-collection", children: item.collectionTitle })
312
+ ]
313
+ }
314
+ );
315
+ }
316
+ function TimeReadout({
317
+ className,
318
+ currentTimeSec,
319
+ durationSec
320
+ }) {
321
+ return /* @__PURE__ */ jsxs(
322
+ "div",
323
+ {
324
+ "aria-label": `${formatTime(currentTimeSec)} of ${formatTime(durationSec)}`,
325
+ className: ["player-time", className].filter(Boolean).join(" "),
326
+ children: [
327
+ /* @__PURE__ */ jsx("time", { children: formatTime(currentTimeSec) }),
328
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "/" }),
329
+ /* @__PURE__ */ jsx("time", { children: formatTime(durationSec) })
330
+ ]
331
+ }
332
+ );
333
+ }
334
+ function IconButton({
335
+ children,
336
+ className,
337
+ disabled = false,
338
+ label,
339
+ onClick,
340
+ pressed
341
+ }) {
342
+ return /* @__PURE__ */ jsx(
343
+ "button",
344
+ {
345
+ "aria-label": label,
346
+ ...pressed === void 0 ? {} : { "aria-pressed": pressed },
347
+ className: ["player-icon-button", className].filter(Boolean).join(" "),
348
+ disabled,
349
+ onClick,
350
+ title: label,
351
+ type: "button",
352
+ children
353
+ }
354
+ );
355
+ }
356
+ function PrimaryPlaybackButton({
357
+ className,
358
+ commands,
359
+ state
360
+ }) {
361
+ const isPlaying = state.status === "playing";
362
+ const label = isPlaying ? "Pause" : "Play";
363
+ return /* @__PURE__ */ jsx(
364
+ IconButton,
365
+ {
366
+ className: [
367
+ "player-play-button",
368
+ isPlaying ? "player-play-button--active" : "",
369
+ className
370
+ ].filter(Boolean).join(" "),
371
+ disabled: state.currentItem === null || state.status === "loading",
372
+ label,
373
+ onClick: () => {
374
+ void commands.togglePlayback();
375
+ },
376
+ children: isPlaying ? /* @__PURE__ */ jsx(PauseIcon, { size: 22 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 22 })
377
+ }
378
+ );
379
+ }
380
+ function TransportControls({
381
+ className,
382
+ commands,
383
+ compact = false,
384
+ showModes = true,
385
+ state
386
+ }) {
387
+ const RepeatModeIcon = state.repeatMode === "one" ? RepeatOneIcon : RepeatIcon;
388
+ return /* @__PURE__ */ jsxs(
389
+ "div",
390
+ {
391
+ "aria-label": "Playback controls",
392
+ className: [
393
+ "player-transport",
394
+ compact ? "player-transport--compact" : "",
395
+ className
396
+ ].filter(Boolean).join(" "),
397
+ role: "group",
398
+ children: [
399
+ showModes ? /* @__PURE__ */ jsx(
400
+ IconButton,
401
+ {
402
+ label: state.shuffleEnabled ? "Shuffle on" : "Shuffle off",
403
+ onClick: () => {
404
+ commands.setShuffle(!state.shuffleEnabled);
405
+ },
406
+ pressed: state.shuffleEnabled,
407
+ children: /* @__PURE__ */ jsx(ShuffleIcon, {})
408
+ }
409
+ ) : null,
410
+ /* @__PURE__ */ jsx(
411
+ IconButton,
412
+ {
413
+ disabled: !state.canGoPrevious,
414
+ label: "Previous",
415
+ onClick: () => {
416
+ void commands.previous();
417
+ },
418
+ children: /* @__PURE__ */ jsx(PreviousIcon, {})
419
+ }
420
+ ),
421
+ /* @__PURE__ */ jsx(PrimaryPlaybackButton, { commands, state }),
422
+ /* @__PURE__ */ jsx(
423
+ IconButton,
424
+ {
425
+ disabled: !state.canGoNext,
426
+ label: "Next",
427
+ onClick: () => {
428
+ void commands.next();
429
+ },
430
+ children: /* @__PURE__ */ jsx(NextIcon, {})
431
+ }
432
+ ),
433
+ showModes ? /* @__PURE__ */ jsx(
434
+ IconButton,
435
+ {
436
+ label: getRepeatLabel(state.repeatMode),
437
+ onClick: () => {
438
+ commands.setRepeatMode(getNextRepeatMode(state.repeatMode));
439
+ },
440
+ pressed: state.repeatMode !== "off",
441
+ children: /* @__PURE__ */ jsx(RepeatModeIcon, {})
442
+ }
443
+ ) : null
444
+ ]
445
+ }
446
+ );
447
+ }
448
+ function VolumeControl({
449
+ className,
450
+ commands,
451
+ state
452
+ }) {
453
+ const isMuted = state.muted || state.volume === 0;
454
+ return /* @__PURE__ */ jsxs(
455
+ "div",
456
+ {
457
+ className: ["player-volume", className].filter(Boolean).join(" "),
458
+ children: [
459
+ /* @__PURE__ */ jsx(
460
+ IconButton,
461
+ {
462
+ label: isMuted ? "Unmute" : "Mute",
463
+ onClick: () => {
464
+ commands.toggleMute();
465
+ },
466
+ pressed: state.muted,
467
+ children: isMuted ? /* @__PURE__ */ jsx(MuteIcon, {}) : /* @__PURE__ */ jsx(VolumeIcon, {})
468
+ }
469
+ ),
470
+ /* @__PURE__ */ jsxs("label", { children: [
471
+ /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: "Volume" }),
472
+ /* @__PURE__ */ jsx(
473
+ "input",
474
+ {
475
+ "aria-label": "Volume",
476
+ max: 1,
477
+ min: 0,
478
+ onChange: (event) => {
479
+ commands.setVolume(Number(event.currentTarget.value));
480
+ },
481
+ step: 0.01,
482
+ type: "range",
483
+ value: state.volume
484
+ }
485
+ )
486
+ ] })
487
+ ]
488
+ }
489
+ );
490
+ }
491
+ function PopoverVolumeControl({
492
+ className,
493
+ commands,
494
+ label,
495
+ state
496
+ }) {
497
+ const [open, setOpen] = useState(false);
498
+ const buttonRef = useRef(null);
499
+ const rootRef = useRef(null);
500
+ const sliderRef = useRef(null);
501
+ const panelId = useId();
502
+ const isMuted = state.muted || state.volume === 0;
503
+ useEffect(() => {
504
+ if (!open) {
505
+ return;
506
+ }
507
+ sliderRef.current?.focus();
508
+ const closeOnOutsidePointer = (event) => {
509
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) {
510
+ setOpen(false);
511
+ }
512
+ };
513
+ document.addEventListener("pointerdown", closeOnOutsidePointer);
514
+ return () => {
515
+ document.removeEventListener("pointerdown", closeOnOutsidePointer);
516
+ };
517
+ }, [open]);
518
+ function closeWhenFocusLeaves(event) {
519
+ if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) {
520
+ return;
521
+ }
522
+ setOpen(false);
523
+ }
524
+ return /* @__PURE__ */ jsxs(
525
+ "div",
526
+ {
527
+ className: ["player-volume-popover", className].filter(Boolean).join(" "),
528
+ onBlur: closeWhenFocusLeaves,
529
+ onKeyDown: (event) => {
530
+ if (event.key === "Escape") {
531
+ setOpen(false);
532
+ buttonRef.current?.focus();
533
+ }
534
+ },
535
+ ref: rootRef,
536
+ children: [
537
+ /* @__PURE__ */ jsx(
538
+ "button",
539
+ {
540
+ "aria-controls": panelId,
541
+ "aria-expanded": open,
542
+ "aria-label": `${open ? "Close" : "Open"} ${label}`,
543
+ className: "player-icon-button",
544
+ onClick: () => {
545
+ setOpen((current) => !current);
546
+ },
547
+ ref: buttonRef,
548
+ title: `${open ? "Close" : "Open"} ${label}`,
549
+ type: "button",
550
+ children: isMuted ? /* @__PURE__ */ jsx(MuteIcon, {}) : /* @__PURE__ */ jsx(VolumeIcon, {})
551
+ }
552
+ ),
553
+ open ? /* @__PURE__ */ jsxs(
554
+ "div",
555
+ {
556
+ "aria-label": label,
557
+ className: "player-volume-popover__panel",
558
+ id: panelId,
559
+ role: "group",
560
+ children: [
561
+ /* @__PURE__ */ jsxs("label", { children: [
562
+ /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: label }),
563
+ /* @__PURE__ */ jsx(
564
+ "input",
565
+ {
566
+ "aria-label": label,
567
+ max: 1,
568
+ min: 0,
569
+ onChange: (event) => {
570
+ commands.setVolume(Number(event.currentTarget.value));
571
+ },
572
+ ref: sliderRef,
573
+ step: 0.01,
574
+ type: "range",
575
+ value: state.volume
576
+ }
577
+ )
578
+ ] }),
579
+ /* @__PURE__ */ jsx("output", { children: Math.round(state.volume * 100) })
580
+ ]
581
+ }
582
+ ) : null
583
+ ]
584
+ }
585
+ );
586
+ }
587
+ function QueueToggle({
588
+ expanded = false,
589
+ onClick
590
+ }) {
591
+ return /* @__PURE__ */ jsx(
592
+ IconButton,
593
+ {
594
+ label: expanded ? "Close queue" : "Open queue",
595
+ onClick,
596
+ pressed: expanded,
597
+ children: /* @__PURE__ */ jsx(QueueIcon, {})
598
+ }
599
+ );
600
+ }
601
+ function PlayerStatus({ state }) {
602
+ return /* @__PURE__ */ jsx("span", { className: "player-status", "data-status": state.status, children: getStatusLabel(state) });
603
+ }
604
+ function PlayerErrorNotice({
605
+ commands,
606
+ state
607
+ }) {
608
+ if (state.error === null) {
609
+ return null;
610
+ }
611
+ return /* @__PURE__ */ jsxs("div", { className: "player-error", role: "alert", children: [
612
+ /* @__PURE__ */ jsx("span", { children: getErrorMessage(state.error) }),
613
+ state.error.recoverable ? /* @__PURE__ */ jsxs(
614
+ "button",
615
+ {
616
+ onClick: () => {
617
+ void commands.retry();
618
+ },
619
+ type: "button",
620
+ children: [
621
+ /* @__PURE__ */ jsx(RetryIcon, { size: 16 }),
622
+ "Retry"
623
+ ]
624
+ }
625
+ ) : null
626
+ ] });
627
+ }
628
+ var FALLBACK_PEAKS = [
629
+ 0.16,
630
+ 0.24,
631
+ 0.42,
632
+ 0.3,
633
+ 0.58,
634
+ 0.76,
635
+ 0.48,
636
+ 0.34,
637
+ 0.66,
638
+ 0.88,
639
+ 0.62,
640
+ 0.4,
641
+ 0.52,
642
+ 0.72,
643
+ 0.44,
644
+ 0.28,
645
+ 0.56,
646
+ 0.82,
647
+ 0.68,
648
+ 0.38,
649
+ 0.24,
650
+ 0.46,
651
+ 0.32,
652
+ 0.18
653
+ ];
654
+ var BAR_COUNT = 512;
655
+ var VIEWBOX_WIDTH = BAR_COUNT;
656
+ function createWaveformBars(peaks) {
657
+ const source = peaks.length > 1 ? peaks : FALLBACK_PEAKS;
658
+ return Array.from({ length: BAR_COUNT }, (_, index) => {
659
+ const sourcePosition = index / Math.max(BAR_COUNT - 1, 1) * (source.length - 1);
660
+ const leftIndex = Math.floor(sourcePosition);
661
+ const rightIndex = Math.min(leftIndex + 1, source.length - 1);
662
+ const mix = sourcePosition - leftIndex;
663
+ const leftPeak = Math.abs(source[leftIndex] ?? 0);
664
+ const rightPeak = Math.abs(source[rightIndex] ?? leftPeak);
665
+ const interpolated = leftPeak + (rightPeak - leftPeak) * mix;
666
+ if (source.length >= BAR_COUNT) {
667
+ return clamp(Math.pow(interpolated, 1.18), 0.04, 1);
668
+ }
669
+ const noiseSeed = Math.sin((index + 1) * 12.9898 + source.length * 78.233) * 43758.5453;
670
+ const noise = noiseSeed - Math.floor(noiseSeed);
671
+ const fineDetail = 0.76 + noise * 0.34;
672
+ const transient = noise > 0.975 ? 3.2 : noise > 0.91 ? 1.75 : 1;
673
+ return clamp(
674
+ Math.pow(interpolated, 1.22) * fineDetail * transient,
675
+ 0.04,
676
+ 1
677
+ );
678
+ });
679
+ }
680
+ function Waveform({
681
+ className,
682
+ opacity = 1,
683
+ playedStateEnabled = true,
684
+ progress = 0,
685
+ waveform
686
+ }) {
687
+ const clipId = useId().replaceAll(":", "");
688
+ const bars = createWaveformBars(waveform?.peaks ?? FALLBACK_PEAKS);
689
+ const progressWidth = clamp(progress, 0, 1) * VIEWBOX_WIDTH;
690
+ const classes = ["player-waveform", className].filter(Boolean).join(" ");
691
+ const barPath = bars.map((peak, index) => {
692
+ const halfHeight = peak * 47;
693
+ const x = index + 0.5;
694
+ return `M ${String(x)} ${String(50 - halfHeight)} V ${String(
695
+ 50 + halfHeight
696
+ )}`;
697
+ }).join(" ");
698
+ return /* @__PURE__ */ jsxs(
699
+ "svg",
700
+ {
701
+ "aria-hidden": "true",
702
+ className: classes,
703
+ focusable: "false",
704
+ preserveAspectRatio: "none",
705
+ style: { opacity: clamp(opacity, 0, 1), pointerEvents: "none" },
706
+ viewBox: `0 0 ${String(VIEWBOX_WIDTH)} 100`,
707
+ children: [
708
+ /* @__PURE__ */ jsx("path", { className: "player-waveform__base", d: barPath }),
709
+ playedStateEnabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
710
+ /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { width: progressWidth, height: "100" }) }),
711
+ /* @__PURE__ */ jsx(
712
+ "path",
713
+ {
714
+ className: "player-waveform__played",
715
+ clipPath: `url(#${clipId})`,
716
+ d: barPath
717
+ }
718
+ )
719
+ ] }) : null
720
+ ]
721
+ }
722
+ );
723
+ }
724
+ function CompactPlayer({
725
+ appearance,
726
+ className,
727
+ commands,
728
+ state
729
+ }) {
730
+ const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
731
+ return /* @__PURE__ */ jsxs(
732
+ PlayerSurface,
733
+ {
734
+ appearance,
735
+ className: ["player-compact", className].filter(Boolean).join(" "),
736
+ label: "Compact music player",
737
+ children: [
738
+ /* @__PURE__ */ jsxs("div", { className: "player-compact__main", children: [
739
+ /* @__PURE__ */ jsx(PrimaryPlaybackButton, { commands, state }),
740
+ /* @__PURE__ */ jsx(Artwork, { className: "player-compact__artwork", item: state.currentItem }),
741
+ /* @__PURE__ */ jsx(
742
+ TrackMetadata,
743
+ {
744
+ item: state.currentItem,
745
+ scrollTitle: state.status === "playing"
746
+ }
747
+ ),
748
+ /* @__PURE__ */ jsxs("div", { className: "player-compact__signal", children: [
749
+ /* @__PURE__ */ jsx(PlayerStatus, { state }),
750
+ /* @__PURE__ */ jsx(
751
+ Waveform,
752
+ {
753
+ opacity: state.experienceConfig.waveformOpacity,
754
+ playedStateEnabled: state.experienceConfig.waveformPlayedStateEnabled,
755
+ progress,
756
+ waveform: state.currentItem?.waveform
757
+ }
758
+ ),
759
+ /* @__PURE__ */ jsx(
760
+ TimeReadout,
761
+ {
762
+ currentTimeSec: state.currentTimeSec,
763
+ durationSec: state.durationSec
764
+ }
765
+ )
766
+ ] }),
767
+ /* @__PURE__ */ jsx(
768
+ PopoverVolumeControl,
769
+ {
770
+ commands,
771
+ label: "Compact volume",
772
+ state
773
+ }
774
+ )
775
+ ] }),
776
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
777
+ ]
778
+ }
779
+ );
780
+ }
781
+ function SeekBar({
782
+ canSeek,
783
+ className,
784
+ commitMode,
785
+ currentTimeSec,
786
+ durationSec,
787
+ label = "Seek",
788
+ onSeek,
789
+ status
790
+ }) {
791
+ const duration = durationSec !== null && Number.isFinite(durationSec) && durationSec > 0 ? durationSec : 0;
792
+ const disabled = !canSeek || duration === 0 || status === "loading";
793
+ const safeCurrentTime = clamp(currentTimeSec, 0, duration);
794
+ const [previewTime, setPreviewTime] = useState(safeCurrentTime);
795
+ const previewProgress = duration === 0 ? 0 : clamp(previewTime / duration, 0, 1) * 100;
796
+ const isAdjusting = useRef(false);
797
+ const lastCommitted = useRef(null);
798
+ useEffect(() => {
799
+ if (!isAdjusting.current) {
800
+ setPreviewTime(safeCurrentTime);
801
+ }
802
+ }, [safeCurrentTime]);
803
+ function updatePreview(value) {
804
+ const nextValue = clamp(value, 0, duration);
805
+ isAdjusting.current = true;
806
+ setPreviewTime(nextValue);
807
+ if (commitMode === "live") {
808
+ onSeek(nextValue);
809
+ lastCommitted.current = nextValue;
810
+ }
811
+ }
812
+ function commitPreview(value = previewTime) {
813
+ const nextValue = clamp(value, 0, duration);
814
+ if (disabled || !isAdjusting.current || commitMode === "live" && lastCommitted.current === nextValue) {
815
+ isAdjusting.current = false;
816
+ return;
817
+ }
818
+ onSeek(nextValue);
819
+ lastCommitted.current = nextValue;
820
+ isAdjusting.current = false;
821
+ }
822
+ function cancelPreview() {
823
+ isAdjusting.current = false;
824
+ lastCommitted.current = null;
825
+ setPreviewTime(safeCurrentTime);
826
+ }
827
+ return /* @__PURE__ */ jsxs(
828
+ "label",
829
+ {
830
+ className: ["player-seek", className].filter(Boolean).join(" "),
831
+ "data-disabled": disabled ? "true" : "false",
832
+ children: [
833
+ /* @__PURE__ */ jsx("span", { className: "player-visually-hidden", children: label }),
834
+ /* @__PURE__ */ jsx(
835
+ "span",
836
+ {
837
+ "aria-hidden": "true",
838
+ className: "player-seek__progress",
839
+ style: { width: `${String(disabled ? 0 : previewProgress)}%` }
840
+ }
841
+ ),
842
+ /* @__PURE__ */ jsx(
843
+ "input",
844
+ {
845
+ "aria-label": label,
846
+ "aria-valuetext": formatTimeAria(previewTime),
847
+ disabled,
848
+ max: duration || 1,
849
+ min: 0,
850
+ onBlur: (event) => {
851
+ commitPreview(Number(event.currentTarget.value));
852
+ },
853
+ onChange: (event) => {
854
+ updatePreview(Number(event.currentTarget.value));
855
+ },
856
+ onKeyUp: (event) => {
857
+ commitPreview(Number(event.currentTarget.value));
858
+ },
859
+ onPointerDown: () => {
860
+ isAdjusting.current = true;
861
+ },
862
+ onPointerCancel: cancelPreview,
863
+ onPointerUp: (event) => {
864
+ commitPreview(Number(event.currentTarget.value));
865
+ },
866
+ step: 0.1,
867
+ type: "range",
868
+ value: disabled ? 0 : previewTime
869
+ }
870
+ )
871
+ ]
872
+ }
873
+ );
874
+ }
875
+ function QueueRow({
876
+ commands,
877
+ index,
878
+ isCurrent,
879
+ queue,
880
+ queueItem
881
+ }) {
882
+ return /* @__PURE__ */ jsx("li", { "data-current": isCurrent ? "true" : "false", children: /* @__PURE__ */ jsxs(
883
+ "button",
884
+ {
885
+ "aria-current": isCurrent ? "true" : void 0,
886
+ "aria-label": `Play ${queueItem.item.title}`,
887
+ onClick: () => {
888
+ void commands.setQueue(queue, {
889
+ autoplay: true,
890
+ startQueueItemId: queueItem.queueItemId
891
+ });
892
+ },
893
+ type: "button",
894
+ children: [
895
+ /* @__PURE__ */ jsx("span", { className: "player-queue-number", children: String(index + 1).padStart(2, "0") }),
896
+ /* @__PURE__ */ jsx("span", { className: "player-queue-title", children: queueItem.item.title }),
897
+ /* @__PURE__ */ jsx("time", { children: formatTime(queueItem.item.durationHintSec ?? null) })
898
+ ]
899
+ }
900
+ ) });
901
+ }
902
+ function DetailedPlayer({
903
+ appearance,
904
+ className,
905
+ commands,
906
+ queueLimit = 8,
907
+ state
908
+ }) {
909
+ const safeQueueLimit = Math.max(1, Math.floor(queueLimit));
910
+ const visibleQueue = state.queue.slice(0, safeQueueLimit);
911
+ const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
912
+ return /* @__PURE__ */ jsxs(
913
+ PlayerSurface,
914
+ {
915
+ appearance,
916
+ className: ["player-detailed", className].filter(Boolean).join(" "),
917
+ label: "Detailed music player",
918
+ children: [
919
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__display", children: [
920
+ /* @__PURE__ */ jsx(
921
+ Artwork,
922
+ {
923
+ className: "player-detailed__artwork",
924
+ item: state.currentItem
925
+ }
926
+ ),
927
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__primary", children: [
928
+ /* @__PURE__ */ jsx(
929
+ TrackMetadata,
930
+ {
931
+ item: state.currentItem,
932
+ scrollTitle: state.status === "playing",
933
+ status: "Now playing"
934
+ }
935
+ ),
936
+ /* @__PURE__ */ jsx(
937
+ Waveform,
938
+ {
939
+ opacity: state.experienceConfig.waveformOpacity,
940
+ playedStateEnabled: state.experienceConfig.waveformPlayedStateEnabled,
941
+ progress,
942
+ waveform: state.currentItem?.waveform
943
+ }
944
+ ),
945
+ /* @__PURE__ */ jsx(
946
+ SeekBar,
947
+ {
948
+ canSeek: state.canSeek,
949
+ commitMode: state.experienceConfig.seekCommitMode,
950
+ currentTimeSec: state.currentTimeSec,
951
+ durationSec: state.durationSec,
952
+ onSeek: (seconds) => {
953
+ commands.seekTo(seconds);
954
+ },
955
+ status: state.status
956
+ }
957
+ )
958
+ ] }),
959
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__numbers", children: [
960
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__metric", children: [
961
+ /* @__PURE__ */ jsx("span", { className: "player-detailed__metric-label", children: "Track" }),
962
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-count", children: [
963
+ state.currentQueueIndex === null ? "--" : String(state.currentQueueIndex + 1).padStart(2, "0"),
964
+ " / ",
965
+ String(state.queue.length).padStart(2, "0")
966
+ ] })
967
+ ] }),
968
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__metric", children: [
969
+ /* @__PURE__ */ jsx("span", { className: "player-detailed__metric-label", children: "Current" }),
970
+ /* @__PURE__ */ jsx("time", { className: "player-detailed__metric-value", children: formatTime(state.currentTimeSec) })
971
+ ] }),
972
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__metric", children: [
973
+ /* @__PURE__ */ jsx("span", { className: "player-detailed__metric-label", children: "Total" }),
974
+ /* @__PURE__ */ jsx("time", { className: "player-detailed__metric-value", children: formatTime(state.durationSec) })
975
+ ] }),
976
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__metric", children: [
977
+ /* @__PURE__ */ jsx("span", { className: "player-detailed__metric-label", children: "Status" }),
978
+ /* @__PURE__ */ jsx(PlayerStatus, { state })
979
+ ] })
980
+ ] })
981
+ ] }),
982
+ /* @__PURE__ */ jsx("div", { className: "player-detailed__transport", children: /* @__PURE__ */ jsx(TransportControls, { commands, state }) }),
983
+ /* @__PURE__ */ jsxs("div", { className: "player-detailed__queue", children: [
984
+ /* @__PURE__ */ jsxs("div", { className: "player-section-heading", children: [
985
+ /* @__PURE__ */ jsx("span", { children: "Queue" }),
986
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-count", children: [
987
+ String(state.queue.length),
988
+ " items"
989
+ ] })
990
+ ] }),
991
+ visibleQueue.length === 0 ? /* @__PURE__ */ jsx("p", { className: "player-empty", children: "Queue is empty." }) : /* @__PURE__ */ jsx("ol", { children: visibleQueue.map((queueItem, index) => /* @__PURE__ */ jsx(
992
+ QueueRow,
993
+ {
994
+ commands,
995
+ index,
996
+ isCurrent: queueItem.queueItemId === state.currentQueueItemId,
997
+ queue: state.queue,
998
+ queueItem
999
+ },
1000
+ queueItem.queueItemId
1001
+ )) })
1002
+ ] }),
1003
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
1004
+ ]
1005
+ }
1006
+ );
1007
+ }
1008
+ function MiniPlayer({
1009
+ appearance,
1010
+ className,
1011
+ commands,
1012
+ state
1013
+ }) {
1014
+ if (state.currentItem === null) {
1015
+ return null;
1016
+ }
1017
+ return /* @__PURE__ */ jsxs(
1018
+ PlayerSurface,
1019
+ {
1020
+ appearance,
1021
+ className: ["player-mini", className].filter(Boolean).join(" "),
1022
+ label: "Mini music player",
1023
+ children: [
1024
+ /* @__PURE__ */ jsxs("div", { className: "player-mini__main", children: [
1025
+ /* @__PURE__ */ jsx(Artwork, { className: "player-mini__artwork", item: state.currentItem }),
1026
+ /* @__PURE__ */ jsx(
1027
+ TrackMetadata,
1028
+ {
1029
+ item: state.currentItem,
1030
+ scrollTitle: state.status === "playing"
1031
+ }
1032
+ ),
1033
+ /* @__PURE__ */ jsx(PlayerStatus, { state }),
1034
+ /* @__PURE__ */ jsxs("div", { className: "player-mini__controls", children: [
1035
+ /* @__PURE__ */ jsx(
1036
+ TransportControls,
1037
+ {
1038
+ commands,
1039
+ compact: true,
1040
+ showModes: false,
1041
+ state
1042
+ }
1043
+ ),
1044
+ /* @__PURE__ */ jsx(
1045
+ PopoverVolumeControl,
1046
+ {
1047
+ commands,
1048
+ label: "Mini volume",
1049
+ state
1050
+ }
1051
+ )
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxs("div", { className: "player-mini__timeline", children: [
1054
+ /* @__PURE__ */ jsx(
1055
+ SeekBar,
1056
+ {
1057
+ canSeek: state.canSeek,
1058
+ commitMode: state.experienceConfig.seekCommitMode,
1059
+ currentTimeSec: state.currentTimeSec,
1060
+ durationSec: state.durationSec,
1061
+ label: "Seek in mini player",
1062
+ onSeek: (seconds) => {
1063
+ commands.seekTo(seconds);
1064
+ },
1065
+ status: state.status
1066
+ }
1067
+ ),
1068
+ /* @__PURE__ */ jsx(
1069
+ TimeReadout,
1070
+ {
1071
+ currentTimeSec: state.currentTimeSec,
1072
+ durationSec: state.durationSec
1073
+ }
1074
+ )
1075
+ ] })
1076
+ ] }),
1077
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
1078
+ ]
1079
+ }
1080
+ );
1081
+ }
1082
+ function PodcastPlayer({
1083
+ appearance,
1084
+ className,
1085
+ commands,
1086
+ state
1087
+ }) {
1088
+ const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
1089
+ const backSeconds = state.experienceConfig.podcastBackSec;
1090
+ const forwardSeconds = state.experienceConfig.podcastForwardSec;
1091
+ return /* @__PURE__ */ jsxs(
1092
+ PlayerSurface,
1093
+ {
1094
+ appearance,
1095
+ className: ["player-podcast", className].filter(Boolean).join(" "),
1096
+ label: "Podcast player",
1097
+ children: [
1098
+ /* @__PURE__ */ jsxs("div", { className: "player-podcast__header", children: [
1099
+ /* @__PURE__ */ jsx(
1100
+ Artwork,
1101
+ {
1102
+ className: "player-podcast__artwork",
1103
+ item: state.currentItem
1104
+ }
1105
+ ),
1106
+ /* @__PURE__ */ jsx(
1107
+ TrackMetadata,
1108
+ {
1109
+ item: state.currentItem,
1110
+ scrollTitle: state.status === "playing",
1111
+ status: "Now playing"
1112
+ }
1113
+ ),
1114
+ /* @__PURE__ */ jsx(PlayerStatus, { state })
1115
+ ] }),
1116
+ /* @__PURE__ */ jsxs("div", { className: "player-podcast__timeline", children: [
1117
+ /* @__PURE__ */ jsx(
1118
+ Waveform,
1119
+ {
1120
+ opacity: state.experienceConfig.waveformOpacity,
1121
+ playedStateEnabled: state.experienceConfig.waveformPlayedStateEnabled,
1122
+ progress,
1123
+ waveform: state.currentItem?.waveform
1124
+ }
1125
+ ),
1126
+ /* @__PURE__ */ jsx(
1127
+ SeekBar,
1128
+ {
1129
+ canSeek: state.canSeek,
1130
+ commitMode: state.experienceConfig.seekCommitMode,
1131
+ currentTimeSec: state.currentTimeSec,
1132
+ durationSec: state.durationSec,
1133
+ onSeek: (seconds) => {
1134
+ commands.seekTo(seconds);
1135
+ },
1136
+ status: state.status
1137
+ }
1138
+ ),
1139
+ /* @__PURE__ */ jsx(
1140
+ TimeReadout,
1141
+ {
1142
+ currentTimeSec: state.currentTimeSec,
1143
+ durationSec: state.durationSec
1144
+ }
1145
+ )
1146
+ ] }),
1147
+ /* @__PURE__ */ jsxs(
1148
+ "div",
1149
+ {
1150
+ "aria-label": "Podcast playback controls",
1151
+ className: "player-podcast__transport",
1152
+ role: "group",
1153
+ children: [
1154
+ /* @__PURE__ */ jsx(
1155
+ IconButton,
1156
+ {
1157
+ disabled: !state.canGoPrevious,
1158
+ label: "Previous episode",
1159
+ onClick: () => {
1160
+ void commands.previous();
1161
+ },
1162
+ children: /* @__PURE__ */ jsx(PreviousIcon, {})
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsx(
1166
+ IconButton,
1167
+ {
1168
+ disabled: !state.canSeek,
1169
+ label: `Skip back ${String(backSeconds)} seconds`,
1170
+ onClick: () => {
1171
+ commands.seekBy(-backSeconds);
1172
+ },
1173
+ children: /* @__PURE__ */ jsx(PodcastSkipBackIcon, { size: 28 })
1174
+ }
1175
+ ),
1176
+ /* @__PURE__ */ jsx(PrimaryPlaybackButton, { commands, state }),
1177
+ /* @__PURE__ */ jsx(
1178
+ IconButton,
1179
+ {
1180
+ disabled: !state.canSeek,
1181
+ label: `Skip forward ${String(forwardSeconds)} seconds`,
1182
+ onClick: () => {
1183
+ commands.seekBy(forwardSeconds);
1184
+ },
1185
+ children: /* @__PURE__ */ jsx(PodcastSkipForwardIcon, { size: 28 })
1186
+ }
1187
+ ),
1188
+ /* @__PURE__ */ jsx(
1189
+ IconButton,
1190
+ {
1191
+ disabled: !state.canGoNext,
1192
+ label: "Next episode",
1193
+ onClick: () => {
1194
+ void commands.next();
1195
+ },
1196
+ children: /* @__PURE__ */ jsx(NextIcon, {})
1197
+ }
1198
+ )
1199
+ ]
1200
+ }
1201
+ ),
1202
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
1203
+ ]
1204
+ }
1205
+ );
1206
+ }
1207
+ function QueuePanel({
1208
+ appearance,
1209
+ className,
1210
+ commands,
1211
+ label = "Queue",
1212
+ onClose,
1213
+ state
1214
+ }) {
1215
+ return /* @__PURE__ */ jsxs(
1216
+ PlayerSurface,
1217
+ {
1218
+ appearance,
1219
+ className: ["player-queue-panel", className].filter(Boolean).join(" "),
1220
+ label: "Queue panel",
1221
+ children: [
1222
+ /* @__PURE__ */ jsxs("header", { children: [
1223
+ /* @__PURE__ */ jsxs("div", { children: [
1224
+ /* @__PURE__ */ jsx("span", { className: "player-kicker", children: "Playlist" }),
1225
+ /* @__PURE__ */ jsx("h2", { children: label })
1226
+ ] }),
1227
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-count", children: [
1228
+ String(state.queue.length),
1229
+ " items"
1230
+ ] }),
1231
+ onClose === void 0 ? null : /* @__PURE__ */ jsx(IconButton, { label: "Close queue", onClick: onClose, children: /* @__PURE__ */ jsx(CloseIcon, {}) })
1232
+ ] }),
1233
+ state.queue.length === 0 ? /* @__PURE__ */ jsx("p", { className: "player-empty", children: "Queue is empty." }) : /* @__PURE__ */ jsx("ol", { children: state.queue.map((queueItem, index) => {
1234
+ const isCurrent = queueItem.queueItemId === state.currentQueueItemId;
1235
+ return /* @__PURE__ */ jsx(
1236
+ "li",
1237
+ {
1238
+ "data-current": isCurrent ? "true" : "false",
1239
+ children: /* @__PURE__ */ jsxs(
1240
+ "button",
1241
+ {
1242
+ "aria-current": isCurrent ? "true" : void 0,
1243
+ "aria-label": `Play ${queueItem.item.title}`,
1244
+ onClick: () => {
1245
+ void commands.setQueue(state.queue, {
1246
+ autoplay: true,
1247
+ startQueueItemId: queueItem.queueItemId
1248
+ });
1249
+ },
1250
+ type: "button",
1251
+ children: [
1252
+ /* @__PURE__ */ jsx("span", { className: "player-queue-number", children: String(index + 1).padStart(2, "0") }),
1253
+ /* @__PURE__ */ jsxs("span", { className: "player-queue-copy", children: [
1254
+ /* @__PURE__ */ jsx("strong", { children: queueItem.item.title }),
1255
+ /* @__PURE__ */ jsx("small", { children: queueItem.item.creatorName ?? "Creator unavailable" })
1256
+ ] }),
1257
+ /* @__PURE__ */ jsx("time", { children: formatTime(queueItem.item.durationHintSec ?? null) })
1258
+ ]
1259
+ }
1260
+ )
1261
+ },
1262
+ queueItem.queueItemId
1263
+ );
1264
+ }) })
1265
+ ]
1266
+ }
1267
+ );
1268
+ }
1269
+ function WidePlayer({
1270
+ appearance,
1271
+ className,
1272
+ commands,
1273
+ state
1274
+ }) {
1275
+ const progress = state.durationSec === null || state.durationSec <= 0 ? 0 : state.currentTimeSec / state.durationSec;
1276
+ return /* @__PURE__ */ jsxs(
1277
+ PlayerSurface,
1278
+ {
1279
+ appearance,
1280
+ className: ["player-wide", className].filter(Boolean).join(" "),
1281
+ label: "Wide music player",
1282
+ children: [
1283
+ /* @__PURE__ */ jsxs("div", { className: "player-wide__grid", children: [
1284
+ /* @__PURE__ */ jsxs("div", { className: "player-wide__header", children: [
1285
+ /* @__PURE__ */ jsx(Artwork, { className: "player-wide__artwork", item: state.currentItem }),
1286
+ /* @__PURE__ */ jsxs("div", { className: "player-wide__identity", children: [
1287
+ /* @__PURE__ */ jsx(
1288
+ TrackMetadata,
1289
+ {
1290
+ item: state.currentItem,
1291
+ scrollTitle: state.status === "playing",
1292
+ status: "Now playing"
1293
+ }
1294
+ ),
1295
+ /* @__PURE__ */ jsx(PlayerStatus, { state })
1296
+ ] })
1297
+ ] }),
1298
+ /* @__PURE__ */ jsxs("div", { className: "player-wide__content", children: [
1299
+ /* @__PURE__ */ jsxs("div", { className: "player-timeline", children: [
1300
+ /* @__PURE__ */ jsx(
1301
+ Waveform,
1302
+ {
1303
+ opacity: state.experienceConfig.waveformOpacity,
1304
+ playedStateEnabled: state.experienceConfig.waveformPlayedStateEnabled,
1305
+ progress,
1306
+ waveform: state.currentItem?.waveform
1307
+ }
1308
+ ),
1309
+ /* @__PURE__ */ jsx(
1310
+ SeekBar,
1311
+ {
1312
+ canSeek: state.canSeek,
1313
+ commitMode: state.experienceConfig.seekCommitMode,
1314
+ currentTimeSec: state.currentTimeSec,
1315
+ durationSec: state.durationSec,
1316
+ onSeek: (seconds) => {
1317
+ commands.seekTo(seconds);
1318
+ },
1319
+ status: state.status
1320
+ }
1321
+ ),
1322
+ /* @__PURE__ */ jsxs("div", { className: "player-wide__timeline-footer", children: [
1323
+ /* @__PURE__ */ jsx(
1324
+ TimeReadout,
1325
+ {
1326
+ currentTimeSec: state.currentTimeSec,
1327
+ durationSec: state.durationSec
1328
+ }
1329
+ ),
1330
+ /* @__PURE__ */ jsx(VolumeControl, { commands, state })
1331
+ ] })
1332
+ ] }),
1333
+ /* @__PURE__ */ jsx(TransportControls, { commands, state })
1334
+ ] })
1335
+ ] }),
1336
+ /* @__PURE__ */ jsx(PlayerErrorNotice, { commands, state })
1337
+ ]
1338
+ }
1339
+ );
1340
+ }
1341
+
1342
+ export { Artwork, CompactPlayer, DetailedPlayer, IconButton, MiniPlayer, PlayerErrorNotice, PlayerStatus, PlayerSurface, PodcastPlayer, PopoverVolumeControl, PrimaryPlaybackButton, QueuePanel, QueueToggle, SeekBar, TimeReadout, TrackMetadata, TransportControls, VolumeControl, Waveform, WidePlayer, clamp, formatTime, formatTimeAria, getErrorMessage, getNextRepeatMode, getRepeatLabel, getStatusLabel };
1343
+ //# sourceMappingURL=index.js.map
1344
+ //# sourceMappingURL=index.js.map