Skip to content

Bar

  • The play/pause glyph is centred in its circle again. The icon spans are toggled to display: block and the svg inside them rode the text baseline, sitting ~3px above centre. The svgs are now blocks.
  • A slimmer bar. Desktop padding is 0.625rem (was 1rem), and with waveform-player 1.28.1 the waveform finally renders at its intended 32px instead of 60px — together the bar drops from ~93px to ~69px tall.
  • barRadius config option — rounded bar caps in px for the bar’s waveform (0 = square), forwarded to the embedded player. null (default) keeps the player’s own default. It was neither in the defaults nor forwarded, so the bar was stuck on the player’s default caps.
  • Inline players without an id were never synced. Discovery looked external-mode players up with WaveformPlayer.instances.get(el.id), but a player whose element has no id registers under a generated wp_… key that is never written back to the element — so the documented trigger + inline markup (which has no id) was never mapped, and never showed play state or progress. Instances are now matched by container.
  • Clicking an inline player that is also a trigger toggled playback straight back off. On the documented data-waveform-player + data-wb-play element, a click on the inline play button dispatched request-play and bubbled to the bar’s delegated trigger listener, which called play() on the now-current URL and toggled it off again; a seek-click on the canvas paused. Clicks inside a registered external player are now left to that player’s own request-* events, and request-play folds in the enclosing trigger’s data-wb-* metadata (id, title, link…) so nothing is lost by ignoring the click.
  • request-play no longer clobbers queued track data. The event detail was spread straight into the queue entry — including the player’s container or generated id (which then keyed favourites), the whole player instance (serialised to sessionStorage every couple of seconds) and null title/artist/artwork, empty markers and a null waveform over good queued values. Every path that queues a track (play(), addToQueue(), request-play, session restore, share links) now goes through one normalizeTrack(), and merges into an existing entry skip empty values.
  • init({ persist: true }) no longer crashes when storage is blocked. restoreQueueState’s catch block cleared the poisoned key with an unguarded sessionStorage.removeItem(), which throws the same SecurityError as the read did (sandboxed iframes, blocked site data) — straight out of init().
  • A persisted queue with corrupt markers no longer breaks every page of the tab. Restore only validated url, so markers: "x" or [null] threw in init() on each load until the session ended. Restored tracks now go through normalizeTrack(), which keeps only marker objects with a finite time.
  • Stale resume position after a track switch or a backwards seek. The saved position and the periodic-save throttle were never reset when a new track loaded, so the previous track’s position was persisted against the new one, and saving stayed quiet until playback climbed past the old last-save time (also after seeking backwards). Both reset on load and the throttle compares the absolute distance. The exact position is now also saved on pagehide and when the page becomes hidden — iOS Safari doesn’t reliably fire beforeunload.
  • repeat was ignored with showRepeat: false. It was only seeded inside the repeat-button branch; it’s now seeded (and validated — unknown values fall back to 'off') before the button check, as shuffle already was.
  • destroy() left inline players stuck “playing”. The external-player map was dropped without pumping setPlayingState(false), so inline players kept their playing state and animation loop running with nothing left to stop them.
  • destroy() leaked session state into a re-init. Mute state, the pre-mute volume, favourites, cart items, the resume position, active markers and the collapsed flag now reset, so init() with different config starts clean.
  • addToQueue() didn’t emit waveformbar:queuechange, although removeFromQueue()/clearQueue() did and the API docs list it.
  • A refused play() no longer strands the bar. togglePlay() and seekToMarker() (and repeat-one) called player.play() without handling its promise, so blocked autoplay was an unhandled rejection and the bar could keep showing “playing”. The rejection is caught and the paused state restored. A rejected loadTrack() is no longer surfaced as an unhandled rejection either (load failures still arrive through onError).
  • Queue entries are keyboard-operable. “Skip to” was a click-only <div>; each row’s number + title/artist is now a real <button>, the remove button is revealed on keyboard focus (:focus-within), the current row carries aria-current, and focus stays in the queue after a keyboard skip.
  • Repeat and favourite buttons expose their state. Both now set aria-pressed; the repeat button’s aria-label names the mode (Repeat: Off/All/One) since three modes don’t fit a pressed/unpressed pair.
  • showTime: false now hides the time display. The option has been in the defaults (and every wrapper’s types) all along, but nothing read it. The time elements are now omitted in both layouts when it’s false.
  • The DOM observer no longer rescans on the bar’s own updates. It rebuilt the external-player map and re-synced every trigger on any body mutation — including the bar’s time text and each inline player’s time display, i.e. every timeupdate tick. Mutations inside the bar, the queue panel and registered inline players are now ignored, and the rest are debounced (50ms) so a burst of page changes rescans once.
  • persist: false now writes nothing to localStorage. setVolume(), toggleMute(), toggleFavorite() and the data-wb-favorited seeding saved volume/mute/favourites regardless of persist, contrary to the docs. In-memory state and events are unchanged; only the storage writes are gated.
  • Queue row markup. A row’s number and text now sit inside <button class="wb-queue-skip">, and .wb-queue-info, .wb-queue-item-title and .wb-queue-item-artist are <span>s (block-level via CSS) instead of <div>s. Class names are unchanged; custom CSS that selected by element type (div.wb-queue-item-title) needs updating.
  • Shuffle picks only tracks not yet played, and stops at the end. Shuffle drew from the whole queue — including tracks already played — and never stopped with repeat off; the docs promise “a random upcoming track”. The bar now tracks a shuffle pass: next() and auto-advance pick among tracks not yet played in it, stop once every track has played (repeat 'off'), or start a new pass under repeat 'all'. Turning shuffle on starts a fresh pass from the current track, and the Next button’s disabled state follows the same rule.
  • Peer dependency raised to @arraypress/waveform-player@^1.8.0 (from ^1.7.2). External mode crashed before 1.8.0, and the bar already relied on APIs that arrived there — setProgress()/setPlayingState(), loadTrack(…, { autoplay: false }), the waveformplayer:destroy event — as does the new barRadius option.
  • The play triangle is optically centred in the round play button. Flexbox centres the icon’s box, but a right-pointing triangle carries its visual weight on the flat left edge, so a box-centred glyph reads as sitting left of the circle’s centre. waveform-player has corrected this on the identical glyph since forever (.waveform-icon-play svg); the bar had no equivalent rule at all, so the two components looked different side by side. Thanks to @sporteka2 for reporting in #1.
  • A corrupted or version-skewed persisted queue no longer breaks restore. restoreQueueState accepted anything with a truthy queue.length — and 'abc'.length is 3 — so a stored value of the wrong shape passed the guard and then threw at the first queue.findIndex(), leaving the bar half-initialised. The queue is now shape-checked as an array of tracks with URLs, entries that aren’t usable are dropped, and a currentIndex that doesn’t address the restored queue resets to 0 instead of stranding the bar on a track that isn’t there. Session storage is only as trustworthy as the last thing that wrote it.
  • formatTime() put 'Infinity:NaN' in the bar’s time display for a streamed or unseekable source, which reports audio.duration === Infinity — truthy and not NaN, so it passed both guards, and Infinity % 60 is NaN. Non-finite, non-numeric and negative input now renders '0:00'. Same class of bug as the core player’s formatTime, fixed there in 1.24.2.
  • Bar theme auto-detection had its own copy of the core player’s transparent-background bug. On any page that doesn’t paint <body>, the computed background is rgba(0, 0, 0, 0); the bar scored that as pure black and pinned itself to the dark theme on white pages. Same root cause as waveform-player#21.
  • Detection now delegates to WaveformPlayer.utils.detectColorScheme when the installed core player provides it (1.24.1+), so the bar and the inline players can’t disagree about the page theme. The local implementation stays as a fallback for older player builds — the peer range is unchanged — and has been fixed to match: it composites <body> and <html> backgrounds over the browser canvas and reads that canvas from the page’s resolved text colour, falling back to prefers-color-scheme only when no text colour resolves.
  • Detection deliberately starts at <body>, never at the bar element: the bar paints its own --wb-bg, so walking from itself would just read back the theme it already had.
  • crossOrigin config option. Forwarded to the embedded <audio> player ('anonymous' | 'use-credentials' | null). Defaults to null — the bar no longer implies any CORS mode, matching the core player’s new default, so CDN-hosted media without Access-Control-Allow-Origin plays out of the box. Only passed to the player when truthy. Pairs with @arraypress/waveform-player@^1.23.0.
  • Forward the core player’s localizable UI strings. seekValueText, playPauseLabel, speedLabel, artworkAlt, and unknownTrackText are now accepted in the bar config and passed through to the embedded player (each defaults to null → the player’s own default). Only seekValueText (the bar’s visible seek slider) and unknownTrackText (the embedded self-mode player’s Media Session title fallback) have a visible effect — the bar hides the player’s own controls/info — but the rest are forwarded for completeness. Requires @arraypress/waveform-player@^1.20.0 for the new options.
  • Mobile classic (Spotify) layout. On phones the classic bar now lays out as a single row — artwork + title/artist left, transport centred, secondary controls right — with the seek bar as a full-width draggable strip below. The 64px now-playing artwork applies at all widths; the volume slider and BPM badge are hidden on the classic mobile row so it does not crowd.
  • Queue as a native mobile sheet. The queue panel is now full-width, edge-to-edge and fused to the top of the bar. The bar publishes its height as a --wb-height CSS variable (via ResizeObserver) so the sheet sits flush regardless of layout or artwork height.
  • Lock-screen skip-track buttons. Wires the embedded player’s onNextTrack / onPreviousTrack to the bar’s queue, so the OS Media Session next/previous controls advance the queue. (Needs @arraypress/waveform-player >= 1.19.)
  • Gradient direction passthrough. Forwards the player’s waveformGradient ('vertical' | 'horizontal' | 'diagonal') to the embedded player, so a gradient waveformColor / progressColor can sweep across the bar’s waveform.
  • Classic (Spotify-style) mode. mode: 'waveform' | 'classic' — 'classic' lays the bar out in three columns: now-playing on the left, transport centred over a full-width seek bar, secondary controls on the right. 'waveform' (default) keeps the existing full-width waveform layout.
  • Shuffle. showShuffle adds a shuffle toggle to the transport and shuffle sets its initial state (random queue advance on next / auto-advance).
  • Simplified layout config to two modes. mode supersedes the old layout / waveform combination; wide gives waveform mode a full-width size.
  • Queue + collapse accessibility. The queue button now exposes aria-haspopup / aria-expanded (open/closed state is announced) and closes on Escape, returning focus to the trigger; the collapse button keeps its aria-expanded in sync.
  • maxWidth config option — superseded by mode / wide sizing.
  • Volume slider collapsed to a tiny stub when the host page styles range inputs. The vertical slider is a horizontal range input rotated -90°. A generic input[type="range"] { width: 100% } on the host page (specificity 0,1,1) out-specified the bar’s .wb-volume-slider rule (0,1,0), shrinking the slider to the 40px popup width — a 40px stub after rotation. The bar’s slider rules are now scoped under .wb-volume-popup (0,2,0, plus a pinned flex-shrink: 0) so the bar’s own sizing wins regardless of host form-control styles.
  • Raised the @arraypress/waveform-player peer dependency floor to ^1.7.2, which ships the native accessible (keyboard / ARIA) seek slider. In external mode the player dispatches waveformplayer:request-seek on keyboard seek — already handled by the bar — so keyboard seeking on inline player surfaces now routes through the bar with no code changes here.
  • Cross-page state seed — when a new external WaveformPlayer instance is discovered after the bar is already playing (typical SPA / page-navigation flow: hit Play on a card → navigate to the product page → inline player mounts), the bar now immediately seeds the newly-discovered player with setPlayingState() and setProgress() so its canvas reflects the live bar state without waiting for the next timeupdate tick.
  • External WaveformPlayer integration — when @arraypress/waveform-player 1.6+ instances are mounted with audioMode: 'external' (data-audio-mode="external"), WaveformBar now discovers them automatically and treats them as synchronized visual surfaces. Click the inline player’s play button → playback happens in the bar; the bar’s progress mirrors into the inline canvas in real time.

    No configuration needed — the bar scans for matching players on init + MutationObserver tick, and listens for waveformplayer:request-play, waveformplayer:request-pause, and waveformplayer:request-seek events at the document level. Any inline player whose URL matches the currently-playing track gets setPlayingState(true) and setProgress(...) calls pumped to it.

    <!-- Same element doubles as a bar trigger (data-wb-play) AND an
    inline visual surface (data-waveform-player + audio-mode). -->
    <div data-waveform-player
    data-audio-mode="external"
    data-url="song.mp3"
    data-waveform-style="bars"
    data-wb-play
    data-wb-url="song.mp3"
    data-wb-title="..."></div>

Fully additive. Stores / pages that don’t render any external-mode WaveformPlayer instances behave exactly as before — the discovery scan returns an empty Map, the pump methods short-circuit. Existing [data-wb-play] triggers without the player markup continue to work unchanged.

  • Removed configPath option (dead code from experimental feature that was never functional in a published release)
  • Fixed waveform-bar-icons.css missing base .wbi class — icons now render correctly
  • Fixed broken comment block and duplicate .wbi-heart-filled rule in icons CSS
  • Removed debug console.log from session restore