@arraypress/waveform-bar 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,9 @@ Think Spotify's bottom player — but lightweight, zero-config, and works on any
47
47
  <!-- WaveformBar -->
48
48
  <link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar.css">
49
49
  <script src="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar.js"></script>
50
+
51
+ <!-- Optional: Page icons (play/pause overlays, hearts, carts, etc.) -->
52
+ <link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar-icons.css">
50
53
  ```
51
54
 
52
55
  ### NPM
@@ -130,21 +133,62 @@ Add to any element to queue a track without immediately playing it.
130
133
  </button>
131
134
  ```
132
135
 
133
- ### Pre-Generated Waveform Data
136
+ ### Pre-Generated Waveforms
137
+
138
+ Skip client-side audio analysis by providing pre-generated waveform data. The `data-wb-waveform` attribute accepts three
139
+ formats:
134
140
 
135
- Skip client-side audio analysis by providing pre-generated waveform peaks.
136
- Use [waveform-gen](https://github.com/arraypress/waveform-gen) to generate the data.
141
+ | Format | Example | Description |
142
+ |---------------|--------------------------|--------------------------------------------------|
143
+ | JSON file URL | `waveforms/song.json` | Fetches peaks from `{ peaks: [...] }` or `[...]` |
144
+ | Inline array | `[0.2, 0.37, 0.41, ...]` | JSON array string |
145
+ | CSV string | `0.2,0.37,0.41,...` | Comma-separated values |
137
146
 
138
147
  ```html
148
+ <!-- JSON file — fetched automatically by the player -->
149
+ <div data-wb-play
150
+ data-url="audio/song.mp3"
151
+ data-title="My Song"
152
+ data-wb-waveform="waveforms/song.json">
153
+ </div>
139
154
 
155
+ <!-- Inline peaks -->
140
156
  <div data-wb-play
141
- data-url="song.mp3"
157
+ data-url="audio/song.mp3"
142
158
  data-title="My Song"
143
- data-wb-waveform='[0.12,0.45,0.89,0.34,0.67,0.92,0.15,0.78]'>
159
+ data-wb-waveform="[0.12,0.45,0.89,0.34,0.67]">
144
160
  </div>
145
161
  ```
146
162
 
147
- The waveform displays instantly on load without downloading and decoding the audio file.
163
+ Generate JSON files with [WaveformGen](https://github.com/arraypress/waveform-gen):
164
+
165
+ ```bash
166
+ npx @arraypress/waveform-gen ./audio/*.mp3 --output ./waveforms/ --bpm
167
+ ```
168
+
169
+ JSON waveform files can also include markers, which are loaded automatically if none are set via data attributes:
170
+
171
+ ```json
172
+ {
173
+ "peaks": [
174
+ 0.12,
175
+ 0.45,
176
+ 0.89,
177
+ 0.34,
178
+ ...
179
+ ],
180
+ "markers": [
181
+ {
182
+ "time": 0,
183
+ "label": "Intro"
184
+ },
185
+ {
186
+ "time": 30,
187
+ "label": "Chorus"
188
+ }
189
+ ]
190
+ }
191
+ ```
148
192
 
149
193
  ### DJ Mode Markers
150
194
 
@@ -166,32 +210,31 @@ and metadata at each marker boundary.
166
210
  </div>
167
211
  ```
168
212
 
169
- Markers can include: `time` (seconds, required), `label` (required), `title`, `artist`, `artwork`, `bpm`, `key`,
170
- `color`.
213
+ Markers can include: `time` (seconds, required), `label` (required), `title`, `artist`, `artwork`, `bpm`, `key`,`color`.
171
214
 
172
215
  The active marker on the waveform gently pulses to indicate the current section.
173
216
 
174
217
  ### Attribute Reference
175
218
 
176
- | Attribute | Description |
177
- |---------------------|------------------------------------------------------------|
178
- | `data-wb-play` | Makes element a play trigger (click to play) |
179
- | `data-wb-queue` | Makes element a queue trigger (click to add to queue) |
180
- | `data-url` | Audio file URL (required) |
181
- | `data-id` | Unique track identifier (defaults to URL) |
182
- | `data-title` | Track title |
183
- | `data-artist` | Artist or subtitle |
184
- | `data-artwork` | Album artwork URL |
185
- | `data-album` | Album name (for Media Session API) |
186
- | `data-link` | URL to navigate when clicking track info in the bar |
187
- | `data-duration` | Display duration string |
188
- | `data-bpm` | BPM value — displayed as a tag in the bar |
189
- | `data-key` | Musical key — displayed as a tag in the bar |
190
- | `data-meta` | JSON object of custom metadata (e.g. `'{"genre":"Trap"}'`) |
191
- | `data-wb-waveform` | Pre-generated waveform peaks JSON array |
192
- | `data-wb-markers` | JSON array of marker objects for DJ mode |
193
- | `data-wb-favorited` | `"true"` to pre-set favorite state (server-side seeding) |
194
- | `data-wb-in-cart` | `"true"` to pre-set cart state (server-side seeding) |
219
+ | Attribute | Description |
220
+ |---------------------|---------------------------------------------------------------------------|
221
+ | `data-wb-play` | Makes element a play trigger (click to play) |
222
+ | `data-wb-queue` | Makes element a queue trigger (click to add to queue) |
223
+ | `data-url` | Audio file URL (required) |
224
+ | `data-id` | Unique track identifier (defaults to URL) |
225
+ | `data-title` | Track title |
226
+ | `data-artist` | Artist or subtitle |
227
+ | `data-artwork` | Album artwork URL |
228
+ | `data-album` | Album name (for Media Session API) |
229
+ | `data-link` | URL to navigate when clicking track info in the bar |
230
+ | `data-duration` | Display duration string |
231
+ | `data-bpm` | BPM value — displayed as a tag in the bar |
232
+ | `data-key` | Musical key — displayed as a tag in the bar |
233
+ | `data-meta` | JSON object of custom metadata (e.g. `'{"genre":"Trap"}'`) |
234
+ | `data-wb-waveform` | Pre-generated waveform peaks: JSON array, CSV string, or `.json` file URL |
235
+ | `data-wb-markers` | JSON array of marker objects for DJ mode |
236
+ | `data-wb-favorited` | `"true"` to pre-set favorite state (server-side seeding) |
237
+ | `data-wb-in-cart` | `"true"` to pre-set cart state (server-side seeding) |
195
238
 
196
239
  All attributes also accept `data-wb-` prefixed versions (`data-wb-url`, `data-wb-title`, etc.) to avoid conflicts with
197
240
  other libraries.
@@ -238,11 +281,11 @@ WaveformBar.init({
238
281
  // Server-side actions (REST callbacks)
239
282
  actions: {
240
283
  favorite: {
241
- endpoint: '/api/favorites', // POST URL
284
+ endpoint: '/api/favorites',
242
285
  method: 'POST'
243
286
  },
244
287
  cart: {
245
- endpoint: '/api/cart', // POST URL
288
+ endpoint: '/api/cart',
246
289
  method: 'POST'
247
290
  }
248
291
  },
@@ -270,7 +313,6 @@ WaveformBar.init({
270
313
  ### Playback
271
314
 
272
315
  ```javascript
273
- // Play a track object
274
316
  WaveformBar.play({
275
317
  url: 'audio/song.mp3',
276
318
  title: 'My Song',
@@ -281,15 +323,12 @@ WaveformBar.play({
281
323
  link: '/products/my-song'
282
324
  });
283
325
 
284
- // Play by URL shorthand
285
326
  WaveformBar.play('audio/song.mp3');
286
-
287
- // Playback controls
288
327
  WaveformBar.togglePlay();
289
328
  WaveformBar.pause();
290
- WaveformBar.next(); // Next track (wraps if repeat: 'all')
291
- WaveformBar.previous(); // Previous track (restarts if >3s in)
292
- WaveformBar.skipTo(3); // Jump to queue index 3
329
+ WaveformBar.next();
330
+ WaveformBar.previous();
331
+ WaveformBar.skipTo(3);
293
332
  ```
294
333
 
295
334
  ### Repeat
@@ -299,102 +338,57 @@ WaveformBar.cycleRepeat(); // Cycles: off → all → one → off
299
338
  WaveformBar.setRepeat('all'); // Set directly: 'off', 'all', 'one'
300
339
  ```
301
340
 
302
- - **off** — Stops at end of queue
303
- - **all** — Loops back to first track after last; prev/next wrap around
304
- - **one** — Replays the current track indefinitely
305
-
306
341
  ### Markers / DJ Mode
307
342
 
308
343
  ```javascript
309
- // Seek to marker by index (0-based) on current track
310
344
  WaveformBar.seekToMarker(3);
311
-
312
- // Seek to marker by label name on current track
313
345
  WaveformBar.seekToMarkerByLabel('Horizon');
314
346
  ```
315
347
 
316
- Both methods auto-play if the player is paused. They only work on the currently loaded track's markers — if the track
317
- with markers isn't loaded, they silently return.
318
-
319
348
  ### Volume
320
349
 
321
350
  ```javascript
322
- WaveformBar.setVolume(0.5); // Set to 50%
323
- WaveformBar.getVolume(); // Returns 0.5
351
+ WaveformBar.setVolume(0.5);
352
+ WaveformBar.getVolume();
324
353
  WaveformBar.toggleMute();
325
- WaveformBar.isMutedState(); // true/false
326
354
  ```
327
355
 
328
- Volume and mute state persist in localStorage across sessions.
329
-
330
356
  ### Queue
331
357
 
332
358
  ```javascript
333
- // Add to end of queue
334
359
  WaveformBar.addToQueue({url: 'track.mp3', title: 'Next Up', artist: 'Someone'});
335
-
336
- // Remove by index
337
360
  WaveformBar.removeFromQueue(2);
338
-
339
- // Replay a previously played track (by URL)
340
- WaveformBar.replay('audio/old-track.mp3');
341
-
342
- // Clear upcoming tracks
343
361
  WaveformBar.clearQueue();
344
-
345
- // Clear play history
346
- WaveformBar.clearHistory();
347
362
  ```
348
363
 
349
- The queue panel shows three sections: **Now Playing** (click to toggle play/pause), **Up Next**, and **Previously Played
350
- **. Tracks can be removed individually from the queue via the × button on hover.
351
-
352
- ### Favorites
364
+ ### Favorites & Cart
353
365
 
354
366
  ```javascript
355
- WaveformBar.toggleFavorite(); // Toggle current track's favorite state
356
- WaveformBar.isFavorited('beat-001'); // Check by track ID
367
+ WaveformBar.toggleFavorite();
368
+ WaveformBar.isFavorited('beat-001');
369
+ WaveformBar.addToCart();
370
+ WaveformBar.isInCart('beat-001');
357
371
  ```
358
372
 
359
- Favorites persist in localStorage. Server-side state can be seeded via `data-wb-favorited="true"` on trigger elements —
360
- this is authoritative and overrides localStorage on page load.
361
-
362
- When a track is favorited/unfavorited, the `.wb-favorited` class is toggled on all matching trigger elements on the
363
- page.
364
-
365
- ### Cart
366
-
367
- ```javascript
368
- WaveformBar.addToCart(); // Add current track to cart
369
- WaveformBar.isInCart('beat-001'); // Check by track ID
370
- ```
371
-
372
- Cart state is NOT persisted to localStorage — it's server-authoritative. Seed from your server via
373
- `data-wb-in-cart="true"`. The `.wb-in-cart` class is toggled on matching trigger elements.
374
-
375
- If an `actions.cart.endpoint` is configured, a POST request fires automatically with the track data.
376
-
377
373
  ### State
378
374
 
379
375
  ```javascript
380
- WaveformBar.getCurrentTrack(); // { url, title, artist, bpm, key, ... }
381
- WaveformBar.isCurrentlyPlaying('song.mp3'); // true if this URL is actively playing
382
- WaveformBar.isCurrentTrack('song.mp3'); // true if current (playing or paused)
383
- WaveformBar.getQueue(); // Full queue array
384
- WaveformBar.getHistory(); // Previously played tracks
385
- WaveformBar.getPlayer(); // Underlying WaveformPlayer instance
376
+ WaveformBar.getCurrentTrack();
377
+ WaveformBar.isCurrentlyPlaying('song.mp3');
378
+ WaveformBar.isCurrentTrack('song.mp3');
379
+ WaveformBar.getQueue();
380
+ WaveformBar.getCurrentIndex();
381
+ WaveformBar.getPlayer();
386
382
  ```
387
383
 
388
384
  ### UI
389
385
 
390
386
  ```javascript
391
- WaveformBar.show(); // Show the bar
392
- WaveformBar.hide(); // Hide the bar
393
- WaveformBar.toggleQueuePanel(); // Toggle queue panel visibility
394
- WaveformBar.toggleVolumePopup(); // Toggle volume popup
395
- WaveformBar.closeQueuePanel();
396
- WaveformBar.closeVolumePopup();
397
- WaveformBar.destroy(); // Remove bar and clean up
387
+ WaveformBar.show();
388
+ WaveformBar.hide();
389
+ WaveformBar.toggleQueuePanel();
390
+ WaveformBar.toggleVolumePopup();
391
+ WaveformBar.destroy();
398
392
  ```
399
393
 
400
394
  ## DOM Events
@@ -405,10 +399,6 @@ All events bubble from the bar element and are prefixed with `waveformbar:`.
405
399
  document.addEventListener('waveformbar:play', (e) => {
406
400
  console.log('Playing:', e.detail.track);
407
401
  });
408
-
409
- document.addEventListener('waveformbar:trackchange', (e) => {
410
- console.log('Track changed:', e.detail.track, 'Index:', e.detail.index);
411
- });
412
402
  ```
413
403
 
414
404
  | Event | Detail |
@@ -434,16 +424,10 @@ WaveformBar automatically adds CSS classes to trigger elements on the page:
434
424
  | `.wb-favorited` | The element's track is favorited |
435
425
  | `.wb-in-cart` | The element's track is in the cart |
436
426
 
437
- These update in real-time as the user interacts with the bar.
438
-
439
427
  ## Helper CSS Classes
440
428
 
441
- Ready-made utility classes for common UI patterns. Add them to your trigger elements.
442
-
443
429
  ### Icon Swap (`wb-icon-swap`)
444
430
 
445
- Swaps play/pause content automatically based on playback state.
446
-
447
431
  ```html
448
432
 
449
433
  <button data-wb-play data-url="song.mp3" class="wb-icon-swap">
@@ -454,100 +438,74 @@ Swaps play/pause content automatically based on playback state.
454
438
 
455
439
  ### Equalizer Bars (`wb-eq-bars`)
456
440
 
457
- Animated equalizer bars that activate when the track is playing.
458
-
459
441
  ```html
460
-
461
- <tr data-wb-play data-url="song.mp3">
462
- <td>
463
- <span class="wb-eq-bars">
464
- <span></span><span></span><span></span><span></span>
465
- </span>
466
- </td>
467
- <td>Track Title</td>
468
- </tr>
442
+ <span class="wb-eq-bars"><span></span><span></span><span></span><span></span></span>
469
443
  ```
470
444
 
471
445
  ### Card Highlight (`wb-card-highlight`)
472
446
 
473
- Adds accent border and glow when the element's track is current.
474
-
475
- ```html
476
-
477
- <div data-wb-play data-url="song.mp3"
478
- class="wb-card-highlight"
479
- style="border: 1px solid transparent; border-radius: 8px;">
480
- <h3>Track Title</h3>
481
- </div>
482
- ```
447
+ Adds accent border when the element's track is current.
483
448
 
484
449
  ### Accent Text (`wb-accent-current`)
485
450
 
486
451
  Colors text with the accent color when the parent track is current.
487
452
 
488
- ```html
489
-
490
- <div data-wb-play data-url="song.mp3">
491
- <span class="wb-accent-current">Track Title</span>
492
- </div>
493
- ```
494
-
495
- ### Pulse Animation (`wb-pulse-playing`)
496
-
497
- Subtle opacity pulse on playing elements.
453
+ ### Favorite/Cart Visibility
498
454
 
499
455
  ```html
500
-
501
- <div data-wb-play data-url="song.mp3" class="wb-pulse-playing">
502
- <img src="cover.jpg">
503
- </div>
456
+ <span class="wb-hide-if-fav">♡ Save</span>
457
+ <span class="wb-show-if-fav">❤ Saved</span>
458
+ <span class="wb-hide-if-cart">🛒 Add</span>
459
+ <span class="wb-show-if-cart">✓ In Cart</span>
504
460
  ```
505
461
 
506
- ### Favorite/Cart Visibility
462
+ ## Page Icons
507
463
 
508
- Show or hide content based on favorite/cart state.
464
+ The optional `waveform-bar-icons.css` provides a lightweight SVG icon set for use in your page elements (play/pause
465
+ overlays, hearts, carts, etc.):
509
466
 
510
467
  ```html
511
468
 
512
- <div data-wb-play data-url="song.mp3" data-id="beat-001">
513
- <button>
514
- <span class="wb-hide-if-fav">♡ Save</span>
515
- <span class="wb-show-if-fav">❤ Saved</span>
516
- </button>
517
- <button>
518
- <span class="wb-hide-if-cart">🛒 Add to Cart</span>
519
- <span class="wb-show-if-cart">✓ In Cart</span>
520
- </button>
521
- </div>
522
- ```
469
+ <link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-bar@latest/dist/waveform-bar-icons.css">
470
+
471
+ <span class="wbi wbi-play"></span>
472
+ <span class="wbi wbi-pause"></span>
473
+ <span class="wbi wbi-heart"></span>
474
+ <span class="wbi wbi-heart-filled"></span>
475
+ <span class="wbi wbi-cart"></span>
476
+ <span class="wbi wbi-cart-check"></span>
477
+ <span class="wbi wbi-queue"></span>
478
+ <span class="wbi wbi-skip-back"></span>
479
+ <span class="wbi wbi-skip-forward"></span>
480
+ <span class="wbi wbi-volume-high"></span>
481
+ <span class="wbi wbi-volume-low"></span>
482
+ <span class="wbi wbi-volume-mute"></span>
483
+ <span class="wbi wbi-repeat"></span>
484
+ <span class="wbi wbi-repeat-one"></span>
485
+ <span class="wbi wbi-close"></span>
486
+ <span class="wbi wbi-check"></span>
487
+ <span class="wbi wbi-link"></span>
488
+ <span class="wbi wbi-download"></span>
489
+ <span class="wbi wbi-share"></span>
490
+ <span class="wbi wbi-music-note"></span>
491
+ ```
492
+
493
+ Icons inherit `color` from their parent and scale with `font-size`. Size variants: `.wbi-sm`, `.wbi-lg`, `.wbi-xl`,
494
+ `.wbi-2x`, `.wbi-3x`.
495
+
496
+ These are for your page elements only — the bar itself uses its own internal SVG icons and does not require this file.
523
497
 
524
498
  ## Persistence
525
499
 
526
- WaveformBar uses two storage mechanisms:
527
-
528
- **sessionStorage** (cleared when browser closes):
500
+ **sessionStorage** (cleared when browser closes): queue, current track index, playback position, playing state.
529
501
 
530
- - Queue contents and order
531
- - Current track index
532
- - Playback position
533
- - Playing state
534
-
535
- **localStorage** (persists across sessions):
536
-
537
- - Volume level
538
- - Mute state
539
- - Favorite track IDs
540
-
541
- When a user navigates between pages, the bar restores the queue, seeks to the saved position, and optionally resumes
542
- playback (if `autoResume: true`). Position is saved on every page unload for accuracy.
502
+ **localStorage** (persists across sessions): volume level, mute state, favorite track IDs.
543
503
 
544
504
  Cart state is intentionally NOT persisted — it seeds from `data-wb-in-cart` attributes on page load, making the server
545
505
  the source of truth.
546
506
 
547
507
  ## Custom Styling
548
508
 
549
- Override CSS custom properties to theme the bar:
550
-
551
509
  ```css
552
510
  .waveform-bar,
553
511
  .wb-queue-panel {
@@ -555,178 +513,32 @@ Override CSS custom properties to theme the bar:
555
513
  --wb-border: rgba(255, 255, 255, 0.1);
556
514
  --wb-text: #ffffff;
557
515
  --wb-text-muted: rgba(255, 255, 255, 0.5);
558
- --wb-accent: #1db954; /* e.g. Spotify green */
516
+ --wb-accent: #1db954;
559
517
  --wb-accent-light: #1ed760;
560
518
  --wb-hover: rgba(255, 255, 255, 0.08);
561
- --wb-tag-bg: rgba(29, 185, 84, 0.12);
562
- --wb-tag-text: #1db954;
563
519
  --wb-fav-color: #ef4444;
564
520
  --wb-cart-color: #4ade80;
565
521
  }
566
522
  ```
567
523
 
568
- A `.wb-light` class is available for light backgrounds:
569
-
570
- ```css
571
- .waveform-bar.wb-light {
572
- --wb-bg: rgba(255, 255, 255, 0.95);
573
- --wb-text: #1a1a1a;
574
- --wb-text-muted: rgba(0, 0, 0, 0.5);
575
- }
576
- ```
577
-
578
- ## Layout
579
-
580
- The bar uses a three-zone flex layout:
581
-
582
- ```
583
- [Left: controls + track info] — [Centre: waveform + time] — [Right: meta + actions + volume + queue]
584
- ```
585
-
586
- - **Left zone** — Prev, Play/Pause, Next, Repeat buttons + artwork + title/artist
587
- - **Centre zone** — Full-width waveform visualization + elapsed/total time
588
- - **Right zone** — BPM/key tags, favorite/cart buttons, volume popup, queue toggle
589
-
590
- On mobile (≤768px):
591
-
592
- - Left + Right share the top row
593
- - Waveform drops to a full-width second row
594
- - Meta tags, actions, and time display are hidden
595
- - Queue panel becomes full-width
596
-
597
- On small mobile (≤480px):
598
-
599
- - Prev/Next and Repeat buttons are hidden
600
- - Volume is hidden
601
- - Only play button, track info, and waveform remain
602
-
603
- ## Use Cases
604
-
605
- ### Beat Store
606
-
607
- ```html
608
-
609
- <div class="beat-card wb-card-highlight"
610
- data-wb-play
611
- data-url="beats/trap-42.mp3"
612
- data-id="beat-42"
613
- data-title="Trap Beat #42"
614
- data-artist="ProducerName"
615
- data-bpm="140"
616
- data-key="Cm"
617
- data-artwork="covers/trap-42.jpg"
618
- data-link="/beats/trap-42"
619
- data-wb-favorited="true">
620
- <img src="covers/trap-42.jpg">
621
- <h3 class="wb-accent-current">Trap Beat #42</h3>
622
- <p>140 BPM · Cm · $29.99</p>
623
- <span class="wb-eq-bars"><span></span><span></span><span></span><span></span></span>
624
- <button>
625
- <span class="wb-hide-if-fav">♡ Save</span>
626
- <span class="wb-show-if-fav">❤ Saved</span>
627
- </button>
628
- </div>
629
- <button data-wb-queue data-url="beats/trap-42.mp3" data-title="Trap Beat #42">+ Queue</button>
630
- ```
631
-
632
- ### Sample Library Table
633
-
634
- ```html
635
-
636
- <table>
637
- <tr data-wb-play
638
- data-url="samples/kick.wav"
639
- data-title="808 Kick"
640
- data-artist="Drum Kit Vol. 3"
641
- data-bpm="128"
642
- data-meta='{"format":"WAV"}'>
643
- <td><span class="wb-eq-bars"><span></span><span></span><span></span><span></span></span></td>
644
- <td class="wb-accent-current">808 Kick</td>
645
- <td>128 BPM</td>
646
- <td>WAV</td>
647
- </tr>
648
- </table>
649
- ```
650
-
651
- ### DJ Mix with Clickable Tracklist
652
-
653
- ```html
654
- <!-- Mix trigger with markers -->
655
- <div data-wb-play
656
- data-url="mixes/guestmix.mp3"
657
- data-title="Guest Mix"
658
- data-artist="Various Artists"
659
- data-wb-waveform='[0.1,0.3,0.5,...]'
660
- data-wb-markers='[
661
- {"time":0,"label":"Intro","title":"Opening","artist":"DJ One"},
662
- {"time":180,"label":"Drop","title":"Big Tune","artist":"Producer X"},
663
- {"time":360,"label":"Chill","title":"Wind Down","artist":"Ambient Co"}
664
- ]'>
665
- 🎧 Play Guest Mix
666
- </div>
667
-
668
- <!-- Clickable tracklist buttons -->
669
- <button onclick="WaveformBar.seekToMarkerByLabel('Intro')">00:00 — DJ One — Opening</button>
670
- <button onclick="WaveformBar.seekToMarkerByLabel('Drop')">03:00 — Producer X — Big Tune</button>
671
- <button onclick="WaveformBar.seekToMarkerByLabel('Chill')">06:00 — Ambient Co — Wind Down</button>
672
- ```
673
-
674
- ### Podcast
675
-
676
- ```html
677
-
678
- <button data-wb-play
679
- data-url="episodes/ep42.mp3"
680
- data-title="Ep 42: The Future of AI"
681
- data-artist="with Dr. Sarah Chen"
682
- data-artwork="ep42-cover.jpg"
683
- data-link="/episodes/42"
684
- class="wb-icon-swap">
685
- <span class="wb-show-play">▶ Listen</span>
686
- <span class="wb-show-pause">⏸ Playing</span>
687
- </button>
688
- ```
689
-
690
- ## Pre-Generating Waveforms
691
-
692
- Use [@arraypress/waveform-gen](https://github.com/arraypress/waveform-gen) to batch-generate waveform data:
693
-
694
- ```bash
695
- # Generate JSON files for all audio
696
- npx @arraypress/waveform-gen ./audio/*.mp3 --output ./waveforms/
697
-
698
- # Generate ready-to-paste HTML markup
699
- npx @arraypress/waveform-gen ./audio/*.mp3 --format html --output ./
700
- ```
701
-
702
- Then reference the data in your HTML:
703
-
704
- ```html
705
-
706
- <div data-wb-play
707
- data-url="song.mp3"
708
- data-title="My Song"
709
- data-wb-waveform='[0.12,0.45,0.89,0.34,0.67]'>
710
- </div>
711
- ```
712
-
713
524
  ## Browser Support
714
525
 
715
- - Chrome/Edge 90+
716
- - Firefox 88+
717
- - Safari 14+
718
- - Mobile browsers (iOS Safari, Chrome Android)
526
+ Chrome/Edge 90+, Firefox 88+, Safari 14+, iOS Safari, Chrome Android
719
527
 
720
528
  ## Dependencies
721
529
 
722
- - [WaveformPlayer](https://github.com/arraypress/waveform-player) ≥1.3.5 — must be loaded before WaveformBar
530
+ - [WaveformPlayer](https://github.com/arraypress/waveform-player) ≥1.5.0 — must be loaded before WaveformBar
723
531
 
724
- ## License
532
+ ## Ecosystem
725
533
 
726
- MIT © [ArrayPress](https://github.com/arraypress)
534
+ | Package | Description |
535
+ |-------------------------------------------------------------------------|-------------------------------------------------------------------|
536
+ | **[WaveformPlayer](https://github.com/arraypress/waveform-player)** | Core audio player with waveform visualization |
537
+ | **[WaveformBar](https://github.com/arraypress/waveform-bar)** | Persistent bottom-bar player with queue, favorites, cart, DJ mode |
538
+ | **[WaveformGen](https://github.com/arraypress/waveform-generator)** | CLI tool to pre-generate waveform JSON from audio files |
539
+ | **[WaveformPlaylist](https://github.com/arraypress/waveform-playlist)** | Playlist and chapter support addon |
540
+ | **[WaveformTracker](https://github.com/arraypress/waveform-tracker)** | Audio engagement analytics |
727
541
 
728
- ## Related
542
+ ## License
729
543
 
730
- - [WaveformPlayer](https://github.com/arraypress/waveform-player) — Core audio player with waveform visualization
731
- - [WaveformPlaylist](https://github.com/arraypress/waveform-playlist) — On-page playlist and chapter navigation
732
- - [WaveformGen](https://github.com/arraypress/waveform-generator) — CLI tool for batch waveform data generation
544
+ MIT © [ArrayPress](https://github.com/arraypress)
@@ -5,12 +5,21 @@
5
5
  * MIT License
6
6
  *
7
7
  * Usage:
8
- * <span class="wbi wbi-play"></span>
8
+ * <span class="wbi wbi-play"></span>
9
9
  */
10
10
 
11
- .wbi-heart-filled {
12
- -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
13
- mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
11
+ .wbi {
12
+ display: inline-block;
13
+ width: 1em;
14
+ height: 1em;
15
+ background-color: currentColor;
16
+ -webkit-mask-size: contain;
17
+ mask-size: contain;
18
+ -webkit-mask-repeat: no-repeat;
19
+ mask-repeat: no-repeat;
20
+ -webkit-mask-position: center;
21
+ mask-position: center;
22
+ vertical-align: -0.125em;
14
23
  }
15
24
 
16
25
  .wbi-sm {
@@ -136,4 +145,4 @@
136
145
  .wbi-share {
137
146
  -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
138
147
  mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
139
- }
148
+ }
@@ -317,8 +317,6 @@ var DEFAULTS = {
317
317
  waveformColor: null,
318
318
  progressColor: null,
319
319
  markerColor: "rgba(255, 255, 255, 0.25)",
320
- configPath: null,
321
- // Directory for auto-resolved config JSON files (e.g. 'waveforms/')
322
320
  volume: 1,
323
321
  storageKey: "waveform-bar",
324
322
  actions: null,
@@ -976,12 +974,6 @@ var WaveformBar = class {
976
974
  if (track.waveform) {
977
975
  loadOpts.waveform = track.waveform;
978
976
  }
979
- if (this.config.configPath && track.url) {
980
- const audioFile = track.url.split("/").pop().split("?")[0];
981
- const jsonFile = audioFile.replace(/\.[^.]+$/, ".json");
982
- const path = this.config.configPath.replace(/\/?$/, "/");
983
- loadOpts.config = path + jsonFile;
984
- }
985
977
  if (track.markers && track.markers.length) {
986
978
  const defaultColor = this.config.markerColor;
987
979
  loadOpts.markers = track.markers.map((m) => ({
@@ -1322,12 +1314,6 @@ var WaveformBar = class {
1322
1314
  if (track.waveform) {
1323
1315
  this.player.options.waveform = track.waveform;
1324
1316
  }
1325
- if (this.config.configPath && track.url) {
1326
- const audioFile = track.url.split("/").pop().split("?")[0];
1327
- const jsonFile = audioFile.replace(/\.[^.]+$/, ".json");
1328
- const path = this.config.configPath.replace(/\/?$/, "/");
1329
- this.player.options.config = path + jsonFile;
1330
- }
1331
1317
  this.player.options.title = track.title || "";
1332
1318
  this.player.options.subtitle = track.artist || "";
1333
1319
  if (track.markers && track.markers.length) {
@@ -318,8 +318,6 @@
318
318
  waveformColor: null,
319
319
  progressColor: null,
320
320
  markerColor: "rgba(255, 255, 255, 0.25)",
321
- configPath: null,
322
- // Directory for auto-resolved config JSON files (e.g. 'waveforms/')
323
321
  volume: 1,
324
322
  storageKey: "waveform-bar",
325
323
  actions: null,
@@ -977,12 +975,6 @@
977
975
  if (track.waveform) {
978
976
  loadOpts.waveform = track.waveform;
979
977
  }
980
- if (this.config.configPath && track.url) {
981
- const audioFile = track.url.split("/").pop().split("?")[0];
982
- const jsonFile = audioFile.replace(/\.[^.]+$/, ".json");
983
- const path = this.config.configPath.replace(/\/?$/, "/");
984
- loadOpts.config = path + jsonFile;
985
- }
986
978
  if (track.markers && track.markers.length) {
987
979
  const defaultColor = this.config.markerColor;
988
980
  loadOpts.markers = track.markers.map((m) => ({
@@ -1323,12 +1315,6 @@
1323
1315
  if (track.waveform) {
1324
1316
  this.player.options.waveform = track.waveform;
1325
1317
  }
1326
- if (this.config.configPath && track.url) {
1327
- const audioFile = track.url.split("/").pop().split("?")[0];
1328
- const jsonFile = audioFile.replace(/\.[^.]+$/, ".json");
1329
- const path = this.config.configPath.replace(/\/?$/, "/");
1330
- this.player.options.config = path + jsonFile;
1331
- }
1332
1318
  this.player.options.title = track.title || "";
1333
1319
  this.player.options.subtitle = track.artist || "";
1334
1320
  if (track.markers && track.markers.length) {
@@ -1,4 +1,4 @@
1
- (()=>{var n={play:'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',pause:'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',prev:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>',next:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>',queue:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>',music:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" opacity="0.5"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>',volHigh:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>',volLow:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>',volMute:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>',heart:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',heartFilled:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',cart:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>',close:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',speaker:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>',repeatOff:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>',repeatAll:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>',repeatOne:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/><text x="12" y="15" text-anchor="middle" font-size="7" font-weight="bold" fill="currentColor">1</text></svg>'};function d(s){return s?s.split("/").pop().split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,t=>t.toUpperCase()):"Untitled"}function u(s){if(!s)return"";let t=document.createElement("div");return t.textContent=s,t.innerHTML}function v(s){if(!s||isNaN(s))return"0:00";let t=Math.floor(s/60),e=Math.floor(s%60);return`${t}:${e.toString().padStart(2,"0")}`}function p(s){let t=s.dataset.wbUrl||s.dataset.url;if(!t)return null;let e={};try{e=JSON.parse(s.dataset.wbMeta||s.dataset.meta||"{}")}catch{}let i=null;try{i=JSON.parse(s.dataset.wbMarkers||s.dataset.markers||"null")}catch{}return{url:t,id:s.dataset.wbId||s.dataset.id||t,title:s.dataset.wbTitle||s.dataset.title||d(t),artist:s.dataset.wbArtist||s.dataset.artist||"",artwork:s.dataset.wbArtwork||s.dataset.artwork||"",album:s.dataset.wbAlbum||s.dataset.album||"",link:s.dataset.wbLink||s.dataset.link||"",duration:s.dataset.wbDuration||s.dataset.duration||"",bpm:s.dataset.wbBpm||s.dataset.bpm||"",key:s.dataset.wbKey||s.dataset.key||"",waveform:s.dataset.wbWaveform||s.dataset.waveform||"",markers:i,favorited:s.dataset.wbFavorited==="true",inCart:s.dataset.wbInCart==="true",meta:e}}function g(s,t){try{sessionStorage.setItem(s,JSON.stringify(t))}catch{}}function w(s){try{let t=sessionStorage.getItem(s);if(!t)return null;let e=JSON.parse(t);return!e||!e.queue||!e.queue.length?null:e}catch{return sessionStorage.removeItem(s),null}}function y(s,t,e,i){try{localStorage.setItem(s+"-vol",JSON.stringify({v:t,m:e,b:i}))}catch{}}function E(s){try{let t=JSON.parse(localStorage.getItem(s+"-vol"));return t?{volume:t.v!=null?t.v:1,muted:t.m||!1,volumeBeforeMute:t.b||1}:null}catch{return null}}function m(s,t){try{localStorage.setItem(s+"-favs",JSON.stringify([...t]))}catch{}}function _(s){try{let t=JSON.parse(localStorage.getItem(s+"-favs"));return Array.isArray(t)?new Set(t):new Set}catch{return new Set}}function b(s,t){if(!(!s||!s.endpoint)){if(typeof s.endpoint=="function"){try{s.endpoint(t)}catch(e){console.warn("WaveformBar action callback error:",e)}return}typeof s.endpoint=="string"&&fetch(s.endpoint,{method:s.method||"POST",headers:{"Content-Type":"application/json",...s.headers||{}},body:JSON.stringify(t)}).catch(e=>console.warn("WaveformBar action request failed:",e))}}function k(s){let t='<div class="wb-left">';t+='<div class="wb-controls">',s.showPrevNext&&(t+=`<button class="wb-btn wb-prev" aria-label="Previous" title="Previous">${n.prev}</button>`),t+=`<button class="wb-btn wb-play" aria-label="Play/Pause" title="Play">
1
+ (()=>{var n={play:'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',pause:'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg>',prev:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>',next:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>',queue:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>',music:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" opacity="0.5"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>',volHigh:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>',volLow:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>',volMute:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>',heart:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',heartFilled:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',cart:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>',close:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',speaker:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>',repeatOff:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>',repeatAll:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>',repeatOne:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/><text x="12" y="15" text-anchor="middle" font-size="7" font-weight="bold" fill="currentColor">1</text></svg>'};function d(s){return s?s.split("/").pop().split(".")[0].replace(/[-_]/g," ").replace(/\b\w/g,t=>t.toUpperCase()):"Untitled"}function u(s){if(!s)return"";let t=document.createElement("div");return t.textContent=s,t.innerHTML}function v(s){if(!s||isNaN(s))return"0:00";let t=Math.floor(s/60),e=Math.floor(s%60);return`${t}:${e.toString().padStart(2,"0")}`}function m(s){let t=s.dataset.wbUrl||s.dataset.url;if(!t)return null;let e={};try{e=JSON.parse(s.dataset.wbMeta||s.dataset.meta||"{}")}catch{}let i=null;try{i=JSON.parse(s.dataset.wbMarkers||s.dataset.markers||"null")}catch{}return{url:t,id:s.dataset.wbId||s.dataset.id||t,title:s.dataset.wbTitle||s.dataset.title||d(t),artist:s.dataset.wbArtist||s.dataset.artist||"",artwork:s.dataset.wbArtwork||s.dataset.artwork||"",album:s.dataset.wbAlbum||s.dataset.album||"",link:s.dataset.wbLink||s.dataset.link||"",duration:s.dataset.wbDuration||s.dataset.duration||"",bpm:s.dataset.wbBpm||s.dataset.bpm||"",key:s.dataset.wbKey||s.dataset.key||"",waveform:s.dataset.wbWaveform||s.dataset.waveform||"",markers:i,favorited:s.dataset.wbFavorited==="true",inCart:s.dataset.wbInCart==="true",meta:e}}function g(s,t){try{sessionStorage.setItem(s,JSON.stringify(t))}catch{}}function w(s){try{let t=sessionStorage.getItem(s);if(!t)return null;let e=JSON.parse(t);return!e||!e.queue||!e.queue.length?null:e}catch{return sessionStorage.removeItem(s),null}}function y(s,t,e,i){try{localStorage.setItem(s+"-vol",JSON.stringify({v:t,m:e,b:i}))}catch{}}function E(s){try{let t=JSON.parse(localStorage.getItem(s+"-vol"));return t?{volume:t.v!=null?t.v:1,muted:t.m||!1,volumeBeforeMute:t.b||1}:null}catch{return null}}function p(s,t){try{localStorage.setItem(s+"-favs",JSON.stringify([...t]))}catch{}}function _(s){try{let t=JSON.parse(localStorage.getItem(s+"-favs"));return Array.isArray(t)?new Set(t):new Set}catch{return new Set}}function b(s,t){if(!(!s||!s.endpoint)){if(typeof s.endpoint=="function"){try{s.endpoint(t)}catch(e){console.warn("WaveformBar action callback error:",e)}return}typeof s.endpoint=="string"&&fetch(s.endpoint,{method:s.method||"POST",headers:{"Content-Type":"application/json",...s.headers||{}},body:JSON.stringify(t)}).catch(e=>console.warn("WaveformBar action request failed:",e))}}function k(s){let t='<div class="wb-left">';t+='<div class="wb-controls">',s.showPrevNext&&(t+=`<button class="wb-btn wb-prev" aria-label="Previous" title="Previous">${n.prev}</button>`),t+=`<button class="wb-btn wb-play" aria-label="Play/Pause" title="Play">
2
2
  <span class="wb-icon-play">${n.play}</span>
3
3
  <span class="wb-icon-pause" style="display:none">${n.pause}</span>
4
4
  </button>`,s.showPrevNext&&(t+=`<button class="wb-btn wb-next" aria-label="Next" title="Next">${n.next}</button>`),s.showRepeat&&(t+=`<button class="wb-btn wb-btn-sm wb-repeat" aria-label="Repeat" title="Repeat: Off">${n.repeatOff}</button>`),t+="</div>",t+=`<div class="wb-track">
@@ -41,7 +41,7 @@
41
41
  <div class="wb-queue-item-title">${u(h.title)}</div>
42
42
  <div class="wb-queue-item-artist">${u(h.artist)}</div>
43
43
  </div>
44
- </div>`}}s.innerHTML=l,s.querySelectorAll(".wb-queue-item[data-qi]").forEach(o=>{o.addEventListener("click",h=>{h.target.closest(".wb-queue-remove")||r.onSkipTo&&r.onSkipTo(parseInt(o.dataset.qi))})}),s.querySelectorAll(".wb-queue-remove").forEach(o=>{o.addEventListener("click",h=>{h.stopPropagation(),r.onRemove&&r.onRemove(parseInt(o.dataset.qi))})})}var x={persist:!0,autoResume:!0,continuous:!0,repeat:"off",showRepeat:!0,showQueue:!0,showPrevNext:!0,showVolume:!0,showMute:!0,showMeta:!0,showTime:!0,showTrackLink:!0,maxMeta:3,defaultArtwork:null,theme:null,waveformStyle:"mirror",waveformHeight:32,barWidth:2,barSpacing:0,waveformColor:null,progressColor:null,markerColor:"rgba(255, 255, 255, 0.25)",configPath:null,volume:1,storageKey:"waveform-bar",actions:null,onPlay:null,onPause:null,onTrackChange:null,onQueueChange:null,onVolumeChange:null,onFavorite:null,onCart:null},f=class{constructor(){this.config=null,this.player=null,this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.isInitialized=!1,this.queueOpen=!1,this.volume=1,this.isMuted=!1,this._volumeBeforeMute=1,this._lastPosition=0,this._favorites=new Set,this._cartItems=new Set,this._observer=null,this._activeMarkers=null,this._currentMarkerIndex=-1,this.repeat="off",this.barEl=null,this.queueEl=null,this.waveformContainer=null,this.volumePopupEl=null,this.titleEl=null,this.artistEl=null,this.metaEl=null,this.playBtnEl=null,this.repeatBtnEl=null,this.queueBtnEl=null,this.queueBodyEl=null,this.queueCountEl=null,this.volumeSliderEl=null,this.muteBtnEl=null,this.favBtnEl=null,this.cartBtnEl=null,this.timeCurrentEl=null,this.timeTotalEl=null}init(t={}){return this.isInitialized&&this.destroy(),this.config={...x,...t},this.volume=this.config.volume,typeof window.WaveformPlayer>"u"?(console.error("WaveformBar: WaveformPlayer is required."),this):(this._createBar(),this._createQueue(),this._initPlayer(),this._bindTriggers(),this._observeDOM(),this.config.persist&&(this._restoreVolume(),this._restoreFavorites()),this._seedFromAttributes(),this.config.persist&&this._restoreState(),this.isInitialized=!0,this._beforeUnloadHandler=()=>this._saveState(),window.addEventListener("beforeunload",this._beforeUnloadHandler),this)}destroy(){return this.player&&(this.player.destroy(),this.player=null),this.barEl&&(this.barEl.remove(),this.barEl=null),this.queueEl&&(this.queueEl.remove(),this.queueEl=null),this._observer&&(this._observer.disconnect(),this._observer=null),this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),document.querySelectorAll("[data-wb-play],[data-wb-queue]").forEach(t=>delete t._wbBound),document.querySelectorAll(".wb-current,.wb-playing").forEach(t=>t.classList.remove("wb-current","wb-playing")),this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.queueOpen=!1,this.isInitialized=!1,this.config=null,this}_createBar(){this.barEl=document.createElement("div"),this.barEl.className="waveform-bar";let t=this.config.theme||this._detectTheme();t==="light"&&this.barEl.classList.add("wb-light"),this._resolvedTheme=t,this.barEl.id="waveform-bar",this.barEl.innerHTML=k(this.config),document.body.appendChild(this.barEl),this.titleEl=this.barEl.querySelector(".wb-title"),this.artistEl=this.barEl.querySelector(".wb-artist"),this.metaEl=this.barEl.querySelector(".wb-meta"),this.playBtnEl=this.barEl.querySelector(".wb-play"),this.waveformContainer=this.barEl.querySelector(".wb-waveform-container"),this.queueBtnEl=this.barEl.querySelector(".wb-queue-btn"),this.muteBtnEl=this.barEl.querySelector(".wb-mute"),this.volumeSliderEl=this.barEl.querySelector(".wb-volume-slider"),this.favBtnEl=this.barEl.querySelector(".wb-fav"),this.cartBtnEl=this.barEl.querySelector(".wb-cart"),this.timeCurrentEl=this.barEl.querySelector(".wb-time-current"),this.timeTotalEl=this.barEl.querySelector(".wb-time-total"),this.playBtnEl.addEventListener("click",()=>this.togglePlay());let e=this.barEl.querySelector(".wb-prev"),i=this.barEl.querySelector(".wb-next");e&&e.addEventListener("click",()=>this.previous()),i&&i.addEventListener("click",()=>this.next()),this.repeatBtnEl=this.barEl.querySelector(".wb-repeat"),this.repeatBtnEl&&(this.repeat=this.config.repeat||"off",this._updateRepeatButton(),this.repeatBtnEl.addEventListener("click",()=>this.cycleRepeat())),this.queueBtnEl&&this.queueBtnEl.addEventListener("click",()=>this.toggleQueuePanel()),this.volumePopupEl=this.barEl.querySelector(".wb-volume-popup");let r=this.barEl.querySelector(".wb-volume");if(this.muteBtnEl&&this.muteBtnEl.addEventListener("click",a=>{a.stopPropagation(),this.toggleMute()}),r&&this.volumePopupEl){let a;r.addEventListener("mouseenter",()=>{clearTimeout(a),this.openVolumePopup()}),r.addEventListener("mouseleave",()=>{a=setTimeout(()=>this.closeVolumePopup(),300)})}this.volumeSliderEl&&this.volumeSliderEl.addEventListener("input",a=>{a.stopPropagation(),this.setVolume(parseInt(a.target.value)/100)}),document.addEventListener("click",a=>{this.volumePopupEl?.classList.contains("wb-volume-open")&&!this.barEl.querySelector(".wb-volume")?.contains(a.target)&&this.closeVolumePopup()}),this.favBtnEl&&this.favBtnEl.addEventListener("click",()=>this.toggleFavorite()),this.cartBtnEl&&this.cartBtnEl.addEventListener("click",()=>this.addToCart()),this.config.showTrackLink&&this.barEl.querySelector(".wb-track").addEventListener("click",()=>{let a=this.getCurrentTrack();a&&a.link&&(window.location.href=a.link)})}_createQueue(){this.config.showQueue&&(this.queueEl=q(),this._resolvedTheme==="light"&&this.queueEl.classList.add("wb-light"),document.body.appendChild(this.queueEl),this.queueBodyEl=this.queueEl.querySelector(".wb-queue-body"),this.queueCountEl=this.queueEl.querySelector(".wb-queue-count"),this.queueEl.querySelector(".wb-queue-clear").addEventListener("click",()=>this.clearQueue()),document.addEventListener("click",t=>{this.queueOpen&&!this.queueEl.contains(t.target)&&!this.queueBtnEl.contains(t.target)&&this.closeQueuePanel()}))}_initPlayer(){let t={showControls:!1,showInfo:!1,waveformStyle:this.config.waveformStyle,height:this.config.waveformHeight,barWidth:this.config.barWidth,barSpacing:this.config.barSpacing,singlePlay:!1,onPlay:()=>{this.isPlaying=!0,this._updatePlayButton(),this._syncPageState();let e=this.getCurrentTrack();this._emit("play",{track:e}),this.config.onPlay&&this.config.onPlay(e)},onPause:()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this._saveState();let e=this.getCurrentTrack();this._emit("pause",{track:e}),this.config.onPause&&this.config.onPause(e)},onEnd:()=>{if(this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.repeat==="one"){this.player&&(this.player.seekTo(0),this.player.play().catch(()=>{}));return}this.config.continuous&&this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack())},onTimeUpdate:(e,i)=>{this._lastPosition=e,this.timeCurrentEl&&(this.timeCurrentEl.textContent=v(e)),this.timeTotalEl&&(this.timeTotalEl.textContent=v(i)),(!this._lastSaveTime||e-this._lastSaveTime>2)&&(this._lastSaveTime=e,this._saveState()),this._activeMarkers&&this._checkMarkerBoundary(e)},onLoad:null};this.config.waveformColor&&(t.waveformColor=this.config.waveformColor),this.config.progressColor&&(t.progressColor=this.config.progressColor),this.player=new window.WaveformPlayer(this.waveformContainer,t),this.player.setVolume(this.volume)}_bindTriggers(){document.querySelectorAll("[data-wb-play]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault();let i=p(t);i&&this.play(i)}))}),document.querySelectorAll("[data-wb-queue]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation();let i=p(t);i&&this.addToQueue(i)}))})}_observeDOM(){typeof MutationObserver>"u"||(this._observer=new MutationObserver(()=>{this._bindTriggers(),this._syncPageState()}),this._observer.observe(document.body,{childList:!0,subtree:!0}))}play(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;if(!e||!e.url)return this;let i=this.getCurrentTrack();if(i&&i.url===e.url)return this.togglePlay(),this;let r=this.queue.findIndex(a=>a.url===e.url);if(r>=0)this.queue[r]={...this.queue[r],...e},this.currentIndex=r;else{let a=this.currentIndex+1;this.queue.splice(a,0,e),this.currentIndex=a}return this._loadCurrentTrack(),this}addToQueue(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;return!e||!e.url?this:this.queue.find(i=>i.url===e.url)?this:(this.queue.push(e),this._renderQueue(),this._saveState(),this._updateNavButtons(),this.currentIndex===-1&&(this.currentIndex=0,this._loadCurrentTrack()),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}togglePlay(){return this.player?(this.isPlaying?this.player.pause():this.player.play(),this):this}pause(){return this.player&&this.isPlaying&&this.player.pause(),this}next(){return this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack()),this}previous(){return this.player&&this.player.audio&&this.player.audio.currentTime>3?(this.player.seekTo(0),this):(this.currentIndex>0?(this.currentIndex--,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=this.queue.length-1,this._loadCurrentTrack()),this)}skipTo(t){return t<0||t>=this.queue.length?this:t===this.currentIndex?(this.togglePlay(),this):(this.currentIndex=t,this._loadCurrentTrack(),this)}seekToMarker(t){if(!this._activeMarkers||t<0||t>=this._activeMarkers.length)return this;let e=this._activeMarkers[t];return this.player&&(this.player.seekTo(e.time),this.isPlaying||this.togglePlay()),this}seekToMarkerByLabel(t){if(!this._activeMarkers)return this;let e=this._activeMarkers.findIndex(i=>(i.label||i.title||"").toLowerCase()===t.toLowerCase());return e>=0&&this.seekToMarker(e),this}setVolume(t){return this.volume=Math.max(0,Math.min(1,t)),this.isMuted=this.volume===0,this.player&&this.player.setVolume(this.volume),this._updateVolumeUI(),y(this.config.storageKey,this.volume,this.isMuted,this._volumeBeforeMute),this._emit("volumechange",{volume:this.volume}),this.config.onVolumeChange&&this.config.onVolumeChange(this.volume),this}getVolume(){return this.volume}toggleMute(){return this.isMuted?this.setVolume(this._volumeBeforeMute||1):(this._volumeBeforeMute=this.volume,this.isMuted=!0,this.player&&this.player.setVolume(0),this._updateVolumeUI()),this}toggleFavorite(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url,i=this._favorites.has(e);return i?this._favorites.delete(e):this._favorites.add(e),this._updateFavoriteUI(),this._syncFavoriteAttributes(t.url,!i),m(this.config.storageKey,this._favorites),this._emit("favorite",{track:t,favorited:!i}),this.config.onFavorite&&this.config.onFavorite(t,!i),this.config.actions?.favorite&&b(this.config.actions.favorite,{action:"favorite",id:e,url:t.url,title:t.title,favorited:!i}),this}addToCart(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url;return this._cartItems.add(e),this.cartBtnEl&&(this.cartBtnEl.classList.add("wb-action-done"),setTimeout(()=>this.cartBtnEl.classList.remove("wb-action-done"),1500)),this._syncCartAttributes(t.url,!0),this._emit("cart",{track:t}),this.config.onCart&&this.config.onCart(t),this.config.actions?.cart&&b(this.config.actions.cart,{action:"cart",id:e,url:t.url,title:t.title}),this}isFavorited(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._favorites.has(t):!1}isInCart(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._cartItems.has(t):!1}removeFromQueue(t){return t<0||t>=this.queue.length||t===this.currentIndex?this:(this.queue.splice(t,1),t<this.currentIndex&&this.currentIndex--,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}clearQueue(){let t=this.getCurrentTrack();return this.queue=t?[t]:[],this.currentIndex=t?0:-1,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this}getCurrentTrack(){return this.currentIndex>=0&&this.currentIndex<this.queue.length?this.queue[this.currentIndex]:null}getQueue(){return[...this.queue]}getCurrentIndex(){return this.currentIndex}isCurrentlyPlaying(t){let e=this.getCurrentTrack();return this.isPlaying&&e&&e.url===t}isCurrentTrack(t){let e=this.getCurrentTrack();return e&&e.url===t}getPlayer(){return this.player}_emit(t,e={}){this.barEl&&this.barEl.dispatchEvent(new CustomEvent("waveformbar:"+t,{bubbles:!0,detail:e}))}show(){return this.barEl&&this.barEl.classList.add("wb-active"),this}hide(){return this.barEl&&this.barEl.classList.remove("wb-active"),this.closeQueuePanel(),this.closeVolumePopup(),this}toggleQueuePanel(){return this.queueOpen?this.closeQueuePanel():this.openQueuePanel()}openQueuePanel(){if(!this.queueEl)return this;if(this.queueOpen=!0,this.closeVolumePopup(),this.queueBtnEl){let t=this.queueBtnEl.getBoundingClientRect();this.queueEl.style.right=window.innerWidth-t.right+"px"}return this.queueEl.classList.add("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.add("wb-active"),this._renderQueue(),this}closeQueuePanel(){return this.queueEl?(this.queueOpen=!1,this.queueEl.classList.remove("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.remove("wb-active"),this):this}toggleVolumePopup(){return this.volumePopupEl?.classList.contains("wb-volume-open")?this.closeVolumePopup():this.openVolumePopup(),this}openVolumePopup(){return this.volumePopupEl?(this.closeQueuePanel(),this.volumePopupEl.classList.add("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.add("wb-active"),this):this}closeVolumePopup(){return this.volumePopupEl?(this.volumePopupEl.classList.remove("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.remove("wb-active"),this):this}_loadCurrentTrack(){let t=this.getCurrentTrack();if(!t||!this.player)return;this.show(),this._updateTrackDisplay(t),this._updateFavoriteUI();let e={artwork:t.artwork,album:t.album};if(t.waveform&&(e.waveform=t.waveform),this.config.configPath&&t.url){let r=t.url.split("/").pop().split("?")[0].replace(/\.[^.]+$/,".json"),a=this.config.configPath.replace(/\/?$/,"/");e.config=a+r}if(t.markers&&t.markers.length){let i=this.config.markerColor;e.markers=t.markers.map(r=>({...r,color:r.color||i}))}else e.markers=[];this.player.loadTrack(t.url,t.title,t.artist,e),this._activeMarkers=t.markers&&t.markers.length?t.markers:null,this._currentMarkerIndex=-1,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._renderQueue(),this._syncPageState(),this._saveState(),this._updateNavButtons(),this._emit("trackchange",{track:t,index:this.currentIndex}),this.config.onTrackChange&&this.config.onTrackChange(t,this.currentIndex)}_updateTrackDisplay(t){this.titleEl&&this._setScrollText(this.titleEl,t.title||"Untitled"),this.artistEl&&this._setScrollText(this.artistEl,t.artist||"");let e=this.barEl.querySelector(".wb-artwork");if(e){let r=t.artwork||this.config.defaultArtwork;e.innerHTML=r?`<img src="${u(r)}" alt="${u(t.title)}" />`:n.music}this.metaEl&&this.config.showMeta&&this._renderMeta(t);let i=this.barEl.querySelector(".wb-track");i&&(i.style.cursor=t.link?"pointer":"default"),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.timeTotalEl&&(this.timeTotalEl.textContent="0:00")}_setScrollText(t,e){t.classList.remove("wb-scrolling"),t.textContent=e,requestAnimationFrame(()=>{if(t.scrollWidth>t.clientWidth){let i=t.scrollWidth-t.clientWidth,r=Math.max(4,i/20);t.innerHTML=`<span class="wb-scroll-inner">${u(e)}</span>`,t.style.setProperty("--wb-scroll-distance",`-${i+48}px`),t.style.setProperty("--wb-scroll-duration",`${r}s`),t.classList.add("wb-scrolling")}})}_renderMeta(t){if(!this.metaEl)return;let e=[];if(t.bpm&&e.push({label:t.bpm+" BPM",type:"bpm"}),t.key&&e.push({label:t.key,type:"key"}),t.duration&&e.push({label:t.duration,type:"duration"}),t.meta)for(let[r,a]of Object.entries(t.meta))a&&e.length<this.config.maxMeta&&e.push({label:String(a),type:r});let i=e.slice(0,this.config.maxMeta);this.metaEl.style.display=i.length?"flex":"none",this.metaEl.innerHTML=i.map(r=>`<span class="wb-tag wb-tag-${u(r.type)}">${u(r.label)}</span>`).join("")}_updatePlayButton(){if(!this.playBtnEl)return;let t=this.playBtnEl.querySelector(".wb-icon-play"),e=this.playBtnEl.querySelector(".wb-icon-pause");t&&(t.style.display=this.isPlaying?"none":"block"),e&&(e.style.display=this.isPlaying?"block":"none"),this.playBtnEl.title=this.isPlaying?"Pause":"Play"}_updateNavButtons(){let t=this.barEl?.querySelector(".wb-prev"),e=this.barEl?.querySelector(".wb-next");this.repeat==="all"?(t&&t.classList.remove("wb-disabled"),e&&e.classList.remove("wb-disabled")):(t&&t.classList.toggle("wb-disabled",this.currentIndex<=0),e&&e.classList.toggle("wb-disabled",this.currentIndex>=this.queue.length-1))}cycleRepeat(){let t=["off","all","one"],e=t.indexOf(this.repeat);return this.repeat=t[(e+1)%t.length],this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat}),this}setRepeat(t){return["off","all","one"].includes(t)&&(this.repeat=t,this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat})),this}_updateRepeatButton(){if(!this.repeatBtnEl)return;let t={off:n.repeatOff,all:n.repeatAll,one:n.repeatOne},e={off:"Repeat: Off",all:"Repeat: All",one:"Repeat: One"};this.repeatBtnEl.innerHTML=t[this.repeat],this.repeatBtnEl.title=e[this.repeat],this.repeatBtnEl.classList.toggle("wb-repeat-active",this.repeat!=="off")}_checkMarkerBoundary(t){if(!this._activeMarkers)return;let e=-1;for(let l=this._activeMarkers.length-1;l>=0;l--)if(t>=this._activeMarkers[l].time){e=l;break}if(e===this._currentMarkerIndex||(this._currentMarkerIndex=e,e<0))return;let i=this._activeMarkers[e],r=this.getCurrentTrack();i.title&&this.titleEl&&this._setScrollText(this.titleEl,i.title),i.artist&&this.artistEl&&this._setScrollText(this.artistEl,i.artist);let a=this.waveformContainer?.querySelectorAll(".waveform-marker");if(a&&a.forEach((l,c)=>l.classList.toggle("wb-marker-active",c===e)),i.artwork){let l=this.barEl.querySelector(".wb-artwork");l&&(l.innerHTML=`<img src="${i.artwork}" alt="${i.title||""}" />`)}if(this.metaEl&&(i.bpm||i.key)){let l={...r||{},bpm:i.bpm||"",key:i.key||""};this._renderMeta(l)}this._emit("markerchange",{marker:i,index:e,track:r})}_updateVolumeUI(){this.volumeSliderEl&&(this.volumeSliderEl.value=this.isMuted?0:Math.round(this.volume*100)),this.muteBtnEl&&(this.isMuted||this.volume===0?(this.muteBtnEl.innerHTML=n.volMute,this.muteBtnEl.classList.add("wb-muted"),this.muteBtnEl.title="Unmute"):this.volume<.5?(this.muteBtnEl.innerHTML=n.volLow,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"):(this.muteBtnEl.innerHTML=n.volHigh,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"))}_detectTheme(){let t=document.documentElement,e=document.body,i=["dark","dark-mode","theme-dark"],r=["light","light-mode","theme-light"];for(let a of i)if(t.classList.contains(a)||e.classList.contains(a))return"dark";if(t.getAttribute("data-theme")==="dark"||e.getAttribute("data-theme")==="dark")return"dark";for(let a of r)if(t.classList.contains(a)||e.classList.contains(a))return"light";if(t.getAttribute("data-theme")==="light"||e.getAttribute("data-theme")==="light")return"light";try{let l=getComputedStyle(e).backgroundColor.match(/\d+/g);if(l&&l.length>=3){let c=(l[0]*299+l[1]*587+l[2]*114)/1e3;if(c>128)return"light";if(c<128)return"dark"}}catch{}return window.matchMedia?.("(prefers-color-scheme: light)").matches?"light":"dark"}_updateFavoriteUI(){if(!this.favBtnEl)return;let t=this.isFavorited();this.favBtnEl.innerHTML=t?n.heartFilled:n.heart,this.favBtnEl.classList.toggle("wb-fav-active",t)}_renderQueue(){B(this.queueBodyEl,this.queueCountEl,this.queue,this.currentIndex,{onSkipTo:t=>this.skipTo(t),onRemove:t=>this.removeFromQueue(t)})}_syncPageState(){let t=this.getCurrentTrack(),e=t?t.url:null;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbUrl||i.dataset.url,a=i.dataset.wbId||i.dataset.id||r,l=r&&r===e;i.classList.toggle("wb-current",l),i.classList.toggle("wb-playing",l&&this.isPlaying),i.classList.toggle("wb-favorited",this._favorites.has(a)),i.classList.toggle("wb-in-cart",this._cartItems.has(a))})}_seedFromAttributes(){let t=!1,e=!1;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbId||i.dataset.id||i.dataset.wbUrl||i.dataset.url;r&&(i.dataset.wbFavorited==="true"&&(this._favorites.add(r),t=!0),i.dataset.wbInCart==="true"&&(this._cartItems.add(r),e=!0))}),t&&m(this.config.storageKey,this._favorites)}_syncFavoriteAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbFavorited=e?"true":"false",i.classList.toggle("wb-favorited",e))})}_syncCartAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbInCart=e?"true":"false",i.classList.toggle("wb-in-cart",e))})}_saveState(){this.config.persist&&g(this.config.storageKey,{queue:this.queue,currentIndex:this.currentIndex,position:this._lastPosition||0,isPlaying:this.isPlaying})}_restoreState(){if(!this.config.persist)return;let t=w(this.config.storageKey);if(!t)return;this.queue=t.queue,this.currentIndex=t.currentIndex;let e=this.getCurrentTrack();if(e){if(this.show(),this._updateTrackDisplay(e),this._updateFavoriteUI(),this._updateNavButtons(),e.waveform&&(this.player.options.waveform=e.waveform),this.config.configPath&&e.url){let r=e.url.split("/").pop().split("?")[0].replace(/\.[^.]+$/,".json"),a=this.config.configPath.replace(/\/?$/,"/");this.player.options.config=a+r}if(this.player.options.title=e.title||"",this.player.options.subtitle=e.artist||"",e.markers&&e.markers.length){let i=this.config.markerColor;this.player.options.markers=e.markers.map(r=>({...r,color:r.color||i})),this._activeMarkers=e.markers}else this.player.options.markers=[],this._activeMarkers=null;this._currentMarkerIndex=-1,this.player.load(e.url).then(()=>{if(this.player&&this.player.setVolume(this.isMuted?0:this.volume),t.isPlaying&&this.config.autoResume)try{let i=this.player.play();i&&typeof i.catch=="function"&&i.catch(()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()})}catch{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()}t.position>0&&setTimeout(()=>{this.player&&(this.player.seekTo(t.position),this._lastPosition=t.position)},100)}).catch(()=>{}),this._renderQueue(),this._syncPageState()}}_restoreVolume(){let t=E(this.config.storageKey);t&&(this.volume=t.volume,this.isMuted=t.muted,this._volumeBeforeMute=t.volumeBeforeMute,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._updateVolumeUI())}_restoreFavorites(){this._favorites=_(this.config.storageKey)}};var C=new f;typeof window<"u"&&(window.WaveformBar=C);var W=C;})();
44
+ </div>`}}s.innerHTML=l,s.querySelectorAll(".wb-queue-item[data-qi]").forEach(o=>{o.addEventListener("click",h=>{h.target.closest(".wb-queue-remove")||r.onSkipTo&&r.onSkipTo(parseInt(o.dataset.qi))})}),s.querySelectorAll(".wb-queue-remove").forEach(o=>{o.addEventListener("click",h=>{h.stopPropagation(),r.onRemove&&r.onRemove(parseInt(o.dataset.qi))})})}var x={persist:!0,autoResume:!0,continuous:!0,repeat:"off",showRepeat:!0,showQueue:!0,showPrevNext:!0,showVolume:!0,showMute:!0,showMeta:!0,showTime:!0,showTrackLink:!0,maxMeta:3,defaultArtwork:null,theme:null,waveformStyle:"mirror",waveformHeight:32,barWidth:2,barSpacing:0,waveformColor:null,progressColor:null,markerColor:"rgba(255, 255, 255, 0.25)",volume:1,storageKey:"waveform-bar",actions:null,onPlay:null,onPause:null,onTrackChange:null,onQueueChange:null,onVolumeChange:null,onFavorite:null,onCart:null},f=class{constructor(){this.config=null,this.player=null,this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.isInitialized=!1,this.queueOpen=!1,this.volume=1,this.isMuted=!1,this._volumeBeforeMute=1,this._lastPosition=0,this._favorites=new Set,this._cartItems=new Set,this._observer=null,this._activeMarkers=null,this._currentMarkerIndex=-1,this.repeat="off",this.barEl=null,this.queueEl=null,this.waveformContainer=null,this.volumePopupEl=null,this.titleEl=null,this.artistEl=null,this.metaEl=null,this.playBtnEl=null,this.repeatBtnEl=null,this.queueBtnEl=null,this.queueBodyEl=null,this.queueCountEl=null,this.volumeSliderEl=null,this.muteBtnEl=null,this.favBtnEl=null,this.cartBtnEl=null,this.timeCurrentEl=null,this.timeTotalEl=null}init(t={}){return this.isInitialized&&this.destroy(),this.config={...x,...t},this.volume=this.config.volume,typeof window.WaveformPlayer>"u"?(console.error("WaveformBar: WaveformPlayer is required."),this):(this._createBar(),this._createQueue(),this._initPlayer(),this._bindTriggers(),this._observeDOM(),this.config.persist&&(this._restoreVolume(),this._restoreFavorites()),this._seedFromAttributes(),this.config.persist&&this._restoreState(),this.isInitialized=!0,this._beforeUnloadHandler=()=>this._saveState(),window.addEventListener("beforeunload",this._beforeUnloadHandler),this)}destroy(){return this.player&&(this.player.destroy(),this.player=null),this.barEl&&(this.barEl.remove(),this.barEl=null),this.queueEl&&(this.queueEl.remove(),this.queueEl=null),this._observer&&(this._observer.disconnect(),this._observer=null),this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),document.querySelectorAll("[data-wb-play],[data-wb-queue]").forEach(t=>delete t._wbBound),document.querySelectorAll(".wb-current,.wb-playing").forEach(t=>t.classList.remove("wb-current","wb-playing")),this.queue=[],this.currentIndex=-1,this.isPlaying=!1,this.queueOpen=!1,this.isInitialized=!1,this.config=null,this}_createBar(){this.barEl=document.createElement("div"),this.barEl.className="waveform-bar";let t=this.config.theme||this._detectTheme();t==="light"&&this.barEl.classList.add("wb-light"),this._resolvedTheme=t,this.barEl.id="waveform-bar",this.barEl.innerHTML=k(this.config),document.body.appendChild(this.barEl),this.titleEl=this.barEl.querySelector(".wb-title"),this.artistEl=this.barEl.querySelector(".wb-artist"),this.metaEl=this.barEl.querySelector(".wb-meta"),this.playBtnEl=this.barEl.querySelector(".wb-play"),this.waveformContainer=this.barEl.querySelector(".wb-waveform-container"),this.queueBtnEl=this.barEl.querySelector(".wb-queue-btn"),this.muteBtnEl=this.barEl.querySelector(".wb-mute"),this.volumeSliderEl=this.barEl.querySelector(".wb-volume-slider"),this.favBtnEl=this.barEl.querySelector(".wb-fav"),this.cartBtnEl=this.barEl.querySelector(".wb-cart"),this.timeCurrentEl=this.barEl.querySelector(".wb-time-current"),this.timeTotalEl=this.barEl.querySelector(".wb-time-total"),this.playBtnEl.addEventListener("click",()=>this.togglePlay());let e=this.barEl.querySelector(".wb-prev"),i=this.barEl.querySelector(".wb-next");e&&e.addEventListener("click",()=>this.previous()),i&&i.addEventListener("click",()=>this.next()),this.repeatBtnEl=this.barEl.querySelector(".wb-repeat"),this.repeatBtnEl&&(this.repeat=this.config.repeat||"off",this._updateRepeatButton(),this.repeatBtnEl.addEventListener("click",()=>this.cycleRepeat())),this.queueBtnEl&&this.queueBtnEl.addEventListener("click",()=>this.toggleQueuePanel()),this.volumePopupEl=this.barEl.querySelector(".wb-volume-popup");let r=this.barEl.querySelector(".wb-volume");if(this.muteBtnEl&&this.muteBtnEl.addEventListener("click",a=>{a.stopPropagation(),this.toggleMute()}),r&&this.volumePopupEl){let a;r.addEventListener("mouseenter",()=>{clearTimeout(a),this.openVolumePopup()}),r.addEventListener("mouseleave",()=>{a=setTimeout(()=>this.closeVolumePopup(),300)})}this.volumeSliderEl&&this.volumeSliderEl.addEventListener("input",a=>{a.stopPropagation(),this.setVolume(parseInt(a.target.value)/100)}),document.addEventListener("click",a=>{this.volumePopupEl?.classList.contains("wb-volume-open")&&!this.barEl.querySelector(".wb-volume")?.contains(a.target)&&this.closeVolumePopup()}),this.favBtnEl&&this.favBtnEl.addEventListener("click",()=>this.toggleFavorite()),this.cartBtnEl&&this.cartBtnEl.addEventListener("click",()=>this.addToCart()),this.config.showTrackLink&&this.barEl.querySelector(".wb-track").addEventListener("click",()=>{let a=this.getCurrentTrack();a&&a.link&&(window.location.href=a.link)})}_createQueue(){this.config.showQueue&&(this.queueEl=q(),this._resolvedTheme==="light"&&this.queueEl.classList.add("wb-light"),document.body.appendChild(this.queueEl),this.queueBodyEl=this.queueEl.querySelector(".wb-queue-body"),this.queueCountEl=this.queueEl.querySelector(".wb-queue-count"),this.queueEl.querySelector(".wb-queue-clear").addEventListener("click",()=>this.clearQueue()),document.addEventListener("click",t=>{this.queueOpen&&!this.queueEl.contains(t.target)&&!this.queueBtnEl.contains(t.target)&&this.closeQueuePanel()}))}_initPlayer(){let t={showControls:!1,showInfo:!1,waveformStyle:this.config.waveformStyle,height:this.config.waveformHeight,barWidth:this.config.barWidth,barSpacing:this.config.barSpacing,singlePlay:!1,onPlay:()=>{this.isPlaying=!0,this._updatePlayButton(),this._syncPageState();let e=this.getCurrentTrack();this._emit("play",{track:e}),this.config.onPlay&&this.config.onPlay(e)},onPause:()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this._saveState();let e=this.getCurrentTrack();this._emit("pause",{track:e}),this.config.onPause&&this.config.onPause(e)},onEnd:()=>{if(this.isPlaying=!1,this._updatePlayButton(),this._syncPageState(),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.repeat==="one"){this.player&&(this.player.seekTo(0),this.player.play().catch(()=>{}));return}this.config.continuous&&this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack())},onTimeUpdate:(e,i)=>{this._lastPosition=e,this.timeCurrentEl&&(this.timeCurrentEl.textContent=v(e)),this.timeTotalEl&&(this.timeTotalEl.textContent=v(i)),(!this._lastSaveTime||e-this._lastSaveTime>2)&&(this._lastSaveTime=e,this._saveState()),this._activeMarkers&&this._checkMarkerBoundary(e)},onLoad:null};this.config.waveformColor&&(t.waveformColor=this.config.waveformColor),this.config.progressColor&&(t.progressColor=this.config.progressColor),this.player=new window.WaveformPlayer(this.waveformContainer,t),this.player.setVolume(this.volume)}_bindTriggers(){document.querySelectorAll("[data-wb-play]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault();let i=m(t);i&&this.play(i)}))}),document.querySelectorAll("[data-wb-queue]").forEach(t=>{t._wbBound||(t._wbBound=!0,t.addEventListener("click",e=>{e.preventDefault(),e.stopPropagation();let i=m(t);i&&this.addToQueue(i)}))})}_observeDOM(){typeof MutationObserver>"u"||(this._observer=new MutationObserver(()=>{this._bindTriggers(),this._syncPageState()}),this._observer.observe(document.body,{childList:!0,subtree:!0}))}play(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;if(!e||!e.url)return this;let i=this.getCurrentTrack();if(i&&i.url===e.url)return this.togglePlay(),this;let r=this.queue.findIndex(a=>a.url===e.url);if(r>=0)this.queue[r]={...this.queue[r],...e},this.currentIndex=r;else{let a=this.currentIndex+1;this.queue.splice(a,0,e),this.currentIndex=a}return this._loadCurrentTrack(),this}addToQueue(t){let e=typeof t=="string"?{url:t,id:t,title:d(t)}:t;return!e||!e.url?this:this.queue.find(i=>i.url===e.url)?this:(this.queue.push(e),this._renderQueue(),this._saveState(),this._updateNavButtons(),this.currentIndex===-1&&(this.currentIndex=0,this._loadCurrentTrack()),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}togglePlay(){return this.player?(this.isPlaying?this.player.pause():this.player.play(),this):this}pause(){return this.player&&this.isPlaying&&this.player.pause(),this}next(){return this.currentIndex<this.queue.length-1?(this.currentIndex++,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=0,this._loadCurrentTrack()),this}previous(){return this.player&&this.player.audio&&this.player.audio.currentTime>3?(this.player.seekTo(0),this):(this.currentIndex>0?(this.currentIndex--,this._loadCurrentTrack()):this.repeat==="all"&&this.queue.length>0&&(this.currentIndex=this.queue.length-1,this._loadCurrentTrack()),this)}skipTo(t){return t<0||t>=this.queue.length?this:t===this.currentIndex?(this.togglePlay(),this):(this.currentIndex=t,this._loadCurrentTrack(),this)}seekToMarker(t){if(!this._activeMarkers||t<0||t>=this._activeMarkers.length)return this;let e=this._activeMarkers[t];return this.player&&(this.player.seekTo(e.time),this.isPlaying||this.togglePlay()),this}seekToMarkerByLabel(t){if(!this._activeMarkers)return this;let e=this._activeMarkers.findIndex(i=>(i.label||i.title||"").toLowerCase()===t.toLowerCase());return e>=0&&this.seekToMarker(e),this}setVolume(t){return this.volume=Math.max(0,Math.min(1,t)),this.isMuted=this.volume===0,this.player&&this.player.setVolume(this.volume),this._updateVolumeUI(),y(this.config.storageKey,this.volume,this.isMuted,this._volumeBeforeMute),this._emit("volumechange",{volume:this.volume}),this.config.onVolumeChange&&this.config.onVolumeChange(this.volume),this}getVolume(){return this.volume}toggleMute(){return this.isMuted?this.setVolume(this._volumeBeforeMute||1):(this._volumeBeforeMute=this.volume,this.isMuted=!0,this.player&&this.player.setVolume(0),this._updateVolumeUI()),this}toggleFavorite(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url,i=this._favorites.has(e);return i?this._favorites.delete(e):this._favorites.add(e),this._updateFavoriteUI(),this._syncFavoriteAttributes(t.url,!i),p(this.config.storageKey,this._favorites),this._emit("favorite",{track:t,favorited:!i}),this.config.onFavorite&&this.config.onFavorite(t,!i),this.config.actions?.favorite&&b(this.config.actions.favorite,{action:"favorite",id:e,url:t.url,title:t.title,favorited:!i}),this}addToCart(){let t=this.getCurrentTrack();if(!t)return this;let e=t.id||t.url;return this._cartItems.add(e),this.cartBtnEl&&(this.cartBtnEl.classList.add("wb-action-done"),setTimeout(()=>this.cartBtnEl.classList.remove("wb-action-done"),1500)),this._syncCartAttributes(t.url,!0),this._emit("cart",{track:t}),this.config.onCart&&this.config.onCart(t),this.config.actions?.cart&&b(this.config.actions.cart,{action:"cart",id:e,url:t.url,title:t.title}),this}isFavorited(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._favorites.has(t):!1}isInCart(t){if(!t){let e=this.getCurrentTrack();t=e?e.id||e.url:null}return t?this._cartItems.has(t):!1}removeFromQueue(t){return t<0||t>=this.queue.length||t===this.currentIndex?this:(this.queue.splice(t,1),t<this.currentIndex&&this.currentIndex--,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this)}clearQueue(){let t=this.getCurrentTrack();return this.queue=t?[t]:[],this.currentIndex=t?0:-1,this._renderQueue(),this._saveState(),this._updateNavButtons(),this._emit("queuechange",{queue:this.queue,currentIndex:this.currentIndex}),this.config.onQueueChange&&this.config.onQueueChange(this.queue,this.currentIndex),this}getCurrentTrack(){return this.currentIndex>=0&&this.currentIndex<this.queue.length?this.queue[this.currentIndex]:null}getQueue(){return[...this.queue]}getCurrentIndex(){return this.currentIndex}isCurrentlyPlaying(t){let e=this.getCurrentTrack();return this.isPlaying&&e&&e.url===t}isCurrentTrack(t){let e=this.getCurrentTrack();return e&&e.url===t}getPlayer(){return this.player}_emit(t,e={}){this.barEl&&this.barEl.dispatchEvent(new CustomEvent("waveformbar:"+t,{bubbles:!0,detail:e}))}show(){return this.barEl&&this.barEl.classList.add("wb-active"),this}hide(){return this.barEl&&this.barEl.classList.remove("wb-active"),this.closeQueuePanel(),this.closeVolumePopup(),this}toggleQueuePanel(){return this.queueOpen?this.closeQueuePanel():this.openQueuePanel()}openQueuePanel(){if(!this.queueEl)return this;if(this.queueOpen=!0,this.closeVolumePopup(),this.queueBtnEl){let t=this.queueBtnEl.getBoundingClientRect();this.queueEl.style.right=window.innerWidth-t.right+"px"}return this.queueEl.classList.add("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.add("wb-active"),this._renderQueue(),this}closeQueuePanel(){return this.queueEl?(this.queueOpen=!1,this.queueEl.classList.remove("wb-queue-open"),this.queueBtnEl&&this.queueBtnEl.classList.remove("wb-active"),this):this}toggleVolumePopup(){return this.volumePopupEl?.classList.contains("wb-volume-open")?this.closeVolumePopup():this.openVolumePopup(),this}openVolumePopup(){return this.volumePopupEl?(this.closeQueuePanel(),this.volumePopupEl.classList.add("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.add("wb-active"),this):this}closeVolumePopup(){return this.volumePopupEl?(this.volumePopupEl.classList.remove("wb-volume-open"),this.muteBtnEl&&this.muteBtnEl.classList.remove("wb-active"),this):this}_loadCurrentTrack(){let t=this.getCurrentTrack();if(!t||!this.player)return;this.show(),this._updateTrackDisplay(t),this._updateFavoriteUI();let e={artwork:t.artwork,album:t.album};if(t.waveform&&(e.waveform=t.waveform),t.markers&&t.markers.length){let i=this.config.markerColor;e.markers=t.markers.map(r=>({...r,color:r.color||i}))}else e.markers=[];this.player.loadTrack(t.url,t.title,t.artist,e),this._activeMarkers=t.markers&&t.markers.length?t.markers:null,this._currentMarkerIndex=-1,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._renderQueue(),this._syncPageState(),this._saveState(),this._updateNavButtons(),this._emit("trackchange",{track:t,index:this.currentIndex}),this.config.onTrackChange&&this.config.onTrackChange(t,this.currentIndex)}_updateTrackDisplay(t){this.titleEl&&this._setScrollText(this.titleEl,t.title||"Untitled"),this.artistEl&&this._setScrollText(this.artistEl,t.artist||"");let e=this.barEl.querySelector(".wb-artwork");if(e){let r=t.artwork||this.config.defaultArtwork;e.innerHTML=r?`<img src="${u(r)}" alt="${u(t.title)}" />`:n.music}this.metaEl&&this.config.showMeta&&this._renderMeta(t);let i=this.barEl.querySelector(".wb-track");i&&(i.style.cursor=t.link?"pointer":"default"),this.timeCurrentEl&&(this.timeCurrentEl.textContent="0:00"),this.timeTotalEl&&(this.timeTotalEl.textContent="0:00")}_setScrollText(t,e){t.classList.remove("wb-scrolling"),t.textContent=e,requestAnimationFrame(()=>{if(t.scrollWidth>t.clientWidth){let i=t.scrollWidth-t.clientWidth,r=Math.max(4,i/20);t.innerHTML=`<span class="wb-scroll-inner">${u(e)}</span>`,t.style.setProperty("--wb-scroll-distance",`-${i+48}px`),t.style.setProperty("--wb-scroll-duration",`${r}s`),t.classList.add("wb-scrolling")}})}_renderMeta(t){if(!this.metaEl)return;let e=[];if(t.bpm&&e.push({label:t.bpm+" BPM",type:"bpm"}),t.key&&e.push({label:t.key,type:"key"}),t.duration&&e.push({label:t.duration,type:"duration"}),t.meta)for(let[r,a]of Object.entries(t.meta))a&&e.length<this.config.maxMeta&&e.push({label:String(a),type:r});let i=e.slice(0,this.config.maxMeta);this.metaEl.style.display=i.length?"flex":"none",this.metaEl.innerHTML=i.map(r=>`<span class="wb-tag wb-tag-${u(r.type)}">${u(r.label)}</span>`).join("")}_updatePlayButton(){if(!this.playBtnEl)return;let t=this.playBtnEl.querySelector(".wb-icon-play"),e=this.playBtnEl.querySelector(".wb-icon-pause");t&&(t.style.display=this.isPlaying?"none":"block"),e&&(e.style.display=this.isPlaying?"block":"none"),this.playBtnEl.title=this.isPlaying?"Pause":"Play"}_updateNavButtons(){let t=this.barEl?.querySelector(".wb-prev"),e=this.barEl?.querySelector(".wb-next");this.repeat==="all"?(t&&t.classList.remove("wb-disabled"),e&&e.classList.remove("wb-disabled")):(t&&t.classList.toggle("wb-disabled",this.currentIndex<=0),e&&e.classList.toggle("wb-disabled",this.currentIndex>=this.queue.length-1))}cycleRepeat(){let t=["off","all","one"],e=t.indexOf(this.repeat);return this.repeat=t[(e+1)%t.length],this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat}),this}setRepeat(t){return["off","all","one"].includes(t)&&(this.repeat=t,this._updateRepeatButton(),this._updateNavButtons(),this._emit("repeatchange",{mode:this.repeat})),this}_updateRepeatButton(){if(!this.repeatBtnEl)return;let t={off:n.repeatOff,all:n.repeatAll,one:n.repeatOne},e={off:"Repeat: Off",all:"Repeat: All",one:"Repeat: One"};this.repeatBtnEl.innerHTML=t[this.repeat],this.repeatBtnEl.title=e[this.repeat],this.repeatBtnEl.classList.toggle("wb-repeat-active",this.repeat!=="off")}_checkMarkerBoundary(t){if(!this._activeMarkers)return;let e=-1;for(let l=this._activeMarkers.length-1;l>=0;l--)if(t>=this._activeMarkers[l].time){e=l;break}if(e===this._currentMarkerIndex||(this._currentMarkerIndex=e,e<0))return;let i=this._activeMarkers[e],r=this.getCurrentTrack();i.title&&this.titleEl&&this._setScrollText(this.titleEl,i.title),i.artist&&this.artistEl&&this._setScrollText(this.artistEl,i.artist);let a=this.waveformContainer?.querySelectorAll(".waveform-marker");if(a&&a.forEach((l,c)=>l.classList.toggle("wb-marker-active",c===e)),i.artwork){let l=this.barEl.querySelector(".wb-artwork");l&&(l.innerHTML=`<img src="${i.artwork}" alt="${i.title||""}" />`)}if(this.metaEl&&(i.bpm||i.key)){let l={...r||{},bpm:i.bpm||"",key:i.key||""};this._renderMeta(l)}this._emit("markerchange",{marker:i,index:e,track:r})}_updateVolumeUI(){this.volumeSliderEl&&(this.volumeSliderEl.value=this.isMuted?0:Math.round(this.volume*100)),this.muteBtnEl&&(this.isMuted||this.volume===0?(this.muteBtnEl.innerHTML=n.volMute,this.muteBtnEl.classList.add("wb-muted"),this.muteBtnEl.title="Unmute"):this.volume<.5?(this.muteBtnEl.innerHTML=n.volLow,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"):(this.muteBtnEl.innerHTML=n.volHigh,this.muteBtnEl.classList.remove("wb-muted"),this.muteBtnEl.title="Mute"))}_detectTheme(){let t=document.documentElement,e=document.body,i=["dark","dark-mode","theme-dark"],r=["light","light-mode","theme-light"];for(let a of i)if(t.classList.contains(a)||e.classList.contains(a))return"dark";if(t.getAttribute("data-theme")==="dark"||e.getAttribute("data-theme")==="dark")return"dark";for(let a of r)if(t.classList.contains(a)||e.classList.contains(a))return"light";if(t.getAttribute("data-theme")==="light"||e.getAttribute("data-theme")==="light")return"light";try{let l=getComputedStyle(e).backgroundColor.match(/\d+/g);if(l&&l.length>=3){let c=(l[0]*299+l[1]*587+l[2]*114)/1e3;if(c>128)return"light";if(c<128)return"dark"}}catch{}return window.matchMedia?.("(prefers-color-scheme: light)").matches?"light":"dark"}_updateFavoriteUI(){if(!this.favBtnEl)return;let t=this.isFavorited();this.favBtnEl.innerHTML=t?n.heartFilled:n.heart,this.favBtnEl.classList.toggle("wb-fav-active",t)}_renderQueue(){B(this.queueBodyEl,this.queueCountEl,this.queue,this.currentIndex,{onSkipTo:t=>this.skipTo(t),onRemove:t=>this.removeFromQueue(t)})}_syncPageState(){let t=this.getCurrentTrack(),e=t?t.url:null;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbUrl||i.dataset.url,a=i.dataset.wbId||i.dataset.id||r,l=r&&r===e;i.classList.toggle("wb-current",l),i.classList.toggle("wb-playing",l&&this.isPlaying),i.classList.toggle("wb-favorited",this._favorites.has(a)),i.classList.toggle("wb-in-cart",this._cartItems.has(a))})}_seedFromAttributes(){let t=!1,e=!1;document.querySelectorAll("[data-wb-play]").forEach(i=>{let r=i.dataset.wbId||i.dataset.id||i.dataset.wbUrl||i.dataset.url;r&&(i.dataset.wbFavorited==="true"&&(this._favorites.add(r),t=!0),i.dataset.wbInCart==="true"&&(this._cartItems.add(r),e=!0))}),t&&p(this.config.storageKey,this._favorites)}_syncFavoriteAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbFavorited=e?"true":"false",i.classList.toggle("wb-favorited",e))})}_syncCartAttributes(t,e){document.querySelectorAll("[data-wb-play]").forEach(i=>{(i.dataset.wbUrl||i.dataset.url)===t&&(i.dataset.wbInCart=e?"true":"false",i.classList.toggle("wb-in-cart",e))})}_saveState(){this.config.persist&&g(this.config.storageKey,{queue:this.queue,currentIndex:this.currentIndex,position:this._lastPosition||0,isPlaying:this.isPlaying})}_restoreState(){if(!this.config.persist)return;let t=w(this.config.storageKey);if(!t)return;this.queue=t.queue,this.currentIndex=t.currentIndex;let e=this.getCurrentTrack();if(e){if(this.show(),this._updateTrackDisplay(e),this._updateFavoriteUI(),this._updateNavButtons(),e.waveform&&(this.player.options.waveform=e.waveform),this.player.options.title=e.title||"",this.player.options.subtitle=e.artist||"",e.markers&&e.markers.length){let i=this.config.markerColor;this.player.options.markers=e.markers.map(r=>({...r,color:r.color||i})),this._activeMarkers=e.markers}else this.player.options.markers=[],this._activeMarkers=null;this._currentMarkerIndex=-1,this.player.load(e.url).then(()=>{if(this.player&&this.player.setVolume(this.isMuted?0:this.volume),t.isPlaying&&this.config.autoResume)try{let i=this.player.play();i&&typeof i.catch=="function"&&i.catch(()=>{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()})}catch{this.isPlaying=!1,this._updatePlayButton(),this._syncPageState()}t.position>0&&setTimeout(()=>{this.player&&(this.player.seekTo(t.position),this._lastPosition=t.position)},100)}).catch(()=>{}),this._renderQueue(),this._syncPageState()}}_restoreVolume(){let t=E(this.config.storageKey);t&&(this.volume=t.volume,this.isMuted=t.muted,this._volumeBeforeMute=t.volumeBeforeMute,this.player&&this.player.setVolume(this.isMuted?0:this.volume),this._updateVolumeUI())}_restoreFavorites(){this._favorites=_(this.config.storageKey)}};var C=new f;typeof window<"u"&&(window.WaveformBar=C);var W=C;})();
45
45
  /**
46
46
  * WaveformBar v1.0.0
47
47
  * Persistent bottom audio player bar for WaveformPlayer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arraypress/waveform-bar",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Persistent bottom audio player bar for WaveformPlayer - queue management, page persistence, and seamless playback.",
5
5
  "main": "dist/waveform-bar.js",
6
6
  "module": "dist/waveform-bar.esm.js",
@@ -5,12 +5,21 @@
5
5
  * MIT License
6
6
  *
7
7
  * Usage:
8
- * <span class="wbi wbi-play"></span>
8
+ * <span class="wbi wbi-play"></span>
9
9
  */
10
10
 
11
- .wbi-heart-filled {
12
- -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
13
- mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor' stroke='currentColor' stroke-width='2'><path d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/></svg>");
11
+ .wbi {
12
+ display: inline-block;
13
+ width: 1em;
14
+ height: 1em;
15
+ background-color: currentColor;
16
+ -webkit-mask-size: contain;
17
+ mask-size: contain;
18
+ -webkit-mask-repeat: no-repeat;
19
+ mask-repeat: no-repeat;
20
+ -webkit-mask-position: center;
21
+ mask-position: center;
22
+ vertical-align: -0.125em;
14
23
  }
15
24
 
16
25
  .wbi-sm {
@@ -136,4 +145,4 @@
136
145
  .wbi-share {
137
146
  -webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
138
147
  mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'><path d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/></svg>");
139
- }
148
+ }
package/src/js/core.js CHANGED
@@ -43,7 +43,6 @@ const DEFAULTS = {
43
43
  waveformColor: null,
44
44
  progressColor: null,
45
45
  markerColor: 'rgba(255, 255, 255, 0.25)',
46
- configPath: null, // Directory for auto-resolved config JSON files (e.g. 'waveforms/')
47
46
  volume: 1,
48
47
  storageKey: 'waveform-bar',
49
48
  actions: null,
@@ -838,14 +837,6 @@ export class WaveformBar {
838
837
  loadOpts.waveform = track.waveform;
839
838
  }
840
839
 
841
- // Auto-resolve config JSON from configPath
842
- if (this.config.configPath && track.url) {
843
- const audioFile = track.url.split('/').pop().split('?')[0];
844
- const jsonFile = audioFile.replace(/\.[^.]+$/, '.json');
845
- const path = this.config.configPath.replace(/\/?$/, '/');
846
- loadOpts.config = path + jsonFile;
847
- }
848
-
849
840
  // Always pass markers — empty array clears previous track's markers
850
841
  if (track.markers && track.markers.length) {
851
842
  const defaultColor = this.config.markerColor;
@@ -1269,14 +1260,6 @@ export class WaveformBar {
1269
1260
  this.player.options.waveform = track.waveform;
1270
1261
  }
1271
1262
 
1272
- // Auto-resolve config JSON from configPath
1273
- if (this.config.configPath && track.url) {
1274
- const audioFile = track.url.split('/').pop().split('?')[0];
1275
- const jsonFile = audioFile.replace(/\.[^.]+$/, '.json');
1276
- const path = this.config.configPath.replace(/\/?$/, '/');
1277
- this.player.options.config = path + jsonFile;
1278
- }
1279
-
1280
1263
  this.player.options.title = track.title || '';
1281
1264
  this.player.options.subtitle = track.artist || '';
1282
1265
 
@@ -1344,4 +1327,5 @@ export class WaveformBar {
1344
1327
  _restoreFavorites() {
1345
1328
  this._favorites = restoreFavorites(this.config.storageKey);
1346
1329
  }
1330
+
1347
1331
  }