Bar
[Unreleased]
Section titled “[Unreleased]”[1.12.1] — 2026-09-24
Section titled “[1.12.1] — 2026-09-24”- The play/pause glyph is centred in its circle again. The icon spans are
toggled to
display: blockand the svg inside them rode the text baseline, sitting ~3px above centre. The svgs are now blocks.
Changed
Section titled “Changed”- A slimmer bar. Desktop padding is
0.625rem(was1rem), 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.
[1.12.0] — 2026-09-24
Section titled “[1.12.0] — 2026-09-24”barRadiusconfig 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
idwere never synced. Discovery looked external-mode players up withWaveformPlayer.instances.get(el.id), but a player whose element has noidregisters under a generatedwp_…key that is never written back to the element — so the documented trigger + inline markup (which has noid) 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-playelement, a click on the inline play button dispatchedrequest-playand bubbled to the bar’s delegated trigger listener, which calledplay()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 ownrequest-*events, andrequest-playfolds in the enclosing trigger’sdata-wb-*metadata (id, title, link…) so nothing is lost by ignoring the click. request-playno longer clobbers queued track data. The event detail was spread straight into the queue entry — including the player’s container or generatedid(which then keyed favourites), the wholeplayerinstance (serialised to sessionStorage every couple of seconds) andnulltitle/artist/artwork, empty markers and anullwaveform over good queued values. Every path that queues a track (play(),addToQueue(),request-play, session restore, share links) now goes through onenormalizeTrack(), 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 unguardedsessionStorage.removeItem(), which throws the sameSecurityErroras the read did (sandboxed iframes, blocked site data) — straight out ofinit().- A persisted queue with corrupt
markersno longer breaks every page of the tab. Restore only validatedurl, somarkers: "x"or[null]threw ininit()on each load until the session ended. Restored tracks now go throughnormalizeTrack(), which keeps only marker objects with a finitetime. - 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
pagehideand when the page becomes hidden — iOS Safari doesn’t reliably firebeforeunload. repeatwas ignored withshowRepeat: 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, asshufflealready was.destroy()left inline players stuck “playing”. The external-player map was dropped without pumpingsetPlayingState(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, soinit()with different config starts clean.addToQueue()didn’t emitwaveformbar:queuechange, althoughremoveFromQueue()/clearQueue()did and the API docs list it.- A refused
play()no longer strands the bar.togglePlay()andseekToMarker()(and repeat-one) calledplayer.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 rejectedloadTrack()is no longer surfaced as an unhandled rejection either (load failures still arrive throughonError). - 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 carriesaria-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’saria-labelnames the mode (Repeat: Off/All/One) since three modes don’t fit a pressed/unpressed pair. showTime: falsenow 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’sfalse.- 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.
Changed
Section titled “Changed”persist: falsenow writes nothing tolocalStorage.setVolume(),toggleMute(),toggleFavorite()and thedata-wb-favoritedseeding saved volume/mute/favourites regardless ofpersist, 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-titleand.wb-queue-item-artistare<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 }), thewaveformplayer:destroyevent — as does the newbarRadiusoption.
[1.11.3] — 2026-08-11
Section titled “[1.11.3] — 2026-08-11”- 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-playerhas 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.
restoreQueueStateaccepted anything with a truthyqueue.length— and'abc'.lengthis 3 — so a stored value of the wrong shape passed the guard and then threw at the firstqueue.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 acurrentIndexthat 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.
[1.11.2] — 2026-08-09
Section titled “[1.11.2] — 2026-08-09”formatTime()put'Infinity:NaN'in the bar’s time display for a streamed or unseekable source, which reportsaudio.duration === Infinity— truthy and notNaN, so it passed both guards, andInfinity % 60isNaN. Non-finite, non-numeric and negative input now renders'0:00'. Same class of bug as the core player’sformatTime, fixed there in 1.24.2.
[1.11.1] — 2026-08-09
Section titled “[1.11.1] — 2026-08-09”- 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 isrgba(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.
Changed
Section titled “Changed”- Detection now delegates to
WaveformPlayer.utils.detectColorSchemewhen 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 toprefers-color-schemeonly 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.
[1.11.0] — 2026-07-22
Section titled “[1.11.0] — 2026-07-22”crossOriginconfig option. Forwarded to the embedded<audio>player ('anonymous'|'use-credentials'|null). Defaults tonull— the bar no longer implies any CORS mode, matching the core player’s new default, so CDN-hosted media withoutAccess-Control-Allow-Originplays out of the box. Only passed to the player when truthy. Pairs with@arraypress/waveform-player@^1.23.0.
[1.10.0] — 2026-07-05
Section titled “[1.10.0] — 2026-07-05”- Forward the core player’s localizable UI strings.
seekValueText,playPauseLabel,speedLabel,artworkAlt, andunknownTrackTextare now accepted in the bar config and passed through to the embedded player (each defaults tonull→ the player’s own default). OnlyseekValueText(the bar’s visible seek slider) andunknownTrackText(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.0for the new options.
[1.9.1] — 2026-07-02
Section titled “[1.9.1] — 2026-07-02”Changed
Section titled “Changed”- 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-heightCSS variable (viaResizeObserver) so the sheet sits flush regardless of layout or artwork height.
[1.9.0] — 2026-07-01
Section titled “[1.9.0] — 2026-07-01”- Lock-screen skip-track buttons. Wires the embedded player’s
onNextTrack/onPreviousTrackto the bar’s queue, so the OS Media Session next/previous controls advance the queue. (Needs@arraypress/waveform-player>= 1.19.)
[1.8.0] — 2026-07-01
Section titled “[1.8.0] — 2026-07-01”- Gradient direction passthrough. Forwards the player’s
waveformGradient('vertical' | 'horizontal' | 'diagonal') to the embedded player, so a gradientwaveformColor/progressColorcan sweep across the bar’s waveform.
[1.7.0] — 2026-07-01
Section titled “[1.7.0] — 2026-07-01”- 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.
showShuffleadds a shuffle toggle to the transport andshufflesets its initial state (random queue advance on next / auto-advance).
Changed
Section titled “Changed”- Simplified layout config to two modes.
modesupersedes the oldlayout/waveformcombination;widegives 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 onEscape, returning focus to the trigger; the collapse button keeps itsaria-expandedin sync.
Removed
Section titled “Removed”maxWidthconfig option — superseded bymode/widesizing.
[1.6.4] - 2026-06-30
Section titled “[1.6.4] - 2026-06-30”- 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 (specificity0,1,1) out-specified the bar’s.wb-volume-sliderrule (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 pinnedflex-shrink: 0) so the bar’s own sizing wins regardless of host form-control styles.
[1.3.2] - 2026-06-27
Section titled “[1.3.2] - 2026-06-27”Changed
Section titled “Changed”- Raised the
@arraypress/waveform-playerpeer dependency floor to^1.7.2, which ships the native accessible (keyboard / ARIA) seek slider. In external mode the player dispatcheswaveformplayer:request-seekon keyboard seek — already handled by the bar — so keyboard seeking on inline player surfaces now routes through the bar with no code changes here.
[1.3.1] - 2026-05-13
Section titled “[1.3.1] - 2026-05-13”- 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()andsetProgress()so its canvas reflects the live bar state without waiting for the nexttimeupdatetick.
[1.3.0] - 2026-05-13
Section titled “[1.3.0] - 2026-05-13”New Features
Section titled “New Features”-
External WaveformPlayer integration — when
@arraypress/waveform-player1.6+ instances are mounted withaudioMode: '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, andwaveformplayer:request-seekevents at the document level. Any inline player whose URL matches the currently-playing track getssetPlayingState(true)andsetProgress(...)calls pumped to it.<!-- Same element doubles as a bar trigger (data-wb-play) AND aninline visual surface (data-waveform-player + audio-mode). --><div data-waveform-playerdata-audio-mode="external"data-url="song.mp3"data-waveform-style="bars"data-wb-playdata-wb-url="song.mp3"data-wb-title="..."></div>
Backward Compatibility
Section titled “Backward Compatibility”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.
[1.2.1] - 2026-03-22
Section titled “[1.2.1] - 2026-03-22”Removed
Section titled “Removed”- Removed
configPathoption (dead code from experimental feature that was never functional in a published release)
- Fixed
waveform-bar-icons.cssmissing base.wbiclass — icons now render correctly - Fixed broken comment block and duplicate
.wbi-heart-filledrule in icons CSS - Removed debug
console.logfrom session restore