@arraypress/waveform-player-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@arraypress/waveform-player-react` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] — Unreleased
9
+
10
+ Initial release.
11
+
12
+ ### Added
13
+
14
+ - `<WaveformPlayer>` React component wrapping every option exposed
15
+ by `@arraypress/waveform-player` 1.6.x as a typed prop:
16
+ - Audio source (`url`, `audioMode`, `preload`)
17
+ - Waveform visualisation (`waveformStyle`, `height`, `samples`,
18
+ `barWidth`, `barSpacing`, `waveform`)
19
+ - Colours (`colorPreset`, `waveformColor`, `progressColor`,
20
+ `buttonColor`, `buttonHoverColor`, `textColor`,
21
+ `textSecondaryColor`, `backgroundColor`, `borderColor`)
22
+ - Playback (`playbackRate`, `showPlaybackSpeed`, `playbackRates`)
23
+ - UI toggles (`showControls`, `showInfo`, `showTime`,
24
+ `showHoverTime`, `showBPM`, `buttonAlign`)
25
+ - Markers (`markers`, `showMarkers`)
26
+ - Metadata (`title`, `subtitle`, `artwork`, `album`)
27
+ - Behaviour (`autoplay`, `singlePlay`, `playOnSeek`,
28
+ `enableMediaSession`)
29
+ - Icons (`playIcon`, `pauseIcon`)
30
+ - Callback props (`onLoad`, `onPlay`, `onPause`, `onEnd`,
31
+ `onTimeUpdate`, `onError`) that map to the library's same-named
32
+ option fields. Callbacks are deliberately NOT in the effect dep
33
+ array, so a parent re-rendering with new inline functions
34
+ doesn't tear the player down.
35
+ - React-specific extras: `id`, `className`, `style`, and `ref`
36
+ forwarding via `WaveformPlayerHandle`.
37
+ - `WaveformPlayerHandle` imperative API on the forwarded ref —
38
+ `play()`, `pause()`, `togglePlay()`, `seekTo()`, `seekToPercent()`,
39
+ `setVolume()`, `setPlaybackRate()`, `setPlayingState()`,
40
+ `setProgress()`, `loadTrack()`, plus the raw `instance`.
41
+ - SSR / RSC safe: the core library is loaded via dynamic
42
+ `import('@arraypress/waveform-player')` inside the effect so
43
+ the browser-only audio surface never runs server-side.
44
+ - Identity-prop re-mount: when any library-construction prop
45
+ changes (`url`, `audioMode`, etc.), the wrapper destroys the
46
+ existing instance and creates a new one with the updated
47
+ options. Simpler and more correct than diffing every option +
48
+ calling granular updaters.
49
+ - Public TypeScript types: `WaveformPlayerProps`,
50
+ `WaveformPlayerHandle`, `WaveformStyle`, `WaveformMarker`,
51
+ `WaveformPeaks`, `ColorPreset`, `AudioMode`, `AudioPreload`,
52
+ `ButtonAlign`.
53
+ - Ambient module shim for `@arraypress/waveform-player` so the
54
+ wrapper typechecks cleanly until the core library ships its
55
+ own `.d.ts`.
56
+ - Vitest test suite (17 tests, jsdom + `@testing-library/react`)
57
+ covering mount, unmount destroy, option pass-through, callback
58
+ forwarding, identity-prop re-mount, callback-churn protection,
59
+ ref forwarding, and the full imperative handle surface. The
60
+ core library is mocked at the module boundary because jsdom
61
+ has no Web Audio API.
62
+ - Dual ESM (`dist/index.js`) + CJS (`dist/index.cjs`) build via
63
+ `tsup`. `.d.ts` for both. React + the core library are
64
+ externalised so they resolve to the consumer's copies.
65
+ - README with full prop reference, seven usage patterns, and the
66
+ imperative-ref control example. `examples/basic.tsx` with seven
67
+ copy-paste-ready snippets.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArrayPress
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # @arraypress/waveform-player-react
2
+
3
+ React component wrapper around [`@arraypress/waveform-player`](https://github.com/arraypress/waveform-player). `forwardRef`-friendly, `useEffect` lifecycle, typed props for every library option, and an imperative handle for `play() / pause() / seekTo() / loadTrack()` that mirrors the underlying instance.
4
+
5
+ The core library stays a zero-dependency vanilla-JS package that works anywhere a `<script>` tag does. This package adds the framework-native ergonomics React developers expect.
6
+
7
+ ```tsx
8
+ import { WaveformPlayer } from '@arraypress/waveform-player-react';
9
+
10
+ function App() {
11
+ return <WaveformPlayer url="/audio/track.mp3" title="My Track" />;
12
+ }
13
+ ```
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @arraypress/waveform-player-react @arraypress/waveform-player react
19
+ ```
20
+
21
+ `react` (^18 or ^19) and `@arraypress/waveform-player` (^1.6) are peer dependencies — you bring them so you control the versions.
22
+
23
+ ## Setup
24
+
25
+ Import the core library's CSS **once** in your app entry (Vite `main.tsx`, Next.js `app/layout.tsx`, Remix `root.tsx`, etc.):
26
+
27
+ ```ts
28
+ import '@arraypress/waveform-player/dist/waveform-player.css';
29
+ ```
30
+
31
+ The wrapper does **not** import the CSS for you — your bundler should own that decision. The library's JS is loaded dynamically inside `useEffect`, so SSR / RSC environments don't trip over the browser-only audio APIs.
32
+
33
+ ## Usage
34
+
35
+ ### Basic
36
+
37
+ ```tsx
38
+ <WaveformPlayer url="/audio/track.mp3" />
39
+ ```
40
+
41
+ ### With metadata + chosen style
42
+
43
+ ```tsx
44
+ <WaveformPlayer
45
+ url="/audio/track.mp3"
46
+ title="Midnight Dreams"
47
+ subtitle="The Wavelength"
48
+ artwork="/img/cover.jpg"
49
+ waveformStyle="bars"
50
+ barWidth={3}
51
+ barSpacing={1}
52
+ height={80}
53
+ />
54
+ ```
55
+
56
+ ### Pre-computed peaks (recommended for catalogues)
57
+
58
+ ```tsx
59
+ <WaveformPlayer url="/audio/track.mp3" waveform="/peaks/track.json" />
60
+ ```
61
+
62
+ Generate the JSON at build time with [`@arraypress/waveform-gen`](https://github.com/arraypress/waveform-gen). Removes the Web Audio decode cost (~1–5 s per file) from the render path.
63
+
64
+ ### Chapter markers
65
+
66
+ ```tsx
67
+ <WaveformPlayer
68
+ url="/audio/podcast.mp3"
69
+ markers={[
70
+ { time: 0, label: 'Intro' },
71
+ { time: 60, label: 'Main topic', color: '#a855f7' },
72
+ { time: 600, label: 'Q&A' },
73
+ ]}
74
+ />
75
+ ```
76
+
77
+ ### Event callbacks
78
+
79
+ Every event the core library exposes is a typed prop:
80
+
81
+ ```tsx
82
+ <WaveformPlayer
83
+ url="/audio/track.mp3"
84
+ onLoad={(instance) => console.log('loaded', instance)}
85
+ onPlay={() => console.log('playing')}
86
+ onPause={() => console.log('paused')}
87
+ onTimeUpdate={(currentTime, duration) => console.log(`${currentTime}s / ${duration}s`)}
88
+ onEnd={() => console.log('finished')}
89
+ onError={(err) => console.error('audio failed:', err)}
90
+ />
91
+ ```
92
+
93
+ Callback props **don't trigger re-mounts** — the wrapper intentionally keeps them out of its effect's dep array so a parent re-rendering with new inline functions on every render doesn't tear the player down.
94
+
95
+ ### Imperative control via ref
96
+
97
+ For "play this track when X happens" flows where wiring through props is awkward:
98
+
99
+ ```tsx
100
+ import { useRef } from 'react';
101
+ import { WaveformPlayer, type WaveformPlayerHandle } from '@arraypress/waveform-player-react';
102
+
103
+ function Controlled() {
104
+ const playerRef = useRef<WaveformPlayerHandle>(null);
105
+
106
+ return (
107
+ <>
108
+ <WaveformPlayer ref={playerRef} url="/audio/track.mp3" />
109
+ <button onClick={() => playerRef.current?.togglePlay()}>Play / Pause</button>
110
+ <button onClick={() => playerRef.current?.seekTo(30)}>Jump to 0:30</button>
111
+ <button onClick={() => playerRef.current?.setVolume(0.5)}>Vol 50%</button>
112
+ </>
113
+ );
114
+ }
115
+ ```
116
+
117
+ The handle methods (`play()`, `pause()`, `togglePlay()`, `seekTo()`, `seekToPercent()`, `setVolume()`, `setPlaybackRate()`, `setPlayingState()`, `setProgress()`, `loadTrack()`) pass straight through to the underlying instance. `ref.current?.instance` exposes the raw instance for anything the handle doesn't surface yet.
118
+
119
+ ### External audio mode
120
+
121
+ When pairing with `@arraypress/waveform-bar` (or any other audio controller you own), the player can render visualisation only and surrender audio playback to the controller:
122
+
123
+ ```tsx
124
+ <WaveformPlayer
125
+ url={track.url}
126
+ audioMode="external"
127
+ waveformStyle="seekbar"
128
+ showInfo={false}
129
+ />
130
+ ```
131
+
132
+ The player dispatches `waveformplayer:request-play | request-pause | request-seek` events instead of touching audio itself. Drive the visualisation from your controller via `playerRef.current?.setProgress(currentTime, duration)` and `setPlayingState(playing)`.
133
+
134
+ ## How prop changes are handled
135
+
136
+ When **any** prop the core library uses at construction time changes (`url`, `audioMode`, `waveformStyle`, `markers`, colours, sizing, etc.), the wrapper destroys the existing instance and creates a new one with the updated options. That's simpler and more correct than diffing every option and calling the right granular updater, and the core library has built-in caches (waveform peaks keyed by URL) that make same-URL re-mounts cheap.
137
+
138
+ Callback props are deliberately **not** in the dep array — a parent re-rendering with a fresh inline `onPlay={() => …}` shouldn't tear the player down.
139
+
140
+ ## Props
141
+
142
+ Every library option surfaces as a typed prop. See the full table in [`src/types.ts`](./src/types.ts) for JSDoc per prop.
143
+
144
+ ### Audio source
145
+
146
+ | Prop | Type | Default |
147
+ | ----------- | ------------------------------------- | ------------ |
148
+ | `url` | `string` *(required)* | — |
149
+ | `audioMode` | `'self' \| 'external'` | `'self'` |
150
+ | `preload` | `'auto' \| 'metadata' \| 'none'` | `'metadata'` |
151
+
152
+ ### Waveform visualisation
153
+
154
+ | Prop | Type | Default |
155
+ | --------------- | ------------------------------------------------------------- | ---------- |
156
+ | `waveformStyle` | `'bars' \| 'mirror' \| 'line' \| 'blocks' \| 'dots' \| 'seekbar'` | `'mirror'` |
157
+ | `height` | `number` | `60` |
158
+ | `samples` | `number` | `200` |
159
+ | `barWidth` | `number` | style-dep |
160
+ | `barSpacing` | `number` | style-dep |
161
+ | `waveform` | `number[] \| string` | — |
162
+
163
+ ### Colours
164
+
165
+ All optional. `colorPreset` controls the auto theme; any individual colour wins over the preset.
166
+
167
+ | Prop | Type |
168
+ | --------------------- | ------------------------------- |
169
+ | `colorPreset` | `'dark' \| 'light' \| null` |
170
+ | `waveformColor` | `string` |
171
+ | `progressColor` | `string` |
172
+ | `buttonColor` | `string` |
173
+ | `buttonHoverColor` | `string` |
174
+ | `textColor` | `string` |
175
+ | `textSecondaryColor` | `string` |
176
+
177
+ ### Playback / UI / behaviour
178
+
179
+ | Prop | Type | Default |
180
+ | ------------------- | ------------------------------------------ | ------------------------------------ |
181
+ | `playbackRate` | `number` | `1` |
182
+ | `showPlaybackSpeed` | `boolean` | `false` |
183
+ | `playbackRates` | `number[]` | `[0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]` |
184
+ | `showControls` | `boolean` | `true` |
185
+ | `showInfo` | `boolean` | `true` |
186
+ | `showTime` | `boolean` | `true` |
187
+ | `showBPM` | `boolean` | `false` |
188
+ | `buttonAlign` | `'auto' \| 'top' \| 'center' \| 'bottom'` | `'auto'` |
189
+ | `autoplay` | `boolean` | `false` |
190
+ | `singlePlay` | `boolean` | `true` |
191
+ | `playOnSeek` | `boolean` | `true` |
192
+ | `enableMediaSession`| `boolean` | `true` |
193
+
194
+ ### Markers + metadata
195
+
196
+ | Prop | Type |
197
+ | ------------- | ---------------------------------------------------------- |
198
+ | `markers` | `Array<{ time: number; label: string; color?: string }>` |
199
+ | `showMarkers` | `boolean` |
200
+ | `title` | `string` |
201
+ | `subtitle` | `string` |
202
+ | `artwork` | `string` |
203
+ | `album` | `string` |
204
+
205
+ ### Callbacks (DO NOT trigger re-mount)
206
+
207
+ | Prop | Signature |
208
+ | -------------- | -------------------------------------------------------------- |
209
+ | `onLoad` | `(instance: unknown) => void` |
210
+ | `onPlay` | `(instance: unknown) => void` |
211
+ | `onPause` | `(instance: unknown) => void` |
212
+ | `onEnd` | `(instance: unknown) => void` |
213
+ | `onTimeUpdate` | `(currentTime: number, duration: number, instance: unknown) => void` |
214
+ | `onError` | `(error: Error, instance: unknown) => void` |
215
+
216
+ ### React-specific
217
+
218
+ | Prop | Type |
219
+ | ----------- | -------------------------- |
220
+ | `id` | `string` |
221
+ | `className` | `string` |
222
+ | `style` | `React.CSSProperties` |
223
+ | `ref` | `Ref<WaveformPlayerHandle>` |
224
+
225
+ ## TypeScript
226
+
227
+ ```ts
228
+ import type {
229
+ WaveformPlayerProps,
230
+ WaveformPlayerHandle,
231
+ WaveformStyle,
232
+ WaveformMarker,
233
+ WaveformPeaks,
234
+ ColorPreset,
235
+ AudioMode,
236
+ AudioPreload,
237
+ ButtonAlign,
238
+ } from '@arraypress/waveform-player-react';
239
+ ```
240
+
241
+ The package ships `.d.ts` for both ESM and CJS consumers.
242
+
243
+ ## Testing
244
+
245
+ ```bash
246
+ npm test # one-shot
247
+ npm run test:watch
248
+ npm run typecheck
249
+ npm run build # emit dist/index.js, dist/index.cjs, dist/index.d.ts
250
+ ```
251
+
252
+ The core library is mocked at the module boundary (jsdom has no Web Audio API). 17 tests cover mount / unmount, option pass-through, ref forwarding, identity-prop re-mount, and callback-churn protection.
253
+
254
+ ## License
255
+
256
+ MIT © ArrayPress
package/dist/index.cjs ADDED
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/WaveformPlayer.tsx
9
+ function buildLibraryOptions(props) {
10
+ const opts = {};
11
+ if (props.url !== void 0) opts.url = props.url;
12
+ if (props.audioMode !== void 0) opts.audioMode = props.audioMode;
13
+ if (props.preload !== void 0) opts.preload = props.preload;
14
+ if (props.waveformStyle !== void 0) opts.waveformStyle = props.waveformStyle;
15
+ if (props.height !== void 0) opts.height = props.height;
16
+ if (props.samples !== void 0) opts.samples = props.samples;
17
+ if (props.barWidth !== void 0) opts.barWidth = props.barWidth;
18
+ if (props.barSpacing !== void 0) opts.barSpacing = props.barSpacing;
19
+ if (props.waveform !== void 0 && props.waveform !== null) {
20
+ opts.waveform = props.waveform;
21
+ }
22
+ if (props.colorPreset !== void 0) opts.colorPreset = props.colorPreset;
23
+ if (props.waveformColor !== void 0) opts.waveformColor = props.waveformColor;
24
+ if (props.progressColor !== void 0) opts.progressColor = props.progressColor;
25
+ if (props.buttonColor !== void 0) opts.buttonColor = props.buttonColor;
26
+ if (props.buttonHoverColor !== void 0) opts.buttonHoverColor = props.buttonHoverColor;
27
+ if (props.textColor !== void 0) opts.textColor = props.textColor;
28
+ if (props.textSecondaryColor !== void 0) opts.textSecondaryColor = props.textSecondaryColor;
29
+ if (props.backgroundColor !== void 0) opts.backgroundColor = props.backgroundColor;
30
+ if (props.borderColor !== void 0) opts.borderColor = props.borderColor;
31
+ if (props.playbackRate !== void 0) opts.playbackRate = props.playbackRate;
32
+ if (props.showPlaybackSpeed !== void 0) opts.showPlaybackSpeed = props.showPlaybackSpeed;
33
+ if (props.playbackRates !== void 0) opts.playbackRates = props.playbackRates;
34
+ if (props.showControls !== void 0) opts.showControls = props.showControls;
35
+ if (props.showInfo !== void 0) opts.showInfo = props.showInfo;
36
+ if (props.showTime !== void 0) opts.showTime = props.showTime;
37
+ if (props.showHoverTime !== void 0) opts.showHoverTime = props.showHoverTime;
38
+ if (props.showBPM !== void 0) opts.showBPM = props.showBPM;
39
+ if (props.buttonAlign !== void 0) opts.buttonAlign = props.buttonAlign;
40
+ if (props.markers !== void 0) opts.markers = props.markers;
41
+ if (props.showMarkers !== void 0) opts.showMarkers = props.showMarkers;
42
+ if (props.title !== void 0) opts.title = props.title;
43
+ if (props.subtitle !== void 0) opts.subtitle = props.subtitle;
44
+ if (props.artwork !== void 0) opts.artwork = props.artwork;
45
+ if (props.album !== void 0) opts.album = props.album;
46
+ if (props.autoplay !== void 0) opts.autoplay = props.autoplay;
47
+ if (props.singlePlay !== void 0) opts.singlePlay = props.singlePlay;
48
+ if (props.playOnSeek !== void 0) opts.playOnSeek = props.playOnSeek;
49
+ if (props.enableMediaSession !== void 0) opts.enableMediaSession = props.enableMediaSession;
50
+ if (props.playIcon !== void 0) opts.playIcon = props.playIcon;
51
+ if (props.pauseIcon !== void 0) opts.pauseIcon = props.pauseIcon;
52
+ if (props.onLoad) opts.onLoad = props.onLoad;
53
+ if (props.onPlay) opts.onPlay = props.onPlay;
54
+ if (props.onPause) opts.onPause = props.onPause;
55
+ if (props.onEnd) opts.onEnd = props.onEnd;
56
+ if (props.onTimeUpdate) opts.onTimeUpdate = props.onTimeUpdate;
57
+ if (props.onError) opts.onError = props.onError;
58
+ return opts;
59
+ }
60
+ var WaveformPlayer = react.forwardRef(
61
+ function WaveformPlayer2(props, ref) {
62
+ const containerRef = react.useRef(null);
63
+ const instanceRef = react.useRef(null);
64
+ react.useEffect(() => {
65
+ let cancelled = false;
66
+ let localInstance = null;
67
+ void import('@arraypress/waveform-player').then((mod) => {
68
+ if (cancelled) return;
69
+ const container = containerRef.current;
70
+ if (!container) return;
71
+ const WaveformPlayerClass = mod.default ?? mod.WaveformPlayer;
72
+ if (typeof WaveformPlayerClass !== "function") {
73
+ console.error(
74
+ "[waveform-player-react] Failed to resolve WaveformPlayer constructor from module."
75
+ );
76
+ return;
77
+ }
78
+ const opts = buildLibraryOptions(props);
79
+ localInstance = new WaveformPlayerClass(container, opts);
80
+ instanceRef.current = localInstance;
81
+ }).catch((err) => {
82
+ console.error("[waveform-player-react] Failed to load library:", err);
83
+ });
84
+ return () => {
85
+ cancelled = true;
86
+ const current = localInstance ?? instanceRef.current;
87
+ if (current && typeof current.destroy === "function") {
88
+ try {
89
+ current.destroy();
90
+ } catch (err) {
91
+ console.warn("[waveform-player-react] destroy() threw:", err);
92
+ }
93
+ }
94
+ instanceRef.current = null;
95
+ };
96
+ }, [
97
+ props.url,
98
+ props.audioMode,
99
+ props.preload,
100
+ props.waveformStyle,
101
+ props.height,
102
+ props.samples,
103
+ props.barWidth,
104
+ props.barSpacing,
105
+ props.waveform,
106
+ props.colorPreset,
107
+ props.waveformColor,
108
+ props.progressColor,
109
+ props.buttonColor,
110
+ props.buttonHoverColor,
111
+ props.textColor,
112
+ props.textSecondaryColor,
113
+ props.backgroundColor,
114
+ props.borderColor,
115
+ props.playbackRate,
116
+ props.showPlaybackSpeed,
117
+ props.playbackRates,
118
+ props.showControls,
119
+ props.showInfo,
120
+ props.showTime,
121
+ props.showHoverTime,
122
+ props.showBPM,
123
+ props.buttonAlign,
124
+ props.markers,
125
+ props.showMarkers,
126
+ props.title,
127
+ props.subtitle,
128
+ props.artwork,
129
+ props.album,
130
+ props.autoplay,
131
+ props.singlePlay,
132
+ props.playOnSeek,
133
+ props.enableMediaSession,
134
+ props.playIcon,
135
+ props.pauseIcon
136
+ ]);
137
+ react.useImperativeHandle(
138
+ ref,
139
+ () => ({
140
+ play() {
141
+ const inst = instanceRef.current;
142
+ return inst?.play?.();
143
+ },
144
+ pause() {
145
+ const inst = instanceRef.current;
146
+ inst?.pause?.();
147
+ },
148
+ togglePlay() {
149
+ const inst = instanceRef.current;
150
+ inst?.togglePlay?.();
151
+ },
152
+ seekTo(seconds) {
153
+ const inst = instanceRef.current;
154
+ inst?.seekTo?.(seconds);
155
+ },
156
+ seekToPercent(percent) {
157
+ const inst = instanceRef.current;
158
+ inst?.seekToPercent?.(percent);
159
+ },
160
+ setVolume(volume) {
161
+ const inst = instanceRef.current;
162
+ inst?.setVolume?.(volume);
163
+ },
164
+ setPlaybackRate(rate) {
165
+ const inst = instanceRef.current;
166
+ inst?.setPlaybackRate?.(rate);
167
+ },
168
+ setPlayingState(playing) {
169
+ const inst = instanceRef.current;
170
+ inst?.setPlayingState?.(playing);
171
+ },
172
+ setProgress(currentTime, duration) {
173
+ const inst = instanceRef.current;
174
+ inst?.setProgress?.(currentTime, duration);
175
+ },
176
+ async loadTrack(url, title, subtitle, options) {
177
+ const inst = instanceRef.current;
178
+ if (!inst?.loadTrack) return;
179
+ await inst.loadTrack(url, title, subtitle, options);
180
+ },
181
+ get instance() {
182
+ return instanceRef.current;
183
+ }
184
+ }),
185
+ []
186
+ );
187
+ return /* @__PURE__ */ jsxRuntime.jsx(
188
+ "div",
189
+ {
190
+ ref: containerRef,
191
+ id: props.id,
192
+ className: ["wfp-host", props.className].filter(Boolean).join(" "),
193
+ style: props.style
194
+ }
195
+ );
196
+ }
197
+ );
198
+
199
+ exports.WaveformPlayer = WaveformPlayer;
200
+ exports.default = WaveformPlayer;
201
+ //# sourceMappingURL=index.cjs.map
202
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/WaveformPlayer.tsx"],"names":["forwardRef","WaveformPlayer","useRef","useEffect","useImperativeHandle","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * WaveformPlayer.tsx\n * ------------------\n *\n * React wrapper around `@arraypress/waveform-player`. Mounts a player\n * instance into a `<div>` on first render, tears it down on unmount,\n * and re-mounts when any \"identity\" prop changes (the props whose\n * change requires the library to start over from scratch — `url`,\n * `audioMode`).\n *\n * For non-identity props, this component currently re-creates the\n * instance as well, which is simpler than diffing every option and\n * calling the right granular updater. The trade-off is acceptable\n * because:\n *\n * - The library re-uses any cached waveform data keyed by URL, so\n * re-mounts on the same URL are cheap.\n * - Per-render churn on a player widget is rare in practice.\n *\n * If you need finer control — imperative `loadTrack()`, `seekTo()`,\n * `setVolume()`, etc. — grab the instance through a `ref`:\n *\n * ```tsx\n * import { useRef, useEffect } from 'react';\n * import { WaveformPlayer, type WaveformPlayerHandle } from '@arraypress/waveform-player-react';\n *\n * function MyPlayer() {\n * const ref = useRef<WaveformPlayerHandle>(null);\n * return (\n * <>\n * <WaveformPlayer ref={ref} url=\"/audio/track.mp3\" />\n * <button onClick={() => ref.current?.seekTo(60)}>Jump to 1:00</button>\n * </>\n * );\n * }\n * ```\n *\n * ## Library setup\n *\n * This component does **not** load the core library's CSS for you.\n * Import it once at your app entry:\n *\n * ```ts\n * import '@arraypress/waveform-player/dist/waveform-player.css';\n * ```\n *\n * The library's JS is imported dynamically inside `useEffect` so it\n * only loads on the client (SSR-safe).\n *\n * @module WaveformPlayer\n */\nimport {\n\tforwardRef,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseRef,\n\ttype ForwardedRef,\n} from 'react';\nimport type { WaveformPlayerHandle, WaveformPlayerProps } from './types';\n\n/**\n * Convert a `WaveformPlayerProps` object into the option shape the\n * core library accepts. Most fields pass straight through; this\n * helper exists so the option-building logic is testable on its own\n * and the component body stays focused on lifecycle.\n *\n * @param props - The component's resolved props.\n * @returns An options object to pass into `new WaveformPlayer(el, …)`.\n */\nfunction buildLibraryOptions(props: WaveformPlayerProps): Record<string, unknown> {\n\tconst opts: Record<string, unknown> = {};\n\n\t/* Audio source */\n\tif (props.url !== undefined) opts.url = props.url;\n\tif (props.audioMode !== undefined) opts.audioMode = props.audioMode;\n\tif (props.preload !== undefined) opts.preload = props.preload;\n\n\t/* Waveform visualisation */\n\tif (props.waveformStyle !== undefined) opts.waveformStyle = props.waveformStyle;\n\tif (props.height !== undefined) opts.height = props.height;\n\tif (props.samples !== undefined) opts.samples = props.samples;\n\tif (props.barWidth !== undefined) opts.barWidth = props.barWidth;\n\tif (props.barSpacing !== undefined) opts.barSpacing = props.barSpacing;\n\tif (props.waveform !== undefined && props.waveform !== null) {\n\t\topts.waveform = props.waveform;\n\t}\n\n\t/* Colours */\n\tif (props.colorPreset !== undefined) opts.colorPreset = props.colorPreset;\n\tif (props.waveformColor !== undefined) opts.waveformColor = props.waveformColor;\n\tif (props.progressColor !== undefined) opts.progressColor = props.progressColor;\n\tif (props.buttonColor !== undefined) opts.buttonColor = props.buttonColor;\n\tif (props.buttonHoverColor !== undefined) opts.buttonHoverColor = props.buttonHoverColor;\n\tif (props.textColor !== undefined) opts.textColor = props.textColor;\n\tif (props.textSecondaryColor !== undefined) opts.textSecondaryColor = props.textSecondaryColor;\n\tif (props.backgroundColor !== undefined) opts.backgroundColor = props.backgroundColor;\n\tif (props.borderColor !== undefined) opts.borderColor = props.borderColor;\n\n\t/* Playback controls */\n\tif (props.playbackRate !== undefined) opts.playbackRate = props.playbackRate;\n\tif (props.showPlaybackSpeed !== undefined) opts.showPlaybackSpeed = props.showPlaybackSpeed;\n\tif (props.playbackRates !== undefined) opts.playbackRates = props.playbackRates;\n\n\t/* UI toggles */\n\tif (props.showControls !== undefined) opts.showControls = props.showControls;\n\tif (props.showInfo !== undefined) opts.showInfo = props.showInfo;\n\tif (props.showTime !== undefined) opts.showTime = props.showTime;\n\tif (props.showHoverTime !== undefined) opts.showHoverTime = props.showHoverTime;\n\tif (props.showBPM !== undefined) opts.showBPM = props.showBPM;\n\tif (props.buttonAlign !== undefined) opts.buttonAlign = props.buttonAlign;\n\n\t/* Markers */\n\tif (props.markers !== undefined) opts.markers = props.markers;\n\tif (props.showMarkers !== undefined) opts.showMarkers = props.showMarkers;\n\n\t/* Content metadata */\n\tif (props.title !== undefined) opts.title = props.title;\n\tif (props.subtitle !== undefined) opts.subtitle = props.subtitle;\n\tif (props.artwork !== undefined) opts.artwork = props.artwork;\n\tif (props.album !== undefined) opts.album = props.album;\n\n\t/* Behaviour */\n\tif (props.autoplay !== undefined) opts.autoplay = props.autoplay;\n\tif (props.singlePlay !== undefined) opts.singlePlay = props.singlePlay;\n\tif (props.playOnSeek !== undefined) opts.playOnSeek = props.playOnSeek;\n\tif (props.enableMediaSession !== undefined) opts.enableMediaSession = props.enableMediaSession;\n\n\t/* Icons */\n\tif (props.playIcon !== undefined) opts.playIcon = props.playIcon;\n\tif (props.pauseIcon !== undefined) opts.pauseIcon = props.pauseIcon;\n\n\t/* Callbacks — wired into the library's option-level callbacks\n\t * rather than custom-event listeners. The library invokes\n\t * these synchronously on the respective state change. */\n\tif (props.onLoad) opts.onLoad = props.onLoad;\n\tif (props.onPlay) opts.onPlay = props.onPlay;\n\tif (props.onPause) opts.onPause = props.onPause;\n\tif (props.onEnd) opts.onEnd = props.onEnd;\n\tif (props.onTimeUpdate) opts.onTimeUpdate = props.onTimeUpdate;\n\tif (props.onError) opts.onError = props.onError;\n\n\treturn opts;\n}\n\n/**\n * `WaveformPlayer` — React component wrapping\n * `@arraypress/waveform-player`.\n *\n * Render at the spot you want a waveform-driven audio player to\n * appear. The container `<div>` is rendered immediately for layout;\n * the actual player UI hydrates in once the library loads\n * client-side.\n *\n * @example Basic\n * <WaveformPlayer url=\"/audio/track.mp3\" title=\"My Track\" />\n *\n * @example With ref for imperative control\n * const ref = useRef<WaveformPlayerHandle>(null);\n * <WaveformPlayer ref={ref} url={url} />\n * <button onClick={() => ref.current?.togglePlay()}>Play/Pause</button>\n */\nexport const WaveformPlayer = forwardRef<WaveformPlayerHandle, WaveformPlayerProps>(\n\tfunction WaveformPlayer(props, ref: ForwardedRef<WaveformPlayerHandle>) {\n\t\tconst containerRef = useRef<HTMLDivElement | null>(null);\n\t\tconst instanceRef = useRef<unknown>(null);\n\n\t\t/**\n\t\t * Mount / re-mount lifecycle.\n\t\t *\n\t\t * The dep array intentionally contains EVERY prop the library\n\t\t * uses at construction time. When any of them change, this\n\t\t * effect tears down the old instance and creates a new one\n\t\t * with the updated options. That's simpler and more correct\n\t\t * than trying to partial-update the live instance, and the\n\t\t * library has built-in caches (waveform peaks keyed by URL)\n\t\t * that make same-URL re-mounts cheap.\n\t\t */\n\t\tuseEffect(() => {\n\t\t\tlet cancelled = false;\n\t\t\tlet localInstance: { destroy?: () => void } | null = null;\n\n\t\t\t/* The library is browser-only. Defer the import until we're\n\t\t\t * actually mounting client-side so SSR / RSC don't try to\n\t\t\t * evaluate the audio + canvas + fetch surface on the server.\n\t\t\t */\n\t\t\tvoid import('@arraypress/waveform-player')\n\t\t\t\t.then((mod) => {\n\t\t\t\t\tif (cancelled) return;\n\t\t\t\t\tconst container = containerRef.current;\n\t\t\t\t\tif (!container) return;\n\n\t\t\t\t\tconst WaveformPlayerClass = (mod.default ?? (mod as { WaveformPlayer?: unknown }).WaveformPlayer) as {\n\t\t\t\t\t\tnew (el: HTMLElement, opts: Record<string, unknown>): { destroy?: () => void };\n\t\t\t\t\t};\n\n\t\t\t\t\tif (typeof WaveformPlayerClass !== 'function') {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t'[waveform-player-react] Failed to resolve WaveformPlayer constructor from module.'\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst opts = buildLibraryOptions(props);\n\t\t\t\t\tlocalInstance = new WaveformPlayerClass(container, opts);\n\t\t\t\t\tinstanceRef.current = localInstance;\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error('[waveform-player-react] Failed to load library:', err);\n\t\t\t\t});\n\n\t\t\treturn () => {\n\t\t\t\tcancelled = true;\n\t\t\t\tconst current = localInstance ?? (instanceRef.current as { destroy?: () => void } | null);\n\t\t\t\tif (current && typeof current.destroy === 'function') {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcurrent.destroy();\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.warn('[waveform-player-react] destroy() threw:', err);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinstanceRef.current = null;\n\t\t\t};\n\t\t\t/* Re-mount on any prop change. Listed exhaustively rather\n\t\t\t * than spread to make the intent explicit and to keep the\n\t\t\t * lint rule happy. Callbacks intentionally NOT in deps:\n\t\t\t * a parent re-rendering with a fresh inline function\n\t\t\t * shouldn't tear the player down. */\n\t\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\t}, [\n\t\t\tprops.url,\n\t\t\tprops.audioMode,\n\t\t\tprops.preload,\n\t\t\tprops.waveformStyle,\n\t\t\tprops.height,\n\t\t\tprops.samples,\n\t\t\tprops.barWidth,\n\t\t\tprops.barSpacing,\n\t\t\tprops.waveform,\n\t\t\tprops.colorPreset,\n\t\t\tprops.waveformColor,\n\t\t\tprops.progressColor,\n\t\t\tprops.buttonColor,\n\t\t\tprops.buttonHoverColor,\n\t\t\tprops.textColor,\n\t\t\tprops.textSecondaryColor,\n\t\t\tprops.backgroundColor,\n\t\t\tprops.borderColor,\n\t\t\tprops.playbackRate,\n\t\t\tprops.showPlaybackSpeed,\n\t\t\tprops.playbackRates,\n\t\t\tprops.showControls,\n\t\t\tprops.showInfo,\n\t\t\tprops.showTime,\n\t\t\tprops.showHoverTime,\n\t\t\tprops.showBPM,\n\t\t\tprops.buttonAlign,\n\t\t\tprops.markers,\n\t\t\tprops.showMarkers,\n\t\t\tprops.title,\n\t\t\tprops.subtitle,\n\t\t\tprops.artwork,\n\t\t\tprops.album,\n\t\t\tprops.autoplay,\n\t\t\tprops.singlePlay,\n\t\t\tprops.playOnSeek,\n\t\t\tprops.enableMediaSession,\n\t\t\tprops.playIcon,\n\t\t\tprops.pauseIcon,\n\t\t]);\n\n\t\t/**\n\t\t * Expose an imperative handle on the forwarded ref. Each\n\t\t * method is a thin pass-through to the live instance — if the\n\t\t * instance hasn't mounted yet (still loading async), calls are\n\t\t * no-ops (`pause`, `seekTo`, etc. return `undefined`).\n\t\t */\n\t\tuseImperativeHandle(\n\t\t\tref,\n\t\t\t() => ({\n\t\t\t\tplay() {\n\t\t\t\t\tconst inst = instanceRef.current as { play?: () => Promise<void> | undefined } | null;\n\t\t\t\t\treturn inst?.play?.();\n\t\t\t\t},\n\t\t\t\tpause() {\n\t\t\t\t\tconst inst = instanceRef.current as { pause?: () => void } | null;\n\t\t\t\t\tinst?.pause?.();\n\t\t\t\t},\n\t\t\t\ttogglePlay() {\n\t\t\t\t\tconst inst = instanceRef.current as { togglePlay?: () => void } | null;\n\t\t\t\t\tinst?.togglePlay?.();\n\t\t\t\t},\n\t\t\t\tseekTo(seconds) {\n\t\t\t\t\tconst inst = instanceRef.current as { seekTo?: (s: number) => void } | null;\n\t\t\t\t\tinst?.seekTo?.(seconds);\n\t\t\t\t},\n\t\t\t\tseekToPercent(percent) {\n\t\t\t\t\tconst inst = instanceRef.current as { seekToPercent?: (p: number) => void } | null;\n\t\t\t\t\tinst?.seekToPercent?.(percent);\n\t\t\t\t},\n\t\t\t\tsetVolume(volume) {\n\t\t\t\t\tconst inst = instanceRef.current as { setVolume?: (v: number) => void } | null;\n\t\t\t\t\tinst?.setVolume?.(volume);\n\t\t\t\t},\n\t\t\t\tsetPlaybackRate(rate) {\n\t\t\t\t\tconst inst = instanceRef.current as { setPlaybackRate?: (r: number) => void } | null;\n\t\t\t\t\tinst?.setPlaybackRate?.(rate);\n\t\t\t\t},\n\t\t\t\tsetPlayingState(playing) {\n\t\t\t\t\tconst inst = instanceRef.current as { setPlayingState?: (p: boolean) => void } | null;\n\t\t\t\t\tinst?.setPlayingState?.(playing);\n\t\t\t\t},\n\t\t\t\tsetProgress(currentTime, duration) {\n\t\t\t\t\tconst inst = instanceRef.current as {\n\t\t\t\t\t\tsetProgress?: (c: number, d: number) => void;\n\t\t\t\t\t} | null;\n\t\t\t\t\tinst?.setProgress?.(currentTime, duration);\n\t\t\t\t},\n\t\t\t\tasync loadTrack(url, title, subtitle, options) {\n\t\t\t\t\tconst inst = instanceRef.current as {\n\t\t\t\t\t\tloadTrack?: (\n\t\t\t\t\t\t\tu: string,\n\t\t\t\t\t\t\tt?: string,\n\t\t\t\t\t\t\ts?: string,\n\t\t\t\t\t\t\to?: Record<string, unknown>\n\t\t\t\t\t\t) => Promise<void>;\n\t\t\t\t\t} | null;\n\t\t\t\t\tif (!inst?.loadTrack) return;\n\t\t\t\t\tawait inst.loadTrack(url, title, subtitle, options);\n\t\t\t\t},\n\t\t\t\tget instance() {\n\t\t\t\t\treturn instanceRef.current;\n\t\t\t\t},\n\t\t\t}),\n\t\t\t[]\n\t\t);\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={containerRef}\n\t\t\t\tid={props.id}\n\t\t\t\tclassName={['wfp-host', props.className].filter(Boolean).join(' ')}\n\t\t\t\tstyle={props.style}\n\t\t\t/>\n\t\t);\n\t}\n);\n"]}