@vanduo-oss/vd3-cbun 1.0.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 (45) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +22 -0
  3. package/README.md +160 -0
  4. package/SKILL.md +119 -0
  5. package/dist/charts/core.d.ts +273 -0
  6. package/dist/charts/index.cjs +1828 -0
  7. package/dist/charts/index.cjs.map +7 -0
  8. package/dist/charts/index.d.ts +65 -0
  9. package/dist/charts/index.js +1805 -0
  10. package/dist/charts/index.js.map +7 -0
  11. package/dist/charts/vd3-charts.css +51 -0
  12. package/dist/charts/vue.d.ts +86 -0
  13. package/dist/flowchart/core.d.ts +288 -0
  14. package/dist/flowchart/index.cjs +3447 -0
  15. package/dist/flowchart/index.cjs.map +7 -0
  16. package/dist/flowchart/index.d.ts +54 -0
  17. package/dist/flowchart/index.js +3424 -0
  18. package/dist/flowchart/index.js.map +7 -0
  19. package/dist/flowchart/vd3-flowchart.css +600 -0
  20. package/dist/flowchart/vue.d.ts +66 -0
  21. package/dist/hex-grid/core.d.ts +200 -0
  22. package/dist/hex-grid/hex-math.cjs +162 -0
  23. package/dist/hex-grid/hex-math.cjs.map +7 -0
  24. package/dist/hex-grid/hex-math.d.ts +119 -0
  25. package/dist/hex-grid/hex-math.js +141 -0
  26. package/dist/hex-grid/hex-math.js.map +7 -0
  27. package/dist/hex-grid/index.cjs +915 -0
  28. package/dist/hex-grid/index.cjs.map +7 -0
  29. package/dist/hex-grid/index.d.ts +15 -0
  30. package/dist/hex-grid/index.js +894 -0
  31. package/dist/hex-grid/index.js.map +7 -0
  32. package/dist/hex-grid/vue.d.ts +14 -0
  33. package/dist/index.d.ts +13 -0
  34. package/dist/index.js +11 -0
  35. package/dist/index.js.map +7 -0
  36. package/dist/meta.json +551 -0
  37. package/dist/music-player/core.d.ts +88 -0
  38. package/dist/music-player/index.cjs +1227 -0
  39. package/dist/music-player/index.cjs.map +7 -0
  40. package/dist/music-player/index.d.ts +12 -0
  41. package/dist/music-player/index.js +1204 -0
  42. package/dist/music-player/index.js.map +7 -0
  43. package/dist/music-player/vd3-music-player.css +829 -0
  44. package/dist/music-player/vue.d.ts +32 -0
  45. package/package.json +105 -0
@@ -0,0 +1,1227 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/music-player/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MusicPlayer: () => MusicPlayer,
24
+ VD_MUSIC_PLAYER_VERSION: () => VD_MUSIC_PLAYER_VERSION,
25
+ VdMusicPlayer: () => VdMusicPlayer
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/music-player/vue.js
30
+ var import_vue = require("vue");
31
+
32
+ // src/music-player/core.js
33
+ var VD_MUSIC_PLAYER_VERSION = "1.0.0";
34
+ var CORNER_POSITIONS = ["bottom-left", "bottom-right", "top-left", "top-right"];
35
+ var REPEAT_MODES = ["off", "one", "all"];
36
+ var REPEAT_CYCLE = { off: "one", one: "all", all: "off" };
37
+ var REPEAT_LABELS = { off: "Repeat", one: "Repeat one", all: "Repeat all" };
38
+ function normalizeRepeat(value) {
39
+ return REPEAT_MODES.includes(value) ? value : "off";
40
+ }
41
+ function normalizeCornerPosition(position) {
42
+ return CORNER_POSITIONS.includes(position) ? position : "bottom-right";
43
+ }
44
+ function shuffleArray(arr) {
45
+ const shuffled = arr.slice();
46
+ for (let i = shuffled.length - 1; i > 0; i--) {
47
+ const j = Math.floor(Math.random() * (i + 1));
48
+ const tmp = shuffled[i];
49
+ shuffled[i] = shuffled[j];
50
+ shuffled[j] = tmp;
51
+ }
52
+ return shuffled;
53
+ }
54
+ function formatTime(seconds) {
55
+ if (!isFinite(seconds) || seconds < 0) return "0:00";
56
+ const m = Math.floor(seconds / 60);
57
+ const s = Math.floor(seconds % 60);
58
+ return m + ":" + (s < 10 ? "0" : "") + s;
59
+ }
60
+ function persistStorageKey(id) {
61
+ return "vanduo:music-player:" + (id && id.trim() ? id.trim() : "default") + ":pos";
62
+ }
63
+ function updateRangeFill(input) {
64
+ const min = parseFloat(input.min) || 0;
65
+ const max = parseFloat(input.max) || 1;
66
+ const val = parseFloat(input.value) || 0;
67
+ const pct = (val - min) / (max - min) * 100;
68
+ input.style.setProperty("--vd-fill", pct + "%");
69
+ input.style.backgroundImage = "linear-gradient(to right, var(--vd-music-player-track-fill, currentColor) 0%, var(--vd-music-player-track-fill, currentColor) " + pct + "%, var(--vd-music-player-track-bg, #ccc) " + pct + "%, var(--vd-music-player-track-bg, #ccc) 100%)";
70
+ }
71
+ function icon(name) {
72
+ const el = document.createElement("i");
73
+ el.className = "ph ph-" + name;
74
+ el.setAttribute("aria-hidden", "true");
75
+ return el;
76
+ }
77
+ var MusicPlayer = {
78
+ /** @type {Map<HTMLElement, Object>} */
79
+ instances: /* @__PURE__ */ new Map(),
80
+ /**
81
+ * Default options.
82
+ */
83
+ defaults: {
84
+ tracks: [],
85
+ volume: 0.5,
86
+ shuffle: false,
87
+ repeat: "off",
88
+ showProgress: false,
89
+ showPlaylist: false,
90
+ autoAdvance: true,
91
+ glass: false,
92
+ detachable: false,
93
+ /** @type {null|string} */
94
+ floatingPosition: null,
95
+ draggable: false,
96
+ minimizable: false,
97
+ startMinimized: false,
98
+ persistPosition: false,
99
+ persistKey: ""
100
+ },
101
+ /**
102
+ * Initialize a single player element.
103
+ * @param {HTMLElement} container
104
+ * @param {Object} [options]
105
+ */
106
+ initPlayer: function(container, options) {
107
+ const opts = Object.assign({}, this.defaults, options || {});
108
+ const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
109
+ const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
110
+ const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
111
+ const state = {
112
+ tracks: trackList,
113
+ originalTracks: tracks.slice(),
114
+ currentIndex: 0,
115
+ isPlaying: false,
116
+ volume: Math.max(0, Math.min(1, opts.volume)),
117
+ shuffle: opts.shuffle,
118
+ repeat: normalizeRepeat(opts.repeat),
119
+ showProgress: opts.showProgress,
120
+ showPlaylist: opts.showPlaylist,
121
+ autoAdvance: opts.autoAdvance,
122
+ audio: null,
123
+ glass: Boolean(opts.glass),
124
+ detachable: Boolean(opts.detachable),
125
+ floatingPosition: opts.floatingPosition || "bottom-right",
126
+ draggable: Boolean(opts.draggable) && Boolean(opts.detachable),
127
+ minimizable: Boolean(opts.minimizable),
128
+ startMinimized: Boolean(opts.startMinimized),
129
+ persistPosition: Boolean(opts.persistPosition),
130
+ persistKey: typeof opts.persistKey === "string" ? opts.persistKey : "",
131
+ isDetached: false,
132
+ isMinimized: false,
133
+ _startMinimizeApplied: false
134
+ };
135
+ const audio = new Audio();
136
+ audio.volume = state.volume;
137
+ audio.preload = "metadata";
138
+ state.audio = audio;
139
+ this._buildDOM(container, state);
140
+ const refs = {
141
+ btnPlay: container.querySelector(".vd-music-player-btn-play"),
142
+ btnPrev: container.querySelector(".vd-music-player-btn-prev"),
143
+ btnNext: container.querySelector(".vd-music-player-btn-next"),
144
+ btnRepeat: container.querySelector(".vd-music-player-btn-repeat"),
145
+ btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
146
+ btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
147
+ btnDetach: container.querySelector(".vd-music-player-btn-detach"),
148
+ btnAttach: container.querySelector(".vd-music-player-btn-attach"),
149
+ btnMinimize: container.querySelector(".vd-music-player-btn-minimize"),
150
+ dragHandle: container.querySelector(".vd-music-player-drag-handle"),
151
+ trackName: container.querySelector(".vd-music-player-track-name"),
152
+ volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
153
+ volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
154
+ progressBar: container.querySelector(".vd-music-player-progress-bar"),
155
+ timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
156
+ timeDuration: container.querySelector(".vd-music-player-time-duration"),
157
+ playlistPanel: container.querySelector(".vd-music-player-playlist")
158
+ };
159
+ const renderPlayIcon = () => {
160
+ const btn = refs.btnPlay;
161
+ if (!btn) return;
162
+ btn.innerHTML = "";
163
+ btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
164
+ btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
165
+ btn.classList.toggle("is-active", state.isPlaying);
166
+ };
167
+ const renderTrackName = () => {
168
+ const el = refs.trackName;
169
+ if (!el) return;
170
+ const track = state.tracks[state.currentIndex];
171
+ if (track) {
172
+ el.textContent = track.name || "Unknown Track";
173
+ el.classList.remove("is-idle");
174
+ } else {
175
+ el.textContent = "No tracks loaded";
176
+ el.classList.add("is-idle");
177
+ }
178
+ };
179
+ const renderVolumeIcon = () => {
180
+ const el = refs.volumeIcon;
181
+ if (!el) return;
182
+ el.innerHTML = "";
183
+ const v = state.volume;
184
+ const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
185
+ el.appendChild(icon(name));
186
+ };
187
+ const renderShuffleBtn = () => {
188
+ const btn = refs.btnShuffle;
189
+ if (!btn) return;
190
+ btn.classList.toggle("is-active", state.shuffle);
191
+ btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
192
+ };
193
+ const renderRepeatBtn = () => {
194
+ const btn = refs.btnRepeat;
195
+ if (!btn) return;
196
+ btn.innerHTML = "";
197
+ btn.appendChild(icon("repeat"));
198
+ if (state.repeat === "one") {
199
+ const badge = document.createElement("span");
200
+ badge.className = "vd-music-player-repeat-badge";
201
+ badge.setAttribute("aria-hidden", "true");
202
+ badge.textContent = "1";
203
+ btn.appendChild(badge);
204
+ }
205
+ btn.classList.toggle("is-active", state.repeat !== "off");
206
+ btn.setAttribute("aria-pressed", state.repeat !== "off" ? "true" : "false");
207
+ const label = REPEAT_LABELS[state.repeat] || REPEAT_LABELS.off;
208
+ btn.setAttribute("aria-label", label);
209
+ btn.title = label;
210
+ };
211
+ const dispatchRepeatChange = () => {
212
+ container.dispatchEvent(
213
+ new CustomEvent("musicplayer:repeatchange", {
214
+ bubbles: true,
215
+ detail: { repeat: state.repeat }
216
+ })
217
+ );
218
+ };
219
+ const cycleRepeat = () => {
220
+ state.repeat = REPEAT_CYCLE[state.repeat] || "off";
221
+ renderRepeatBtn();
222
+ dispatchRepeatChange();
223
+ };
224
+ const setRepeatMode = (mode) => {
225
+ state.repeat = normalizeRepeat(mode);
226
+ renderRepeatBtn();
227
+ dispatchRepeatChange();
228
+ };
229
+ const renderPlaylistItems = () => {
230
+ const panel = refs.playlistPanel;
231
+ if (!panel) return;
232
+ panel.innerHTML = "";
233
+ state.tracks.forEach((track, i) => {
234
+ const item = document.createElement("button");
235
+ item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
236
+ item.type = "button";
237
+ item.setAttribute("data-index", String(i));
238
+ item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
239
+ const num = document.createElement("span");
240
+ num.className = "vd-music-player-playlist-num";
241
+ num.textContent = String(i + 1);
242
+ const name = document.createElement("span");
243
+ name.className = "vd-music-player-playlist-name";
244
+ name.textContent = track.name || "Track " + (i + 1);
245
+ item.appendChild(num);
246
+ item.appendChild(name);
247
+ panel.appendChild(item);
248
+ });
249
+ };
250
+ const renderProgress = () => {
251
+ const bar = refs.progressBar;
252
+ if (!bar || !audio.duration) return;
253
+ const pct = audio.currentTime / audio.duration * 100;
254
+ bar.value = String(pct);
255
+ updateRangeFill(bar);
256
+ if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
257
+ if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
258
+ };
259
+ const loadTrack = (index, autoPlay) => {
260
+ const track = state.tracks[index];
261
+ if (!track) return;
262
+ state.currentIndex = index;
263
+ audio.src = track.url;
264
+ renderTrackName();
265
+ renderPlaylistItems();
266
+ if (refs.progressBar) {
267
+ refs.progressBar.value = "0";
268
+ updateRangeFill(refs.progressBar);
269
+ }
270
+ if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
271
+ if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
272
+ container.dispatchEvent(
273
+ new CustomEvent("musicplayer:trackchange", {
274
+ bubbles: true,
275
+ detail: { index, name: track.name, url: track.url }
276
+ })
277
+ );
278
+ if (autoPlay) {
279
+ audio.play().catch(() => {
280
+ });
281
+ }
282
+ };
283
+ const cleanupFunctions = [];
284
+ const onPlay = () => {
285
+ state.isPlaying = true;
286
+ renderPlayIcon();
287
+ container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
288
+ };
289
+ const onPause = () => {
290
+ state.isPlaying = false;
291
+ renderPlayIcon();
292
+ container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
293
+ };
294
+ const onEnded = () => {
295
+ if (state.repeat === "one") {
296
+ audio.currentTime = 0;
297
+ audio.play().catch(() => {
298
+ });
299
+ return;
300
+ }
301
+ if (state.repeat === "all") {
302
+ if (state.tracks.length > 0) {
303
+ const next = (state.currentIndex + 1) % state.tracks.length;
304
+ loadTrack(next, true);
305
+ }
306
+ return;
307
+ }
308
+ if (state.autoAdvance && state.tracks.length > 1) {
309
+ const next = (state.currentIndex + 1) % state.tracks.length;
310
+ loadTrack(next, true);
311
+ } else {
312
+ state.isPlaying = false;
313
+ renderPlayIcon();
314
+ container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
315
+ }
316
+ };
317
+ const onTimeUpdate = () => {
318
+ if (state.showProgress) renderProgress();
319
+ };
320
+ const onLoadedMetadata = () => {
321
+ if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
322
+ if (refs.progressBar) {
323
+ refs.progressBar.max = "100";
324
+ updateRangeFill(refs.progressBar);
325
+ }
326
+ };
327
+ audio.addEventListener("play", onPlay);
328
+ audio.addEventListener("pause", onPause);
329
+ audio.addEventListener("ended", onEnded);
330
+ audio.addEventListener("timeupdate", onTimeUpdate);
331
+ audio.addEventListener("loadedmetadata", onLoadedMetadata);
332
+ cleanupFunctions.push(() => {
333
+ audio.removeEventListener("play", onPlay);
334
+ audio.removeEventListener("pause", onPause);
335
+ audio.removeEventListener("ended", onEnded);
336
+ audio.removeEventListener("timeupdate", onTimeUpdate);
337
+ audio.removeEventListener("loadedmetadata", onLoadedMetadata);
338
+ audio.pause();
339
+ audio.src = "";
340
+ });
341
+ if (refs.btnPlay) {
342
+ const handler = () => {
343
+ if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
344
+ if (state.isPlaying) {
345
+ audio.pause();
346
+ } else {
347
+ audio.play().catch(() => {
348
+ });
349
+ }
350
+ };
351
+ refs.btnPlay.addEventListener("click", handler);
352
+ cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
353
+ const keyHandler = (e) => {
354
+ if (e.key === " " || e.key === "Enter") {
355
+ e.preventDefault();
356
+ handler();
357
+ }
358
+ };
359
+ refs.btnPlay.addEventListener("keydown", keyHandler);
360
+ cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
361
+ }
362
+ if (refs.btnPrev) {
363
+ const handler = () => {
364
+ if (!state.tracks.length) return;
365
+ if (audio.currentTime > 3) {
366
+ audio.currentTime = 0;
367
+ } else {
368
+ const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
369
+ loadTrack(prev, state.isPlaying);
370
+ }
371
+ };
372
+ refs.btnPrev.addEventListener("click", handler);
373
+ cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
374
+ }
375
+ if (refs.btnNext) {
376
+ const handler = () => {
377
+ if (!state.tracks.length) return;
378
+ const next = (state.currentIndex + 1) % state.tracks.length;
379
+ loadTrack(next, state.isPlaying);
380
+ };
381
+ refs.btnNext.addEventListener("click", handler);
382
+ cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
383
+ }
384
+ if (refs.btnRepeat) {
385
+ const handler = () => {
386
+ cycleRepeat();
387
+ };
388
+ refs.btnRepeat.addEventListener("click", handler);
389
+ cleanupFunctions.push(() => refs.btnRepeat.removeEventListener("click", handler));
390
+ }
391
+ if (refs.btnShuffle) {
392
+ const handler = () => {
393
+ state.shuffle = !state.shuffle;
394
+ if (state.shuffle) {
395
+ const current = state.tracks[state.currentIndex];
396
+ state.tracks = shuffleArray(state.tracks);
397
+ const newIdx = state.tracks.findIndex((t) => t === current);
398
+ if (newIdx > 0) {
399
+ state.tracks.splice(newIdx, 1);
400
+ state.tracks.unshift(current);
401
+ }
402
+ state.currentIndex = 0;
403
+ } else {
404
+ const current = state.tracks[state.currentIndex];
405
+ state.tracks = state.originalTracks.slice();
406
+ state.currentIndex = state.tracks.findIndex((t) => t === current);
407
+ if (state.currentIndex < 0) state.currentIndex = 0;
408
+ }
409
+ renderShuffleBtn();
410
+ renderPlaylistItems();
411
+ };
412
+ refs.btnShuffle.addEventListener("click", handler);
413
+ cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
414
+ }
415
+ if (refs.btnPlaylist) {
416
+ const handler = () => {
417
+ const panel = refs.playlistPanel;
418
+ if (!panel) return;
419
+ const isOpen = panel.classList.toggle("is-open");
420
+ refs.btnPlaylist.classList.toggle("is-active", isOpen);
421
+ refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
422
+ };
423
+ refs.btnPlaylist.addEventListener("click", handler);
424
+ cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
425
+ }
426
+ if (refs.volumeSlider) {
427
+ const handler = (e) => {
428
+ const v = parseFloat(e.target.value);
429
+ state.volume = v;
430
+ audio.volume = v;
431
+ renderVolumeIcon();
432
+ updateRangeFill(refs.volumeSlider);
433
+ container.dispatchEvent(
434
+ new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
435
+ );
436
+ };
437
+ refs.volumeSlider.addEventListener("input", handler);
438
+ cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
439
+ updateRangeFill(refs.volumeSlider);
440
+ }
441
+ if (refs.progressBar) {
442
+ const handler = (e) => {
443
+ if (!audio.duration) return;
444
+ const pct = parseFloat(e.target.value);
445
+ audio.currentTime = pct / 100 * audio.duration;
446
+ updateRangeFill(refs.progressBar);
447
+ };
448
+ refs.progressBar.addEventListener("input", handler);
449
+ cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
450
+ }
451
+ if (refs.playlistPanel) {
452
+ const panelHandler = (e) => {
453
+ const item = e.target.closest(".vd-music-player-playlist-item");
454
+ if (!item) return;
455
+ const idx = parseInt(item.getAttribute("data-index"), 10);
456
+ if (!isNaN(idx)) loadTrack(idx, true);
457
+ };
458
+ refs.playlistPanel.addEventListener("click", panelHandler);
459
+ cleanupFunctions.push(() => refs.playlistPanel.removeEventListener("click", panelHandler));
460
+ }
461
+ if (refs.btnDetach) {
462
+ const h2 = () => {
463
+ this.detach(container);
464
+ };
465
+ refs.btnDetach.addEventListener("click", h2);
466
+ cleanupFunctions.push(() => refs.btnDetach.removeEventListener("click", h2));
467
+ }
468
+ if (refs.btnAttach) {
469
+ const h2 = () => {
470
+ this.attach(container);
471
+ };
472
+ refs.btnAttach.addEventListener("click", h2);
473
+ cleanupFunctions.push(() => refs.btnAttach.removeEventListener("click", h2));
474
+ }
475
+ if (refs.btnMinimize) {
476
+ const h2 = () => {
477
+ this.toggleMinimize(container);
478
+ };
479
+ refs.btnMinimize.addEventListener("click", h2);
480
+ cleanupFunctions.push(() => refs.btnMinimize.removeEventListener("click", h2));
481
+ }
482
+ renderPlayIcon();
483
+ renderTrackName();
484
+ renderVolumeIcon();
485
+ renderRepeatBtn();
486
+ if (opts.showPlaylist) renderPlaylistItems();
487
+ this.instances.set(container, {
488
+ state,
489
+ audio,
490
+ refs,
491
+ cleanup: cleanupFunctions,
492
+ ui: { restore: null, unbindDrag: null },
493
+ cycleRepeat,
494
+ setRepeatMode
495
+ });
496
+ container.setAttribute("data-music-player-initialized", "true");
497
+ },
498
+ /* ─── DOM builder ─────────────────────────────────────── */
499
+ /**
500
+ * Build the inner DOM structure inside container.
501
+ * Pre-existing inner content is replaced only if it has no
502
+ * recognised child elements (allows server-rendered markup).
503
+ * @param {HTMLElement} container
504
+ * @param {Object} state
505
+ */
506
+ _buildDOM: function(container, state) {
507
+ if (container.querySelector(".vd-music-player-controls")) return;
508
+ container.setAttribute("role", "region");
509
+ container.setAttribute("aria-label", "Music Player");
510
+ if (state.showProgress) container.classList.add("has-progress");
511
+ if (state.showPlaylist) container.classList.add("has-playlist");
512
+ if (state.glass) container.classList.add("vd-music-player-glass");
513
+ if (state.draggable) container.classList.add("vd-music-player-draggable");
514
+ if (state.detachable || state.minimizable) {
515
+ const tb = document.createElement("div");
516
+ tb.className = "vd-music-player-toolbar";
517
+ tb.setAttribute("role", "toolbar");
518
+ tb.setAttribute("aria-label", "Player window");
519
+ if (state.draggable) {
520
+ const h2 = document.createElement("button");
521
+ h2.type = "button";
522
+ h2.className = "vd-music-player-drag-handle";
523
+ h2.setAttribute("aria-label", "Drag to move player");
524
+ h2.appendChild(icon("dots-six-vertical"));
525
+ tb.appendChild(h2);
526
+ }
527
+ const tSp = document.createElement("span");
528
+ tSp.className = "vd-music-player-toolbar-spacer";
529
+ tSp.setAttribute("aria-hidden", "true");
530
+ tb.appendChild(tSp);
531
+ if (state.minimizable) {
532
+ const bMin = document.createElement("button");
533
+ bMin.type = "button";
534
+ bMin.className = "vd-music-player-btn vd-music-player-btn-minimize";
535
+ bMin.setAttribute("aria-label", "Minimize player");
536
+ bMin.setAttribute("aria-expanded", "true");
537
+ bMin.appendChild(icon("minus"));
538
+ tb.appendChild(bMin);
539
+ }
540
+ if (state.detachable) {
541
+ const bOut = document.createElement("button");
542
+ bOut.type = "button";
543
+ bOut.className = "vd-music-player-btn vd-music-player-btn-detach";
544
+ bOut.setAttribute("aria-label", "Detach player");
545
+ bOut.appendChild(icon("arrows-out"));
546
+ tb.appendChild(bOut);
547
+ const bIn = document.createElement("button");
548
+ bIn.type = "button";
549
+ bIn.className = "vd-music-player-btn vd-music-player-btn-attach";
550
+ bIn.setAttribute("aria-label", "Attach player");
551
+ bIn.appendChild(icon("arrows-in"));
552
+ tb.appendChild(bIn);
553
+ }
554
+ container.classList.add("vd-music-player-has-chrome");
555
+ container.appendChild(tb);
556
+ }
557
+ const info = document.createElement("div");
558
+ info.className = "vd-music-player-info";
559
+ const iconWrap = document.createElement("span");
560
+ iconWrap.className = "vd-music-player-icon";
561
+ iconWrap.setAttribute("aria-hidden", "true");
562
+ iconWrap.appendChild(icon("music-note"));
563
+ const trackName = document.createElement("span");
564
+ trackName.className = "vd-music-player-track-name";
565
+ trackName.setAttribute("aria-live", "polite");
566
+ trackName.setAttribute("aria-atomic", "true");
567
+ info.appendChild(iconWrap);
568
+ info.appendChild(trackName);
569
+ container.appendChild(info);
570
+ const controls = document.createElement("div");
571
+ controls.className = "vd-music-player-controls";
572
+ controls.setAttribute("role", "group");
573
+ controls.setAttribute("aria-label", "Playback controls");
574
+ const btnPrev = document.createElement("button");
575
+ btnPrev.type = "button";
576
+ btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
577
+ btnPrev.setAttribute("aria-label", "Previous track");
578
+ btnPrev.appendChild(icon("skip-back"));
579
+ const btnPlay = document.createElement("button");
580
+ btnPlay.type = "button";
581
+ btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
582
+ btnPlay.setAttribute("aria-label", "Play");
583
+ btnPlay.appendChild(icon("play"));
584
+ const btnNext = document.createElement("button");
585
+ btnNext.type = "button";
586
+ btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
587
+ btnNext.setAttribute("aria-label", "Next track");
588
+ btnNext.appendChild(icon("skip-forward"));
589
+ controls.appendChild(btnPrev);
590
+ controls.appendChild(btnPlay);
591
+ controls.appendChild(btnNext);
592
+ const btnRepeat = document.createElement("button");
593
+ btnRepeat.type = "button";
594
+ btnRepeat.className = "vd-music-player-btn vd-music-player-btn-repeat";
595
+ btnRepeat.setAttribute("aria-label", REPEAT_LABELS[normalizeRepeat(state.repeat)]);
596
+ btnRepeat.setAttribute("aria-pressed", state.repeat !== "off" ? "true" : "false");
597
+ btnRepeat.appendChild(icon("repeat"));
598
+ controls.appendChild(btnRepeat);
599
+ if (state.showPlaylist || state.shuffle !== void 0) {
600
+ const btnShuffle = document.createElement("button");
601
+ btnShuffle.type = "button";
602
+ btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
603
+ btnShuffle.setAttribute("aria-label", "Shuffle");
604
+ btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
605
+ btnShuffle.appendChild(icon("shuffle"));
606
+ controls.appendChild(btnShuffle);
607
+ }
608
+ const spacer = document.createElement("span");
609
+ spacer.className = "vd-music-player-spacer";
610
+ spacer.setAttribute("aria-hidden", "true");
611
+ controls.appendChild(spacer);
612
+ const volumeWrap = document.createElement("div");
613
+ volumeWrap.className = "vd-music-player-volume";
614
+ const volumeIcon = document.createElement("span");
615
+ volumeIcon.className = "vd-music-player-volume-icon";
616
+ volumeIcon.setAttribute("aria-hidden", "true");
617
+ const volumeSlider = document.createElement("input");
618
+ volumeSlider.type = "range";
619
+ volumeSlider.className = "vd-music-player-volume-slider";
620
+ volumeSlider.min = "0";
621
+ volumeSlider.max = "1";
622
+ volumeSlider.step = "0.01";
623
+ volumeSlider.value = String(state.volume);
624
+ volumeSlider.setAttribute("aria-label", "Volume");
625
+ volumeWrap.appendChild(volumeIcon);
626
+ volumeWrap.appendChild(volumeSlider);
627
+ controls.appendChild(volumeWrap);
628
+ if (state.showPlaylist) {
629
+ const btnPlaylist = document.createElement("button");
630
+ btnPlaylist.type = "button";
631
+ btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
632
+ btnPlaylist.setAttribute("aria-label", "Show playlist");
633
+ btnPlaylist.setAttribute("aria-expanded", "false");
634
+ btnPlaylist.appendChild(icon("playlist"));
635
+ controls.appendChild(btnPlaylist);
636
+ }
637
+ container.appendChild(controls);
638
+ if (state.showProgress) {
639
+ const progressRow = document.createElement("div");
640
+ progressRow.className = "vd-music-player-progress";
641
+ const timeElapsed = document.createElement("span");
642
+ timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
643
+ timeElapsed.textContent = "0:00";
644
+ timeElapsed.setAttribute("aria-hidden", "true");
645
+ const progressBar = document.createElement("input");
646
+ progressBar.type = "range";
647
+ progressBar.className = "vd-music-player-progress-bar";
648
+ progressBar.min = "0";
649
+ progressBar.max = "100";
650
+ progressBar.step = "0.1";
651
+ progressBar.value = "0";
652
+ progressBar.setAttribute("aria-label", "Seek");
653
+ const timeDuration = document.createElement("span");
654
+ timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
655
+ timeDuration.textContent = "0:00";
656
+ timeDuration.setAttribute("aria-hidden", "true");
657
+ progressRow.appendChild(timeElapsed);
658
+ progressRow.appendChild(progressBar);
659
+ progressRow.appendChild(timeDuration);
660
+ container.appendChild(progressRow);
661
+ }
662
+ if (state.showPlaylist) {
663
+ const playlist = document.createElement("div");
664
+ playlist.className = "vd-music-player-playlist";
665
+ playlist.setAttribute("aria-label", "Playlist");
666
+ container.appendChild(playlist);
667
+ }
668
+ },
669
+ /* ─── Public API ──────────────────────────────────────── */
670
+ /**
671
+ * @param {HTMLElement} container
672
+ */
673
+ play: function(container) {
674
+ const inst = this.instances.get(container);
675
+ if (!inst) return;
676
+ if (!inst.audio.src && inst.state.tracks.length) {
677
+ inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
678
+ }
679
+ inst.audio.play().catch(() => {
680
+ });
681
+ },
682
+ /**
683
+ * @param {HTMLElement} container
684
+ */
685
+ pause: function(container) {
686
+ const inst = this.instances.get(container);
687
+ if (inst) inst.audio.pause();
688
+ },
689
+ /**
690
+ * @param {HTMLElement} container
691
+ */
692
+ toggle: function(container) {
693
+ const inst = this.instances.get(container);
694
+ if (!inst) return;
695
+ if (inst.state.isPlaying) {
696
+ this.pause(container);
697
+ } else {
698
+ this.play(container);
699
+ }
700
+ },
701
+ /**
702
+ * @param {HTMLElement} container
703
+ */
704
+ next: function(container) {
705
+ const inst = this.instances.get(container);
706
+ if (!inst || !inst.state.tracks.length) return;
707
+ const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
708
+ this._loadTrack(inst, next, inst.state.isPlaying);
709
+ },
710
+ /**
711
+ * @param {HTMLElement} container
712
+ */
713
+ previous: function(container) {
714
+ const inst = this.instances.get(container);
715
+ if (!inst || !inst.state.tracks.length) return;
716
+ const len = inst.state.tracks.length;
717
+ const prev = (inst.state.currentIndex - 1 + len) % len;
718
+ this._loadTrack(inst, prev, inst.state.isPlaying);
719
+ },
720
+ /**
721
+ * @param {HTMLElement} container
722
+ * @param {number} value - 0 to 1
723
+ */
724
+ setVolume: function(container, value) {
725
+ const inst = this.instances.get(container);
726
+ if (!inst) return;
727
+ const v = Math.max(0, Math.min(1, value));
728
+ inst.state.volume = v;
729
+ inst.audio.volume = v;
730
+ if (inst.refs.volumeSlider) {
731
+ inst.refs.volumeSlider.value = String(v);
732
+ updateRangeFill(inst.refs.volumeSlider);
733
+ }
734
+ container.dispatchEvent(
735
+ new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
736
+ );
737
+ },
738
+ /**
739
+ * @param {HTMLElement} container
740
+ * @param {number} index - Track index
741
+ */
742
+ setTrack: function(container, index) {
743
+ const inst = this.instances.get(container);
744
+ if (!inst) return;
745
+ this._loadTrack(inst, index, inst.state.isPlaying);
746
+ },
747
+ /**
748
+ * Shuffle or un-shuffle the track list.
749
+ * @param {HTMLElement} container
750
+ */
751
+ shuffle: function(container) {
752
+ const inst = this.instances.get(container);
753
+ if (!inst || !inst.refs.btnShuffle) return;
754
+ inst.refs.btnShuffle.click();
755
+ },
756
+ /**
757
+ * Cycle repeat mode: off → one → all → off.
758
+ * @param {HTMLElement} container
759
+ */
760
+ repeat: function(container) {
761
+ const inst = this.instances.get(container);
762
+ if (!inst || typeof inst.cycleRepeat !== "function") return;
763
+ inst.cycleRepeat();
764
+ },
765
+ /**
766
+ * Set repeat mode explicitly.
767
+ * @param {HTMLElement} container
768
+ * @param {'off'|'one'|'all'} mode
769
+ */
770
+ setRepeat: function(container, mode) {
771
+ const inst = this.instances.get(container);
772
+ if (!inst || typeof inst.setRepeatMode !== "function") return;
773
+ inst.setRepeatMode(mode);
774
+ },
775
+ /**
776
+ * Float the player above the page. Requires { detachable: true } at init.
777
+ * @param {HTMLElement} container
778
+ * @param {string} [position] Corner preset or uses floatingPosition from init
779
+ */
780
+ detach: function(container, position) {
781
+ const inst = this.instances.get(container);
782
+ if (!inst || !inst.state.detachable || inst.state.isDetached) return;
783
+ const s = inst.state;
784
+ inst.ui = inst.ui || { restore: null, unbindDrag: null };
785
+ s.isDetached = true;
786
+ inst.ui.restore = {
787
+ parent: container.parentNode,
788
+ next: container.nextSibling
789
+ };
790
+ document.body.appendChild(container);
791
+ container.classList.add("vd-music-player-floating", "vd-music-player-detached");
792
+ const pos = position != null && position !== void 0 ? position : s.floatingPosition;
793
+ this._setCornerPosition(container, normalizeCornerPosition(pos));
794
+ this._loadPersistedPosition(container, inst);
795
+ if (s.startMinimized && !s._startMinimizeApplied) {
796
+ s._startMinimizeApplied = true;
797
+ this.minimize(container);
798
+ }
799
+ this._bindFloatingDrag(inst);
800
+ container.dispatchEvent(new CustomEvent("musicplayer:detach", { bubbles: true }));
801
+ },
802
+ /**
803
+ * Return a detached player to its original place in the document.
804
+ * @param {HTMLElement} container
805
+ */
806
+ attach: function(container) {
807
+ const inst = this.instances.get(container);
808
+ if (!inst || !inst.state.isDetached) return;
809
+ this._unbindFloatingDrag(inst);
810
+ inst.state.isDetached = false;
811
+ const r = inst.ui && inst.ui.restore;
812
+ container.classList.remove(
813
+ "vd-music-player-floating",
814
+ "vd-music-player-detached",
815
+ "vd-music-player-floating-bottom-left",
816
+ "vd-music-player-floating-bottom-right",
817
+ "vd-music-player-floating-top-left",
818
+ "vd-music-player-floating-top-right",
819
+ "is-position-custom"
820
+ );
821
+ container.style.removeProperty("--vd-music-player-floating-top");
822
+ container.style.removeProperty("--vd-music-player-floating-left");
823
+ if (r && r.parent && r.parent.isConnected) {
824
+ r.parent.insertBefore(container, r.next);
825
+ }
826
+ if (inst.ui) {
827
+ inst.ui.restore = null;
828
+ inst.ui.unbindDrag = null;
829
+ }
830
+ container.dispatchEvent(new CustomEvent("musicplayer:attach", { bubbles: true }));
831
+ },
832
+ /**
833
+ * Collapse to essential controls. Requires { minimizable: true } at init.
834
+ * @param {HTMLElement} container
835
+ */
836
+ minimize: function(container) {
837
+ const inst = this.instances.get(container);
838
+ if (!inst || !inst.state.minimizable || inst.state.isMinimized) return;
839
+ const s = inst.state;
840
+ s.isMinimized = true;
841
+ container.classList.add("vd-music-player-minimized");
842
+ this._setMinimizeButtonState(inst, true);
843
+ if (inst.refs.playlistPanel && inst.refs.playlistPanel.classList.contains("is-open") && inst.refs.btnPlaylist) {
844
+ inst.refs.playlistPanel.classList.remove("is-open");
845
+ inst.refs.btnPlaylist.classList.remove("is-active");
846
+ inst.refs.btnPlaylist.setAttribute("aria-expanded", "false");
847
+ }
848
+ container.dispatchEvent(new CustomEvent("musicplayer:minimize", { bubbles: true }));
849
+ },
850
+ /**
851
+ * Restore from minimized state.
852
+ * @param {HTMLElement} container
853
+ */
854
+ expand: function(container) {
855
+ const inst = this.instances.get(container);
856
+ if (!inst || !inst.state.minimizable || !inst.state.isMinimized) return;
857
+ inst.state.isMinimized = false;
858
+ container.classList.remove("vd-music-player-minimized");
859
+ this._setMinimizeButtonState(inst, false);
860
+ container.dispatchEvent(new CustomEvent("musicplayer:expand", { bubbles: true }));
861
+ },
862
+ /**
863
+ * Toggle minimize / expand.
864
+ * @param {HTMLElement} container
865
+ */
866
+ toggleMinimize: function(container) {
867
+ const inst = this.instances.get(container);
868
+ if (!inst || !inst.state.minimizable) return;
869
+ if (inst.state.isMinimized) {
870
+ this.expand(container);
871
+ } else {
872
+ this.minimize(container);
873
+ }
874
+ },
875
+ /**
876
+ * Set floating corner or pixel position (detached only).
877
+ * @param {HTMLElement} container
878
+ * @param {string|{x:number,y:number}} position Corner preset or { x, y } viewport pixels
879
+ */
880
+ setPosition: function(container, position) {
881
+ const inst = this.instances.get(container);
882
+ if (!inst || !inst.state.isDetached) return;
883
+ if (typeof position === "string") {
884
+ this._setCornerPosition(container, normalizeCornerPosition(position));
885
+ } else if (position && typeof position.x === "number" && typeof position.y === "number") {
886
+ this._setCustomPositionFromRect(container, position.x, position.y);
887
+ }
888
+ if (inst.state.persistPosition) {
889
+ const r = container.getBoundingClientRect();
890
+ this._savePositionPixels(inst, r.left, r.top);
891
+ }
892
+ },
893
+ /**
894
+ * @param {Object} inst
895
+ * @param {boolean} minimized
896
+ */
897
+ _setMinimizeButtonState: function(inst, minimized) {
898
+ const b = inst.refs && inst.refs.btnMinimize;
899
+ if (!b) return;
900
+ b.innerHTML = "";
901
+ b.appendChild(icon(minimized ? "plus" : "minus"));
902
+ b.setAttribute("aria-label", minimized ? "Expand player" : "Minimize player");
903
+ b.setAttribute("aria-expanded", minimized ? "false" : "true");
904
+ },
905
+ /**
906
+ * @param {HTMLElement} container
907
+ * @param {string} which Corner preset from CORNER_POSITIONS
908
+ */
909
+ _setCornerPosition: function(container, which) {
910
+ const corner = normalizeCornerPosition(which);
911
+ container.classList.remove(
912
+ "is-position-custom",
913
+ "vd-music-player-floating-bottom-left",
914
+ "vd-music-player-floating-bottom-right",
915
+ "vd-music-player-floating-top-left",
916
+ "vd-music-player-floating-top-right"
917
+ );
918
+ container.style.removeProperty("--vd-music-player-floating-top");
919
+ container.style.removeProperty("--vd-music-player-floating-left");
920
+ container.classList.add("vd-music-player-floating-" + corner);
921
+ },
922
+ /**
923
+ * @param {HTMLElement} container
924
+ * @param {number} left
925
+ * @param {number} top
926
+ */
927
+ _setCustomPositionFromRect: function(container, left, top) {
928
+ container.classList.remove(
929
+ "vd-music-player-floating-bottom-left",
930
+ "vd-music-player-floating-bottom-right",
931
+ "vd-music-player-floating-top-left",
932
+ "vd-music-player-floating-top-right"
933
+ );
934
+ container.classList.add("is-position-custom");
935
+ container.style.setProperty("--vd-music-player-floating-left", left + "px");
936
+ container.style.setProperty("--vd-music-player-floating-top", top + "px");
937
+ },
938
+ /**
939
+ * @param {HTMLElement} container
940
+ * @param {Object} inst
941
+ */
942
+ _loadPersistedPosition: function(container, inst) {
943
+ if (!inst.state.persistPosition) return;
944
+ const key = this._persistKeyForInstance(inst, container);
945
+ let raw = null;
946
+ try {
947
+ raw = localStorage.getItem(key);
948
+ } catch {
949
+ }
950
+ if (!raw) return;
951
+ try {
952
+ const o = JSON.parse(raw);
953
+ if (o && typeof o.x === "number" && typeof o.y === "number") {
954
+ this._setCustomPositionFromRect(container, o.x, o.y);
955
+ }
956
+ } catch {
957
+ }
958
+ },
959
+ /**
960
+ * @param {Object} inst
961
+ * @param {number} x
962
+ * @param {number} y
963
+ */
964
+ _savePositionPixels: function(inst, x, y) {
965
+ if (!inst.state.persistPosition) return;
966
+ const container = this._containerOf(inst);
967
+ if (!container) return;
968
+ const key = this._persistKeyForInstance(inst, container);
969
+ const val = JSON.stringify({ x, y });
970
+ try {
971
+ localStorage.setItem(key, val);
972
+ } catch {
973
+ }
974
+ },
975
+ /**
976
+ * @param {Object} inst
977
+ * @param {HTMLElement} container
978
+ * @returns {string}
979
+ */
980
+ _persistKeyForInstance: function(inst, container) {
981
+ const pk = inst.state.persistKey;
982
+ if (pk && String(pk).trim()) return persistStorageKey(String(pk).trim());
983
+ return persistStorageKey(container.id || "");
984
+ },
985
+ /**
986
+ * @param {Object} inst
987
+ */
988
+ _unbindFloatingDrag: function(inst) {
989
+ if (inst.ui && typeof inst.ui.unbindDrag === "function") {
990
+ inst.ui.unbindDrag();
991
+ inst.ui.unbindDrag = null;
992
+ }
993
+ },
994
+ /**
995
+ * Free-form pointer drag on the handle. Vanduo's `draggable` component uses HTML5
996
+ * drag/drop for list reordering; floating players use pointer events on the handle instead.
997
+ * @param {Object} inst
998
+ */
999
+ _bindFloatingDrag: function(inst) {
1000
+ this._unbindFloatingDrag(inst);
1001
+ const h2 = inst.refs && inst.refs.dragHandle;
1002
+ if (!h2 || !inst.state || !inst.state.draggable) return;
1003
+ const self = this;
1004
+ const container = this._containerOf(inst);
1005
+ if (!container) return;
1006
+ let startX = 0;
1007
+ let startY = 0;
1008
+ let origL = 0;
1009
+ let origT = 0;
1010
+ let activeDrag = false;
1011
+ const onDown = function(e) {
1012
+ if (e.pointerType === "mouse" && e.button !== 0) return;
1013
+ e.preventDefault();
1014
+ activeDrag = true;
1015
+ const r = container.getBoundingClientRect();
1016
+ origL = r.left;
1017
+ origT = r.top;
1018
+ startX = e.clientX;
1019
+ startY = e.clientY;
1020
+ self._setCustomPositionFromRect(container, origL, origT);
1021
+ try {
1022
+ h2.setPointerCapture(e.pointerId);
1023
+ } catch {
1024
+ }
1025
+ };
1026
+ const onMove = function(e) {
1027
+ if (!activeDrag) return;
1028
+ const dx = e.clientX - startX;
1029
+ const dy = e.clientY - startY;
1030
+ let nl = origL + dx;
1031
+ let nt = origT + dy;
1032
+ const r = container.getBoundingClientRect();
1033
+ const w = r.width;
1034
+ const ph = r.height;
1035
+ const vw = window.innerWidth;
1036
+ const vh = window.innerHeight;
1037
+ const pad = 8;
1038
+ nl = Math.max(pad, Math.min(nl, vw - w - pad));
1039
+ nt = Math.max(pad, Math.min(nt, vh - ph - pad));
1040
+ self._setCustomPositionFromRect(container, nl, nt);
1041
+ };
1042
+ const onUp = function(e) {
1043
+ if (!activeDrag) return;
1044
+ activeDrag = false;
1045
+ if (typeof h2.hasPointerCapture === "function" && h2.hasPointerCapture(e.pointerId)) {
1046
+ try {
1047
+ h2.releasePointerCapture(e.pointerId);
1048
+ } catch {
1049
+ }
1050
+ }
1051
+ if (inst.state.persistPosition) {
1052
+ const r = container.getBoundingClientRect();
1053
+ self._savePositionPixels(inst, r.left, r.top);
1054
+ }
1055
+ };
1056
+ h2.addEventListener("pointerdown", onDown);
1057
+ h2.addEventListener("pointermove", onMove);
1058
+ h2.addEventListener("pointerup", onUp);
1059
+ h2.addEventListener("pointercancel", onUp);
1060
+ inst.ui = inst.ui || { restore: null, unbindDrag: null };
1061
+ inst.ui.unbindDrag = function() {
1062
+ h2.removeEventListener("pointerdown", onDown);
1063
+ h2.removeEventListener("pointermove", onMove);
1064
+ h2.removeEventListener("pointerup", onUp);
1065
+ h2.removeEventListener("pointercancel", onUp);
1066
+ };
1067
+ },
1068
+ /**
1069
+ * Return a shallow copy of the current player state.
1070
+ * @param {HTMLElement} container
1071
+ * @returns {Object|null}
1072
+ */
1073
+ getState: function(container) {
1074
+ const inst = this.instances.get(container);
1075
+ if (!inst) return null;
1076
+ const s = inst.state;
1077
+ return {
1078
+ isPlaying: s.isPlaying,
1079
+ currentIndex: s.currentIndex,
1080
+ currentTrack: s.tracks[s.currentIndex] || null,
1081
+ volume: s.volume,
1082
+ shuffle: s.shuffle,
1083
+ repeat: s.repeat,
1084
+ tracks: s.tracks.slice(),
1085
+ isDetached: Boolean(s.isDetached),
1086
+ isMinimized: Boolean(s.isMinimized)
1087
+ };
1088
+ },
1089
+ /**
1090
+ * Stop playback, clean up listeners, remove instance.
1091
+ * @param {HTMLElement} container
1092
+ */
1093
+ destroy: function(container) {
1094
+ const inst = this.instances.get(container);
1095
+ if (!inst) return;
1096
+ this._unbindFloatingDrag(inst);
1097
+ if (inst.state && inst.state.isDetached) {
1098
+ try {
1099
+ this.attach(container);
1100
+ } catch {
1101
+ }
1102
+ }
1103
+ inst.cleanup.forEach((fn) => fn());
1104
+ this.instances.delete(container);
1105
+ container.removeAttribute("data-music-player-initialized");
1106
+ },
1107
+ /**
1108
+ * Destroy all instances.
1109
+ */
1110
+ destroyAll: function() {
1111
+ this.instances.forEach((_, container) => this.destroy(container));
1112
+ },
1113
+ /* ─── Internal helpers ────────────────────────────────── */
1114
+ /**
1115
+ * Load track by index on an already-initialised instance object.
1116
+ * @param {Object} inst
1117
+ * @param {number} index
1118
+ * @param {boolean} autoPlay
1119
+ */
1120
+ _loadTrack: function(inst, index, autoPlay) {
1121
+ const track = inst.state.tracks[index];
1122
+ if (!track) return;
1123
+ const container = this._containerOf(inst);
1124
+ inst.state.currentIndex = index;
1125
+ inst.audio.src = track.url;
1126
+ if (inst.refs.trackName) {
1127
+ inst.refs.trackName.textContent = track.name || "Unknown Track";
1128
+ inst.refs.trackName.classList.remove("is-idle");
1129
+ }
1130
+ if (inst.refs.playlistPanel) {
1131
+ inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
1132
+ const active = i === index;
1133
+ item.classList.toggle("is-active", active);
1134
+ item.setAttribute("aria-current", active ? "true" : "false");
1135
+ });
1136
+ }
1137
+ if (inst.refs.progressBar) {
1138
+ inst.refs.progressBar.value = "0";
1139
+ updateRangeFill(inst.refs.progressBar);
1140
+ }
1141
+ if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
1142
+ if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
1143
+ if (container) {
1144
+ container.dispatchEvent(
1145
+ new CustomEvent("musicplayer:trackchange", {
1146
+ bubbles: true,
1147
+ detail: { index, name: track.name, url: track.url }
1148
+ })
1149
+ );
1150
+ }
1151
+ if (autoPlay) inst.audio.play().catch(() => {
1152
+ });
1153
+ },
1154
+ /**
1155
+ * Reverse-lookup the container element for a given instance object.
1156
+ * @param {Object} inst
1157
+ * @returns {HTMLElement|null}
1158
+ */
1159
+ _containerOf: function(inst) {
1160
+ for (const [container, i] of this.instances) {
1161
+ if (i === inst) return container;
1162
+ }
1163
+ return null;
1164
+ }
1165
+ };
1166
+ MusicPlayer.version = VD_MUSIC_PLAYER_VERSION;
1167
+
1168
+ // src/music-player/vue.js
1169
+ var PLAYER_EVENTS = [
1170
+ "play",
1171
+ "pause",
1172
+ "trackchange",
1173
+ "volumechange",
1174
+ "repeatchange",
1175
+ "ended",
1176
+ "detach",
1177
+ "attach",
1178
+ "minimize",
1179
+ "expand"
1180
+ ];
1181
+ var VdMusicPlayer = (0, import_vue.defineComponent)({
1182
+ name: "VdMusicPlayer",
1183
+ props: {
1184
+ /** Playlist — `[{ name, url }]`. */
1185
+ tracks: { type: Array, default: () => [] },
1186
+ /** Player options (volume, shuffle, repeat, glass, detachable, …). */
1187
+ options: { type: Object, default: () => ({}) }
1188
+ },
1189
+ emits: [...PLAYER_EVENTS, "ready"],
1190
+ setup(props, { emit, expose }) {
1191
+ const el = (0, import_vue.ref)(null);
1192
+ const bound = [];
1193
+ const create = () => {
1194
+ MusicPlayer.initPlayer(el.value, { tracks: props.tracks, ...props.options });
1195
+ PLAYER_EVENTS.forEach((name) => {
1196
+ const type = `musicplayer:${name}`;
1197
+ const handler = (e) => emit(name, e.detail);
1198
+ el.value.addEventListener(type, handler);
1199
+ bound.push([type, handler]);
1200
+ });
1201
+ emit("ready", el.value);
1202
+ };
1203
+ const teardown = () => {
1204
+ bound.forEach(([type, handler]) => {
1205
+ if (el.value) el.value.removeEventListener(type, handler);
1206
+ });
1207
+ bound.length = 0;
1208
+ if (el.value) MusicPlayer.destroy(el.value);
1209
+ };
1210
+ (0, import_vue.onMounted)(() => {
1211
+ if (typeof window === "undefined" || !el.value) return;
1212
+ create();
1213
+ });
1214
+ (0, import_vue.watch)(
1215
+ () => [props.tracks, props.options],
1216
+ () => {
1217
+ teardown();
1218
+ create();
1219
+ },
1220
+ { deep: true }
1221
+ );
1222
+ (0, import_vue.onBeforeUnmount)(teardown);
1223
+ expose({ player: MusicPlayer, container: () => el.value });
1224
+ return () => (0, import_vue.h)("div", { ref: el, class: "vd-music-player" });
1225
+ }
1226
+ });
1227
+ //# sourceMappingURL=index.cjs.map