wavesurf 1.0.0 → 1.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/README.md CHANGED
@@ -1,38 +1,38 @@
1
1
  # wavesurf
2
2
 
3
- A React audio player with WaveSurfer.js waveform visualization, global state management, and mini-player support.
3
+ A production-ready React audio player built on [WaveSurfer.js](https://wavesurfer.xyz/). Features waveform visualization, global state management, a persistent mini-player, and social sharing—everything you need for a music streaming experience.
4
4
 
5
- ## Features
5
+ ## Why wavesurf?
6
6
 
7
- - **WaveSurfer.js Integration** - Beautiful waveform visualization for audio playback
8
- - **Global Audio State** - React Context for managing playback across your app
9
- - **Mini Player** - A persistent bottom/top bar for controlling playback
10
- - **Pre-computed Peaks Support** - Fast loading with pre-generated waveform data
11
- - **Volume Fade-in Effect** - Smooth 3-second volume fade when starting playback
12
- - **Volume Persistence** - Remember user's volume preference via localStorage
13
- - **Lazy Loading** - Load waveforms only when visible using IntersectionObserver
14
- - **Mobile Responsive** - Adapts to different screen sizes
15
- - **CSS Variables** - Easy theming with CSS custom properties
16
- - **TypeScript** - Full TypeScript support with exported types
7
+ Building a good audio player is harder than it looks. You need:
8
+
9
+ - **Global state** so only one song plays at a time (like Spotify)
10
+ - **Waveform visualization** that's performant and interactive
11
+ - **A persistent mini-player** that stays visible while users browse
12
+ - **Volume fade-in** so playback doesn't blast at full volume
13
+ - **Mobile responsiveness** across all screen sizes
14
+ - **Lazy loading** so pages with many tracks don't lag
15
+
16
+ wavesurf handles all of this out of the box, so you can focus on your actual product.
17
17
 
18
18
  ## Installation
19
19
 
20
20
  ```bash
21
21
  npm install wavesurf wavesurfer.js
22
- # or
23
- yarn add wavesurf wavesurfer.js
24
- # or
25
- pnpm add wavesurf wavesurfer.js
26
22
  ```
27
23
 
24
+ > **Why wavesurfer.js is a peer dependency:** You might already have it in your project, or want to control the version. Making it a peer dependency prevents duplicate bundles and version conflicts.
25
+
28
26
  ## Quick Start
29
27
 
30
- ### 1. Wrap your app with the provider
28
+ ### 1. Add the Provider
29
+
30
+ Wrap your app (or the part that needs audio) with `AudioPlayerProvider`:
31
31
 
32
32
  ```tsx
33
33
  import { AudioPlayerProvider } from 'wavesurf';
34
34
 
35
- function App() {
35
+ export default function App() {
36
36
  return (
37
37
  <AudioPlayerProvider>
38
38
  <YourApp />
@@ -41,12 +41,15 @@ function App() {
41
41
  }
42
42
  ```
43
43
 
44
- ### 2. Add the MiniPlayer component
44
+ ### 2. Add the Mini Player
45
+
46
+ Place `MiniPlayer` in your layout—it appears automatically when a song plays:
45
47
 
46
48
  ```tsx
47
49
  import { MiniPlayer } from 'wavesurf';
50
+ import 'wavesurf/styles.css';
48
51
 
49
- function Layout({ children }) {
52
+ export default function Layout({ children }) {
50
53
  return (
51
54
  <div>
52
55
  {children}
@@ -56,25 +59,24 @@ function Layout({ children }) {
56
59
  }
57
60
  ```
58
61
 
59
- ### 3. Use the WaveformPlayer for songs
62
+ ### 3. Display Songs with WaveformPlayer
60
63
 
61
64
  ```tsx
62
65
  import { WaveformPlayer } from 'wavesurf';
63
- import 'wavesurf/styles.css';
64
66
 
65
- function SongList({ songs }) {
67
+ function TrackList({ tracks }) {
66
68
  return (
67
69
  <div>
68
- {songs.map((song) => (
70
+ {tracks.map((track) => (
69
71
  <WaveformPlayer
70
- key={song.id}
72
+ key={track.id}
71
73
  song={{
72
- id: song.id,
73
- title: song.title,
74
- artist: song.artist,
75
- audioUrl: song.url,
76
- duration: song.duration,
77
- peaks: song.peaks, // Optional: pre-computed waveform data
74
+ id: track.id,
75
+ title: track.title,
76
+ artist: track.artist,
77
+ audioUrl: track.url,
78
+ duration: track.duration,
79
+ peaks: track.peaks, // Optional but recommended
78
80
  }}
79
81
  />
80
82
  ))}
@@ -83,164 +85,265 @@ function SongList({ songs }) {
83
85
  }
84
86
  ```
85
87
 
86
- ## API Reference
88
+ That's it. Click play on any track, and the mini-player appears. Click another track, and it seamlessly switches.
89
+
90
+ ---
91
+
92
+ ## Architecture & Design Decisions
93
+
94
+ ### Global Audio Context
95
+
96
+ **Problem:** In a typical music app, you have multiple track listings, album pages, and a persistent player bar. Without global state, you'd have multiple `<audio>` elements fighting each other.
97
+
98
+ **Solution:** wavesurf uses React Context to maintain a single audio source. When you call `play()` from anywhere in your app, it:
99
+
100
+ 1. Pauses any currently playing audio
101
+ 2. Loads the new track
102
+ 3. Starts playback with a volume fade-in
103
+ 4. Notifies all `WaveformPlayer` components to update their UI
104
+
105
+ ```tsx
106
+ // Any component can control playback
107
+ const { play, pause, currentSong, isPlaying } = useAudioPlayer();
108
+ ```
109
+
110
+ ### Waveform Visualization (Why WaveSurfer.js?)
111
+
112
+ **Problem:** Audio waveforms require decoding audio data and rendering thousands of bars. Doing this poorly kills performance.
113
+
114
+ **Solution:** WaveSurfer.js is the industry standard for web audio visualization. It handles:
115
+
116
+ - Efficient canvas rendering
117
+ - Audio decoding
118
+ - Responsive resize handling
119
+ - Click-to-seek interactions
120
+
121
+ wavesurf wraps WaveSurfer.js with React lifecycle management, so you don't deal with manual cleanup or memory leaks.
122
+
123
+ ### Pre-computed Peaks (Performance)
124
+
125
+ **Problem:** Decoding audio to generate waveforms is slow—especially for longer tracks or pages with many songs. Users see loading spinners everywhere.
126
+
127
+ **Solution:** Generate peaks once (server-side), store them, and pass them to wavesurf:
128
+
129
+ ```tsx
130
+ <WaveformPlayer
131
+ song={{
132
+ id: '1',
133
+ title: 'My Song',
134
+ audioUrl: '/audio/song.mp3',
135
+ duration: 245,
136
+ peaks: [0.1, 0.3, 0.5, 0.8, ...], // Pre-computed!
137
+ }}
138
+ />
139
+ ```
140
+
141
+ When peaks are provided:
142
+ - **No audio decoding needed** — waveform renders instantly
143
+ - **No network request for audio** — until the user clicks play
144
+ - **Pages load faster** — even with 50+ tracks
145
+
146
+ #### How to Generate Peaks
147
+
148
+ Using [audiowaveform](https://github.com/bbc/audiowaveform) (recommended):
149
+
150
+ ```bash
151
+ # Install
152
+ brew install audiowaveform # macOS
153
+ apt install audiowaveform # Ubuntu
154
+
155
+ # Generate peaks
156
+ audiowaveform -i song.mp3 -o peaks.json --pixels-per-second 10 -b 8
157
+ ```
158
+
159
+ Or server-side with FFmpeg/Node.js—compute once when uploading audio, store in your database.
160
+
161
+ ### Volume Fade-in (UX)
162
+
163
+ **Problem:** Clicking play and getting blasted with sudden audio is jarring. Users instinctively reach for the volume.
164
+
165
+ **Solution:** wavesurf fades volume from 0 to the user's set level over 3 seconds (configurable). This:
166
+
167
+ - Creates a professional, polished feel
168
+ - Prevents startling users
169
+ - Matches how streaming services behave
170
+
171
+ ```tsx
172
+ <AudioPlayerProvider config={{
173
+ fadeInEnabled: true, // default: true
174
+ fadeInDuration: 3000, // default: 3000ms
175
+ }}>
176
+ ```
177
+
178
+ ### Volume Persistence (UX)
179
+
180
+ **Problem:** Users set their volume, navigate to another page, and it resets.
181
+
182
+ **Solution:** Volume is automatically saved to localStorage and restored on page load.
183
+
184
+ ```tsx
185
+ <AudioPlayerProvider config={{
186
+ persistVolume: true, // default: true
187
+ storageKey: 'myAppVolume', // default: 'audioPlayerVolume'
188
+ defaultVolume: 0.8, // default: 1
189
+ }}>
190
+ ```
191
+
192
+ ### Lazy Loading (Performance)
193
+
194
+ **Problem:** A page with 20 tracks means 20 WaveSurfer instances initializing at once, causing jank.
195
+
196
+ **Solution:** wavesurf uses IntersectionObserver to only initialize waveforms when they scroll into view:
197
+
198
+ ```tsx
199
+ <WaveformPlayer
200
+ song={song}
201
+ lazyLoad={true} // default: true
202
+ />
203
+ ```
204
+
205
+ Tracks off-screen are just empty containers until needed.
206
+
207
+ ### Mini Player (UX Pattern)
208
+
209
+ **Problem:** Users want to browse your site while listening. A player embedded in the track list disappears when they navigate.
210
+
211
+ **Solution:** The `MiniPlayer` component is a fixed bar (bottom or top) that:
212
+
213
+ - Appears when playback starts
214
+ - Shows current track, progress, volume controls
215
+ - Has its own mini waveform for seeking
216
+ - Stays visible during navigation
217
+ - Can be closed by the user
218
+
219
+ ```tsx
220
+ <MiniPlayer
221
+ position="bottom" // or "top"
222
+ showVolume={true} // auto-hidden on mobile
223
+ showClose={true}
224
+ onClose={() => console.log('Player closed')}
225
+ />
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Components
87
231
 
88
232
  ### AudioPlayerProvider
89
233
 
90
- Wrap your application with this provider to enable global audio state management.
234
+ Wraps your app to provide global audio state.
91
235
 
92
236
  ```tsx
93
- <AudioPlayerProvider config={config}>
237
+ <AudioPlayerProvider config={{
238
+ fadeInEnabled: true,
239
+ fadeInDuration: 3000,
240
+ persistVolume: true,
241
+ storageKey: 'audioPlayerVolume',
242
+ defaultVolume: 1,
243
+ onPlay: (song) => analytics.track('play', song),
244
+ onPause: () => analytics.track('pause'),
245
+ onEnd: () => analytics.track('songEnded'),
246
+ onTimeUpdate: (time) => {},
247
+ }}>
94
248
  {children}
95
249
  </AudioPlayerProvider>
96
250
  ```
97
251
 
98
- #### Config Options
99
-
100
- | Property | Type | Default | Description |
101
- |----------|------|---------|-------------|
102
- | `fadeInEnabled` | `boolean` | `true` | Enable volume fade-in effect on play |
103
- | `fadeInDuration` | `number` | `3000` | Duration of fade-in in milliseconds |
104
- | `persistVolume` | `boolean` | `true` | Persist volume to localStorage |
105
- | `storageKey` | `string` | `'audioPlayerVolume'` | localStorage key for volume |
106
- | `defaultVolume` | `number` | `1` | Default volume level (0-1) |
107
- | `onPlay` | `(song: Song) => void` | - | Callback when playback starts |
108
- | `onPause` | `() => void` | - | Callback when playback pauses |
109
- | `onEnd` | `() => void` | - | Callback when song ends |
110
- | `onTimeUpdate` | `(time: number) => void` | - | Callback on time update |
111
-
112
252
  ### useAudioPlayer Hook
113
253
 
114
- Access the audio player state and controls from any component.
254
+ Access state and controls from any component:
115
255
 
116
256
  ```tsx
117
- import { useAudioPlayer } from 'wavesurf';
118
-
119
- function CustomControls() {
120
- const {
121
- // State
122
- currentSong,
123
- isPlaying,
124
- currentTime,
125
- duration,
126
- volume,
127
- displayVolume,
128
- isFadingIn,
129
- // Actions
130
- play,
131
- pause,
132
- togglePlay,
133
- seek,
134
- setVolume,
135
- stop,
136
- } = useAudioPlayer();
137
-
138
- return (
139
- <button onClick={togglePlay}>
140
- {isPlaying ? 'Pause' : 'Play'}
141
- </button>
142
- );
143
- }
257
+ const {
258
+ // State
259
+ currentSong, // Song | null
260
+ isPlaying, // boolean
261
+ currentTime, // number (seconds)
262
+ duration, // number (seconds)
263
+ volume, // number (0-1, user's saved volume)
264
+ displayVolume, // number (0-1, actual volume during fade)
265
+ isFadingIn, // boolean
266
+
267
+ // Actions
268
+ play, // (song: Song) => void
269
+ pause, // () => void
270
+ togglePlay, // () => void
271
+ seek, // (time: number) => void
272
+ setVolume, // (volume: number) => void
273
+ stop, // () => void
274
+ } = useAudioPlayer();
144
275
  ```
145
276
 
146
277
  ### WaveformPlayer
147
278
 
148
- Displays a waveform visualization with play controls for a single song.
279
+ Displays a track with waveform visualization:
149
280
 
150
281
  ```tsx
151
282
  <WaveformPlayer
152
- song={song}
283
+ song={{
284
+ id: string,
285
+ title: string,
286
+ artist?: string,
287
+ album?: string,
288
+ audioUrl: string,
289
+ duration?: number,
290
+ peaks?: number[],
291
+ }}
153
292
  waveformConfig={{
154
293
  waveColor: '#666666',
155
294
  progressColor: '#D4AF37',
295
+ cursorColor: '#D4AF37',
296
+ barWidth: 2,
297
+ barGap: 1,
298
+ barRadius: 2,
156
299
  height: 60,
157
300
  }}
158
301
  lazyLoad={true}
159
302
  showTime={true}
160
- className="my-player"
161
- renderHeader={(song, isPlaying) => (
162
- <div>{song.title} {isPlaying && '▶'}</div>
163
- )}
164
- renderControls={(song, isPlaying) => (
165
- <button>Share</button>
166
- )}
303
+ className=""
304
+ renderHeader={(song, isPlaying) => <CustomHeader />}
305
+ renderControls={(song, isPlaying) => <CustomControls />}
167
306
  />
168
307
  ```
169
308
 
170
- #### Props
171
-
172
- | Prop | Type | Default | Description |
173
- |------|------|---------|-------------|
174
- | `song` | `Song` | required | The song object to play |
175
- | `waveformConfig` | `WaveformConfig` | - | Waveform styling options |
176
- | `lazyLoad` | `boolean` | `true` | Enable lazy loading via IntersectionObserver |
177
- | `showTime` | `boolean` | `true` | Show time display below waveform |
178
- | `className` | `string` | `''` | Additional CSS class |
179
- | `renderHeader` | `(song, isPlaying) => ReactNode` | - | Custom header renderer |
180
- | `renderControls` | `(song, isPlaying) => ReactNode` | - | Custom controls renderer |
181
-
182
309
  ### MiniPlayer
183
310
 
184
- A fixed position player bar for persistent playback control.
311
+ Persistent playback bar:
185
312
 
186
313
  ```tsx
187
314
  <MiniPlayer
188
- position="bottom"
315
+ position="bottom" // 'top' | 'bottom'
189
316
  showVolume={true}
190
317
  showClose={true}
191
- onClose={() => console.log('Player closed')}
192
- className="my-mini-player"
193
- waveformConfig={{
194
- progressColor: '#FF0000',
195
- }}
318
+ onClose={() => {}}
319
+ className=""
320
+ waveformConfig={{...}}
196
321
  />
197
322
  ```
198
323
 
199
- #### Props
324
+ ### ShareButtons
200
325
 
201
- | Prop | Type | Default | Description |
202
- |------|------|---------|-------------|
203
- | `position` | `'top' \| 'bottom'` | `'bottom'` | Position on screen |
204
- | `showVolume` | `boolean` | `true` | Show volume control (auto-hidden on mobile) |
205
- | `showClose` | `boolean` | `true` | Show close button |
206
- | `onClose` | `() => void` | - | Callback when close is clicked |
207
- | `className` | `string` | `''` | Additional CSS class |
208
- | `waveformConfig` | `WaveformConfig` | - | Waveform styling options |
326
+ Social sharing for tracks:
209
327
 
210
- ### Song Interface
211
-
212
- ```typescript
213
- interface Song {
214
- id: string; // Unique identifier
215
- title: string; // Display title
216
- artist?: string; // Artist name (optional)
217
- album?: string; // Album name (optional)
218
- audioUrl: string; // URL to the audio file
219
- duration?: number; // Duration in seconds (optional)
220
- peaks?: number[]; // Pre-computed waveform peaks (optional)
221
- }
328
+ ```tsx
329
+ import { ShareButtons } from 'wavesurf';
330
+
331
+ <ShareButtons
332
+ url="https://mysite.com/track/123"
333
+ text="Check out this song!"
334
+ platforms={['facebook', 'twitter', 'whatsapp', 'copy']}
335
+ onShare={(platform, url) => analytics.track('share', { platform })}
336
+ showLabels={false}
337
+ />
222
338
  ```
223
339
 
224
- ### WaveformConfig Interface
340
+ **Available platforms:** `facebook`, `twitter`, `whatsapp`, `linkedin`, `reddit`, `telegram`, `email`, `copy`
225
341
 
226
- ```typescript
227
- interface WaveformConfig {
228
- waveColor?: string; // Color of the waveform (default: '#666666')
229
- progressColor?: string; // Color of played portion (default: '#D4AF37')
230
- cursorColor?: string; // Color of playhead (default: '#D4AF37')
231
- barWidth?: number; // Width of bars in px (default: 2)
232
- barGap?: number; // Gap between bars in px (default: 1)
233
- barRadius?: number; // Border radius of bars (default: 2)
234
- height?: number; // Height in pixels (default: 60)
235
- normalize?: boolean; // Normalize waveform (default: true)
236
- }
237
- ```
342
+ ---
238
343
 
239
- ## Styling & Theming
344
+ ## Styling
240
345
 
241
- ### Using the default styles
242
-
243
- Import the CSS file in your app:
346
+ ### Using Default Styles
244
347
 
245
348
  ```tsx
246
349
  import 'wavesurf/styles.css';
@@ -248,24 +351,27 @@ import 'wavesurf/styles.css';
248
351
 
249
352
  ### Customizing with CSS Variables
250
353
 
251
- Override the CSS variables to customize the appearance:
354
+ Override any of these in your CSS:
252
355
 
253
356
  ```css
254
357
  :root {
255
- /* Colors */
256
- --wsp-wave-color: #888888;
257
- --wsp-progress-color: #FF5500;
258
- --wsp-cursor-color: #FF5500;
358
+ /* Waveform */
359
+ --wsp-wave-color: #666666;
360
+ --wsp-progress-color: #D4AF37;
361
+ --wsp-cursor-color: #D4AF37;
362
+
363
+ /* Backgrounds */
259
364
  --wsp-background: transparent;
365
+ --wsp-background-secondary: rgba(255, 255, 255, 0.05);
260
366
 
261
- /* Button Colors */
262
- --wsp-button-bg: #FF5500;
263
- --wsp-button-bg-hover: #FF7733;
264
- --wsp-button-text: #ffffff;
367
+ /* Buttons */
368
+ --wsp-button-bg: #D4AF37;
369
+ --wsp-button-bg-hover: #e5c04a;
370
+ --wsp-button-text: #000000;
265
371
 
266
- /* Text Colors */
372
+ /* Text */
267
373
  --wsp-text: #ffffff;
268
- --wsp-text-muted: #999999;
374
+ --wsp-text-muted: #a3a3a3;
269
375
 
270
376
  /* Sizing */
271
377
  --wsp-height: 60px;
@@ -273,54 +379,32 @@ Override the CSS variables to customize the appearance:
273
379
  --wsp-button-size: 56px;
274
380
 
275
381
  /* Mini Player */
276
- --wsp-mini-bg: #1a1a1a;
277
- --wsp-mini-border-color: #FF5500;
382
+ --wsp-mini-bg: #0a0a0a;
383
+ --wsp-mini-border-color: #D4AF37;
384
+ --wsp-mini-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
385
+
386
+ /* Transitions */
387
+ --wsp-transition: 150ms ease;
278
388
  }
279
389
  ```
280
390
 
281
- ### Using your own styles
391
+ ### Custom Styling
282
392
 
283
- You can also write completely custom CSS targeting the class names:
393
+ All components use BEM-style class names you can target:
284
394
 
285
- - `.wsp-player` - Main player container
395
+ - `.wsp-player` - WaveformPlayer container
286
396
  - `.wsp-play-button` - Play/pause button
287
397
  - `.wsp-waveform` - Waveform container
288
- - `.wsp-time-display` - Time display
289
- - `.wsp-mini-player` - Mini player container
290
- - `.wsp-mini-play-button` - Mini player play button
291
- - `.wsp-mini-waveform` - Mini player waveform
292
-
293
- ## Pre-computed Peaks
398
+ - `.wsp-time-display` - Time labels
399
+ - `.wsp-mini-player` - MiniPlayer container
400
+ - `.wsp-share-buttons` - ShareButtons container
401
+ - `.wsp-share-button` - Individual share button
294
402
 
295
- For optimal performance, especially with large audio files, you can pre-compute waveform peaks on your server. This eliminates the need to decode audio on the client.
296
-
297
- ### Generating peaks with audiowaveform
298
-
299
- ```bash
300
- # Install audiowaveform (on Ubuntu/Debian)
301
- sudo apt install audiowaveform
302
-
303
- # Generate peaks JSON
304
- audiowaveform -i audio.mp3 -o peaks.json --pixels-per-second 10
305
- ```
306
-
307
- ### Using peaks in your app
308
-
309
- ```tsx
310
- const song = {
311
- id: '1',
312
- title: 'My Song',
313
- audioUrl: '/audio/song.mp3',
314
- duration: 180, // 3 minutes
315
- peaks: peaksData, // Array of numbers from audiowaveform
316
- };
317
-
318
- <WaveformPlayer song={song} />
319
- ```
403
+ ---
320
404
 
321
405
  ## TypeScript
322
406
 
323
- All types are exported from the package:
407
+ All types are exported:
324
408
 
325
409
  ```typescript
326
410
  import type {
@@ -331,18 +415,79 @@ import type {
331
415
  WaveformConfig,
332
416
  WaveformPlayerProps,
333
417
  MiniPlayerProps,
418
+ SharePlatform,
419
+ ShareButtonsProps,
334
420
  } from 'wavesurf';
335
421
  ```
336
422
 
423
+ ---
424
+
425
+ ## Examples
426
+
427
+ ### Custom Play Button (Headless Usage)
428
+
429
+ ```tsx
430
+ function CustomPlayButton({ song }) {
431
+ const { play, pause, currentSong, isPlaying } = useAudioPlayer();
432
+ const isThisSong = currentSong?.id === song.id;
433
+ const playing = isThisSong && isPlaying;
434
+
435
+ return (
436
+ <button onClick={() => playing ? pause() : play(song)}>
437
+ {playing ? 'Pause' : 'Play'}
438
+ </button>
439
+ );
440
+ }
441
+ ```
442
+
443
+ ### Track Card with Share
444
+
445
+ ```tsx
446
+ function TrackCard({ track }) {
447
+ const shareUrl = `https://mysite.com/track/${track.id}`;
448
+
449
+ return (
450
+ <div className="track-card">
451
+ <WaveformPlayer song={track} />
452
+ <ShareButtons
453
+ url={shareUrl}
454
+ text={`Listen to ${track.title}`}
455
+ platforms={['twitter', 'whatsapp', 'copy']}
456
+ />
457
+ </div>
458
+ );
459
+ }
460
+ ```
461
+
462
+ ### Analytics Integration
463
+
464
+ ```tsx
465
+ <AudioPlayerProvider config={{
466
+ onPlay: (song) => {
467
+ analytics.track('song_play', {
468
+ songId: song.id,
469
+ title: song.title,
470
+ });
471
+ },
472
+ onEnd: () => {
473
+ analytics.track('song_completed');
474
+ },
475
+ }}>
476
+ ```
477
+
478
+ ---
479
+
337
480
  ## Browser Support
338
481
 
339
- This package requires browsers that support:
482
+ Requires browsers with:
340
483
  - Web Audio API
341
484
  - CSS Custom Properties
342
- - IntersectionObserver (for lazy loading)
485
+ - IntersectionObserver
343
486
 
344
487
  All modern browsers (Chrome, Firefox, Safari, Edge) are supported.
345
488
 
489
+ ---
490
+
346
491
  ## License
347
492
 
348
- MIT
493
+ MIT © [TheDecipherist](https://github.com/TheDecipherist)